From cd1d46a26b923260b6c87cc93a0723b8166c609e Mon Sep 17 00:00:00 2001 From: "kali kaneko (leap communications)" Date: Fri, 27 Aug 2021 19:45:41 +0200 Subject: [ui] refactor ui --- bitmask.pro | 9 + docs/hacking-qml.rst | 6 + gui/backend.go | 2 +- gui/components/BoldLabel.qml | 21 +++ gui/components/Footer.qml | 63 +++++++ gui/components/Header.qml | 32 ++++ gui/components/Help.qml | 32 ++++ gui/components/Home.qml | 8 + gui/components/Icon.qml | 9 + gui/components/LightLabel.qml | 19 +++ gui/components/Locations.qml | 63 +++++++ gui/components/MainView.qml | 131 +++++++++++++++ gui/components/MaterialButton.qml | 84 ++++++++++ gui/components/Preferences.qml | 23 +++ gui/components/Spinner.qml | 63 +++++++ gui/components/Splash.qml | 64 ++++++++ gui/components/StatusBox.qml | 125 ++++++++++++++ gui/components/Style.qml | 12 ++ gui/components/Systray.qml | 27 +++ gui/components/VPNButtonBase.qml | 56 +++++++ gui/components/VPNMouseArea.qml | 37 +++++ gui/components/VPNState.qml | 193 ++++++++++++++++++++++ gui/components/VPNToggle.qml | 338 ++++++++++++++++++++++++++++++++++++++ gui/components/VerticalSpacer.qml | 6 + gui/gui.qrc | 46 +++++- gui/main.cpp | 21 ++- gui/main.qml | 116 +++++++++++++ gui/providers/providers.json | 2 +- gui/qml/BackgroundImage.qml | 16 +- gui/qml/BridgesItem.qml | 16 +- gui/qml/VPNSwitch.qml | 1 - gui/qml/VpnState.qml | 13 +- gui/qml/main.qml | 65 ++++---- gui/qml/readme.txt | 1 + gui/resources/about.svg | 62 +++++++ gui/resources/arrow-left.svg | 3 + gui/resources/birds.svg | 1 + gui/resources/bridge.png | Bin 0 -> 5543 bytes gui/resources/close.svg | 3 + gui/resources/donate.svg | 4 + gui/resources/gear-fill.svg | 3 + gui/resources/globe.svg | 3 + gui/resources/help.svg | 4 + gui/resources/icon-noshield.svg | 68 ++++++++ gui/resources/location.svg | 58 +++++++ gui/resources/power.svg | 10 ++ gui/resources/quit.svg | 4 + gui/resources/reception-0.svg | 3 + gui/resources/reception-2.svg | 3 + gui/resources/reception-4.svg | 3 + gui/resources/riseup-icon.svg | 80 +++++++++ gui/resources/riseup-logo.png | Bin 0 -> 4943 bytes gui/resources/settings.svg | 62 +++++++ gui/resources/speed-green.svg | 72 ++++++++ gui/resources/speed-red.svg | 72 ++++++++ gui/resources/speed-yellow.svg | 72 ++++++++ gui/resources/spy.gif | Bin 0 -> 2662 bytes gui/resources/tools.svg | 4 + gui/themes/themes.js | 51 ++++++ pkg/backend/init.go | 4 +- pkg/backend/status.go | 1 + 61 files changed, 2301 insertions(+), 69 deletions(-) create mode 100644 docs/hacking-qml.rst create mode 100644 gui/components/BoldLabel.qml create mode 100644 gui/components/Footer.qml create mode 100644 gui/components/Header.qml create mode 100644 gui/components/Help.qml create mode 100644 gui/components/Home.qml create mode 100644 gui/components/Icon.qml create mode 100644 gui/components/LightLabel.qml create mode 100644 gui/components/Locations.qml create mode 100644 gui/components/MainView.qml create mode 100644 gui/components/MaterialButton.qml create mode 100644 gui/components/Preferences.qml create mode 100644 gui/components/Spinner.qml create mode 100644 gui/components/Splash.qml create mode 100644 gui/components/StatusBox.qml create mode 100644 gui/components/Style.qml create mode 100644 gui/components/Systray.qml create mode 100644 gui/components/VPNButtonBase.qml create mode 100644 gui/components/VPNMouseArea.qml create mode 100644 gui/components/VPNState.qml create mode 100644 gui/components/VPNToggle.qml create mode 100644 gui/components/VerticalSpacer.qml create mode 100644 gui/main.qml create mode 100644 gui/qml/readme.txt create mode 100644 gui/resources/about.svg create mode 100644 gui/resources/arrow-left.svg create mode 100644 gui/resources/birds.svg create mode 100644 gui/resources/bridge.png create mode 100644 gui/resources/close.svg create mode 100644 gui/resources/donate.svg create mode 100644 gui/resources/gear-fill.svg create mode 100644 gui/resources/globe.svg create mode 100644 gui/resources/help.svg create mode 100644 gui/resources/icon-noshield.svg create mode 100644 gui/resources/location.svg create mode 100644 gui/resources/power.svg create mode 100644 gui/resources/quit.svg create mode 100644 gui/resources/reception-0.svg create mode 100644 gui/resources/reception-2.svg create mode 100644 gui/resources/reception-4.svg create mode 100644 gui/resources/riseup-icon.svg create mode 100644 gui/resources/riseup-logo.png create mode 100644 gui/resources/settings.svg create mode 100644 gui/resources/speed-green.svg create mode 100644 gui/resources/speed-red.svg create mode 100644 gui/resources/speed-yellow.svg create mode 100644 gui/resources/spy.gif create mode 100644 gui/resources/tools.svg create mode 100644 gui/themes/themes.js diff --git a/bitmask.pro b/bitmask.pro index 627e2d3..2513944 100644 --- a/bitmask.pro +++ b/bitmask.pro @@ -1,6 +1,9 @@ #TARGET = $$BINARY_NAME +QT += quickcontrols2 +CONFIG += c++11 CONFIG += qt staticlib +CONFIG += qtquickcompiler CONFIG+=force_debug_info CONFIG+=debug_and_release #CONFIG+=release @@ -11,6 +14,12 @@ QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.11 QMAKE_TARGET_BUNDLE_PREFIX = se.leap QMAKE_BUNDLE = $$TARGET +# The following define makes your compiler emit warnings if you use +# any feature of Qt which as been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + !defined(VENDOR_PATH, var):VENDOR_PATH="providers/riseup" message("[qmake] VENDOR_PATH: $$VENDOR_PATH") diff --git a/docs/hacking-qml.rst b/docs/hacking-qml.rst new file mode 100644 index 0000000..7e793ba --- /dev/null +++ b/docs/hacking-qml.rst @@ -0,0 +1,6 @@ +QML best practices +================== +* https://github.com/Furkanzmc/QML-Coding-Guide/blob/master/README.md +* lint your qml files:: + + make qmllint diff --git a/gui/backend.go b/gui/backend.go index 04d1293..bc056b9 100644 --- a/gui/backend.go +++ b/gui/backend.go @@ -83,7 +83,7 @@ func InitializeBitmaskContext(provider string, opts.Obfs4 = obfs4 opts.DisableAutostart = disableAutostart opts.StartVPN = startVPN - backend.InitializeBitmaskContext(opts) + go backend.InitializeBitmaskContext(opts) } //export InitializeTestBitmaskContext diff --git a/gui/components/BoldLabel.qml b/gui/components/BoldLabel.qml new file mode 100644 index 0000000..6c6c3c3 --- /dev/null +++ b/gui/components/BoldLabel.qml @@ -0,0 +1,21 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.14 +import "../themes/themes.js" as Theme +import "../themes" + +Label { + FontLoader { + id: boldFont + source: "qrc:/oxanium-bold.ttf" + } + + font.pixelSize: Theme.fontSize * 1.55555 + //font.family: boldFont.name + font.bold: true + //color: Theme.fontColorDark + color: "black" + text: parent.text + //wrapMode: Text.WordWrap + Accessible.name: text + Accessible.role: Accessible.StaticText +} diff --git a/gui/components/Footer.qml b/gui/components/Footer.qml new file mode 100644 index 0000000..2c7c875 --- /dev/null +++ b/gui/components/Footer.qml @@ -0,0 +1,63 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.14 + +ToolBar { + + Material.background: Material.backgroundColor + Material.foreground: "black" + Material.elevation: 0 + visible: stackView.depth > 1 && ctx !== undefined ? false : true + + Item { + + id: footerRow + width: root.width + + ToolButton { + id: gwButton + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 10 + anchors.left: parent.left + anchors.verticalCenterOffset: 5 + icon.source: stackView.depth > 1 ? "" : "../resources/globe.svg" + onClicked: stackView.push("Locations.qml") + } + + Label { + id: locationLabel + anchors.left: gwButton.right + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 5 + text: "Seattle" + } + + Item { + Layout.fillWidth: true + height: gwButton.implicitHeight + } + + Image { + id: bridge + height: 24 + width: 24 + source: "../resources/bridge.png" + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 5 + anchors.right: gwQuality.left + anchors.rightMargin: 10 + } + + Image { + id: gwQuality + height: 24 + width: 24 + source: "../resources/reception-0.svg" + anchors.right: parent.right + anchors.rightMargin: 20 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 5 + } + } +} diff --git a/gui/components/Header.qml b/gui/components/Header.qml new file mode 100644 index 0000000..92f4bdd --- /dev/null +++ b/gui/components/Header.qml @@ -0,0 +1,32 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls.Material 2.1 + +ToolBar { + visible: stackView.depth > 1 + Material.foreground: Material.Black + Material.background: "#ffffff" + Material.elevation: 0 + + contentHeight: settingsButton.implicitHeight + + ToolButton { + id: settingsButton + anchors.left: parent.left + font.pixelSize: Qt.application.font.pixelSize * 1.6 + icon.source: "../resources/arrow-left.svg" + onClicked: { + if (stackView.depth > 1) { + stackView.pop() + } else { + settingsDrawer.open() + } + } + } + + Label { + text: stackView.currentItem.title + anchors.centerIn: parent + } +} diff --git a/gui/components/Help.qml b/gui/components/Help.qml new file mode 100644 index 0000000..aced273 --- /dev/null +++ b/gui/components/Help.qml @@ -0,0 +1,32 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +Page { + title: qsTr("Help") + + Column { + anchors.centerIn: parent + spacing: 10 + + Button { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Donate") + onClicked: stackView.push("Donate.qml") + } + Button { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Terms of Service") + onClicked: stackView.push("Donate.qml") + } + Button { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Contact Support") + onClicked: stackView.push("Donate.qml") + } + Button { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Report bug") + onClicked: stackView.push("Donate.qml") + } + } +} diff --git a/gui/components/Home.qml b/gui/components/Home.qml new file mode 100644 index 0000000..c9eab2a --- /dev/null +++ b/gui/components/Home.qml @@ -0,0 +1,8 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 + +Page { + + StatusBox {} +} diff --git a/gui/components/Icon.qml b/gui/components/Icon.qml new file mode 100644 index 0000000..5dfd6ee --- /dev/null +++ b/gui/components/Icon.qml @@ -0,0 +1,9 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.14 +import "../themes/themes.js" as Theme + +Image { + sourceSize.height: 24 + sourceSize.width: 24 + fillMode: Image.PreserveAspectFit +} diff --git a/gui/components/LightLabel.qml b/gui/components/LightLabel.qml new file mode 100644 index 0000000..78f82b6 --- /dev/null +++ b/gui/components/LightLabel.qml @@ -0,0 +1,19 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.14 +import "../themes/themes.js" as Theme + +Text { + font.pixelSize: Theme.fontSize - 2 + font.family: Theme.fontFamily + color: Theme.fontColor + width: parent.width * 0.80 + text: parent.text + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + //lineHeightMode: Text.FixedHeight + wrapMode: Text.Wrap + + Accessible.role: Accessible.StaticText + Accessible.name: text +} diff --git a/gui/components/Locations.qml b/gui/components/Locations.qml new file mode 100644 index 0000000..d3e0f5a --- /dev/null +++ b/gui/components/Locations.qml @@ -0,0 +1,63 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +import "../themes/themes.js" as Theme + +Page { + title: qsTr("Select Location") + + ListView { + id: gwList + focus: true + currentIndex: -1 + anchors.fill: parent + spacing: 1 + + 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() + } + } + } + + model: ListModel { + ListElement { + text: qsTr("Paris") + triggered: function () {} + icon: "../resources/reception-4.svg" + } + ListElement { + text: qsTr("Montreal") + triggered: function () {} + icon: "../resources/reception-4.svg" + } + ListElement { + text: qsTr("Seattle") + triggered: function () {} + icon: "../resources/reception-2.svg" + } + } + } +} diff --git a/gui/components/MainView.qml b/gui/components/MainView.qml new file mode 100644 index 0000000..7ce723d --- /dev/null +++ b/gui/components/MainView.qml @@ -0,0 +1,131 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.14 + +Page { + + StackView { + id: stackView + anchors.fill: parent + initialItem: Home {} + } + + Drawer { + id: settingsDrawer + + width: Math.min(root.width, root.height) / 3 * 2 + height: root.height + + ListView { + focus: true + currentIndex: -1 + anchors.fill: parent + + delegate: ItemDelegate { + width: parent.width + text: model.text + highlighted: ListView.isCurrentItem + icon.color: "transparent" + icon.source: model.icon + onClicked: { + settingsDrawer.close() + model.triggered() + } + } + + model: ListModel { + ListElement { + text: qsTr("Preferences") + icon: "../resources/tools.svg" + triggered: function () { + stackView.push("Preferences.qml") + } + } + ListElement { + text: qsTr("Donate") + icon: "../resources/donate.svg" + triggered: function () { + aboutDialog.open() + } + } + ListElement { + text: qsTr("Help") + icon: "../resources/help.svg" + triggered: function () { + stackView.push("Help.qml") + settingsDrawer.close() + } + } // -> can link to another dialog with report bug / support / contribute / FAQ + ListElement { + text: qsTr("About") + icon: "../resources/about.svg" + triggered: function () { + aboutDialog.open() + } + } + ListElement { + text: qsTr("Quit") + icon: "../resources/quit.svg" + triggered: function () { + Qt.callLater(backend.quit) + } + } + } + + ScrollIndicator.vertical: ScrollIndicator {} + } + } + + 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 {} + footer: Footer {} + + Dialog { + id: aboutDialog + title: qsTr("About") + Label { + anchors.fill: parent + text: qsTr("RiseupVPN\nhttps://riseupvpn.net/vpn") + horizontalAlignment: Text.AlignHCenter + } + + standardButtons: StandardButton.Ok + } +} diff --git a/gui/components/MaterialButton.qml b/gui/components/MaterialButton.qml new file mode 100644 index 0000000..25911f3 --- /dev/null +++ b/gui/components/MaterialButton.qml @@ -0,0 +1,84 @@ +import QtQuick 2.12 +import QtQuick.Templates 2.12 as T +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 +import QtQuick.Controls.Material 2.12 +import QtQuick.Controls.Material.impl 2.12 + +T.Button { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + topInset: 6 + bottomInset: 6 + padding: 12 + horizontalPadding: padding - 4 + spacing: 6 + + icon.width: 24 + icon.height: 24 + icon.color: !enabled ? Material.hintTextColor : flat + && highlighted ? Material.accentColor : highlighted ? Material.primaryHighlightedTextColor : Material.foreground + + Material.elevation: flat ? control.down + || control.hovered ? 2 : 0 : control.down ? 8 : 2 + Material.background: flat ? "transparent" : undefined + + contentItem: IconLabel { + spacing: control.spacing + mirrored: control.mirrored + display: control.display + + icon: control.icon + text: control.text + font: control.font + + color: !control.enabled ? control.Material.hintTextColor : control.flat + && control.highlighted ? control.Material.accentColor : control.highlighted ? control.Material.primaryHighlightedTextColor : control.Material.foreground + } + + background: Rectangle { + implicitWidth: 64 + implicitHeight: control.Material.buttonHeight + + radius: 4 + border.color: "black" + border.width: 1 + color: !control.enabled ? control.Material.buttonDisabledColor : control.highlighted ? control.Material.highlightedButtonColor : control.Material.buttonColor + + PaddedRectangle { + y: parent.height - 4 + width: parent.width + height: 4 + radius: 2 + topPadding: -2 + clip: true + visible: control.checkable && (!control.highlighted || control.flat) + color: control.checked + && control.enabled ? control.Material.accentColor : control.Material.secondaryTextColor + } + + // The layer is disabled when the button color is transparent so you can do + // Material.background: "transparent" and get a proper flat button without needing + // to set Material.elevation as well + layer.enabled: control.enabled && control.Material.buttonColor.a > 0 + layer.effect: ElevationEffect { + elevation: control.Material.elevation + } + + Ripple { + clipRadius: 2 + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: control.down || control.visualFocus || control.hovered + color: control.flat + && control.highlighted ? control.Material.highlightedRippleColor : control.Material.rippleColor + } + } +} diff --git a/gui/components/Preferences.qml b/gui/components/Preferences.qml new file mode 100644 index 0000000..e5c4828 --- /dev/null +++ b/gui/components/Preferences.qml @@ -0,0 +1,23 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +Page { + title: qsTr("Preferences") + + Column { + spacing: 2 + topPadding: root.width * 0.2 + leftPadding: root.width * 0.15 + rightPadding: root.width * 0.15 + + Label { + text: qsTr("Anti-censorship") + font.bold: true + } + + CheckBox { + checked: false + text: qsTr("Use Bridges") + } + } +} diff --git a/gui/components/Spinner.qml b/gui/components/Spinner.qml new file mode 100644 index 0000000..9fc535f --- /dev/null +++ b/gui/components/Spinner.qml @@ -0,0 +1,63 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 + +BusyIndicator { + + id: control + + anchors.horizontalCenter: parent.horizontalCenter + + contentItem: Item { + implicitWidth: 64 + implicitHeight: 64 + + Item { + id: item + x: parent.width / 2 - 32 + y: parent.height / 2 - 32 + width: 64 + height: 64 + opacity: control.running ? 1 : 0 + + Behavior on opacity { + OpacityAnimator { + duration: 250 + } + } + + RotationAnimator { + target: item + running: control.visible && control.running + from: 0 + to: 360 + loops: Animation.Infinite + duration: 6200 + } + + Repeater { + id: repeater + model: 6 + + Rectangle { + x: item.width / 2 - width / 2 + y: item.height / 2 - height / 2 + implicitWidth: 10 + implicitHeight: 10 + radius: 5 + color: "#21be2b" + transform: [ + Translate { + y: -Math.min(item.width, item.height) * 0.5 + 5 + }, + Rotation { + angle: index / repeater.count * 360 + origin.x: 5 + origin.y: 5 + } + ] + } + } + } + } +} diff --git a/gui/components/Splash.qml b/gui/components/Splash.qml new file mode 100644 index 0000000..b494494 --- /dev/null +++ b/gui/components/Splash.qml @@ -0,0 +1,64 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 + +Page { + id: splash + property int timeoutInterval: 1600 + + Column { + width: parent.width * 0.8 + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 24 + + VerticalSpacer { + visible: true + height: root.height * 0.10 + } + + Image { + id: connectionImage + height: 200 + anchors.horizontalCenter: parent.horizontalCenter + source: "../resources/icon-noshield.svg" + fillMode: Image.PreserveAspectFit + } + + Spinner {} + } + + Timer { + id: splashTimer + } + + function delay(delayTime, cb) { + splashTimer.interval = delayTime + splashTimer.repeat = false + splashTimer.triggered.connect(cb) + splashTimer.start() + } + + function loadMainViewWhenReady() { + console.debug("ready?") + if (ctx && ctx.isReady) { + console.debug("ready?", ctx.isReady) + // FIXME check errors == None + loader.source = "MainView.qml" + } else { + delay(100, loadMainViewWhenReady) + } + } + + Timer { + interval: timeoutInterval + running: true + repeat: false + onTriggered: { + loadMainViewWhenReady() + } + } + + Component.onCompleted: { + + } +} diff --git a/gui/components/StatusBox.qml b/gui/components/StatusBox.qml new file mode 100644 index 0000000..fa24cd8 --- /dev/null +++ b/gui/components/StatusBox.qml @@ -0,0 +1,125 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtGraphicalEffects 1.14 +import QtQuick.Layouts 1.14 + +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 { + id: statusbox + anchors.fill: parent + + VPNState { + id: vpn + } + + Rectangle { + id: statusBoxBackground + anchors.fill: parent + anchors.margins: 20 + anchors.bottomMargin: 30 + height: 300 + radius: 10 + color: Theme.bgColor + border.color: Theme.accentOff + border.width: 2 + antialiasing: true + } + + ToolButton { + id: settingsButton + objectName: "settingsButton" + opacity: 1 + + font.pixelSize: Qt.application.font.pixelSize * 1.6 + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: Theme.windowMargin + 10 + anchors.leftMargin: Theme.windowMargin + 10 + + onClicked: { + if (stackView.depth > 1) { + stackView.pop() + } else { + settingsDrawer.open() + } + } + + Icon { + id: settingsImage + width: 24 + height: 24 + // TODO move arrow left to toolbar top + source: stackView.depth + > 1 ? "../resources/arrow-left.svg" : "../resources/gear-fill.svg" + anchors.centerIn: settingsButton + } + } + + Column { + id: col + anchors.centerIn: parent + anchors.topMargin: 24 + width: parent.width * 0.8 + + BoldLabel { + id: connectionState + text: "" + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } + + VerticalSpacer { + id: spacerPreImg + visible: false + height: 40 + } + + Image { + id: connectionImage + height: 200 + source: "../resources/spy.gif" + fillMode: Image.PreserveAspectFit + } + + VerticalSpacer { + id: spacerPostImg + visible: false + height: 35 + } + + MaterialButton { + id: toggleVPN + anchors.horizontalCenter: parent.horizontalCenter + Layout.alignment: Qt.AlignBottom + font.capitalization: Font.Capitalize + spacing: 8 + + onClicked: { + if (vpn.state === "on") { + console.debug("should turn off") + backend.switchOff() + } else if (vpn.state === "off") { + console.debug("should turn on") + backend.switchOn() + } else { + console.debug("unknown state") + } + } + + + /* + XXX this hijacks click events, so better no pointing for now. + MouseArea { + anchors.fill: toggleVPN + hoverEnabled: true + cursorShape: !hoverEnabled ? Qt.ForbiddenCursor : Qt.PointingHandCursor + } + */ + } + } +} diff --git a/gui/components/Style.qml b/gui/components/Style.qml new file mode 100644 index 0000000..acd6036 --- /dev/null +++ b/gui/components/Style.qml @@ -0,0 +1,12 @@ +import "themes.js" as Theme + +Item { + property alias fontFoo: fontFooLoader.name + readonly property color colourBlackground: "#efefef" + + // TODO use theme.background + FontLoader { + id: fontFooLoader + source: "qrc:/resources/fonts/Oxanium-Bold.ttf" + } +} diff --git a/gui/components/Systray.qml b/gui/components/Systray.qml new file mode 100644 index 0000000..6fb1046 --- /dev/null +++ b/gui/components/Systray.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.4 +import Qt.labs.platform 1.0 as Labs + +Labs.SystemTrayIcon { + + visible: systrayVisible + property alias statusItem: statusItem + + menu: Labs.Menu { + + id: systrayMenu + + Labs.MenuItem { + id: statusItem + text: qsTr("Checking status…") + enabled: false + } + + Labs.MenuSeparator {} + + Labs.MenuItem { + text: qsTr("Quit") + onTriggered: backend.quit() + } + } +} diff --git a/gui/components/VPNButtonBase.qml b/gui/components/VPNButtonBase.qml new file mode 100644 index 0000000..6f67710 --- /dev/null +++ b/gui/components/VPNButtonBase.qml @@ -0,0 +1,56 @@ + + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import QtQuick 2.0 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.14 +import "../themes/themes.js" as Theme + +RoundButton { + id: root + + property var visualStateItem: root + property var uiState: Theme.uiState + property var loaderVisible: false + property var handleKeyClick: function () { + clicked() + } + + focusPolicy: Qt.StrongFocus + Keys.onPressed: { + if (loaderVisible) { + return + } + + if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) + visualStateItem.state = uiState.statePressed + } + Keys.onReleased: { + if (loaderVisible) { + return + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Space) { + visualStateItem.state = uiState.stateDefault + } + if (event.key === Qt.Key_Return) + handleKeyClick() + } + + Accessible.role: Accessible.Button + Accessible.onPressAction: handleKeyClick() + Accessible.focusable: true + + onActiveFocusChanged: { + if (!activeFocus) + return visualStateItem.state = uiState.stateDefault + + if (vpnFlickable && typeof (vpnFlickable.ensureVisible) !== "undefined") + return vpnFlickable.ensureVisible(visualStateItem) + } + + background: Rectangle { + color: "transparent" + } +} diff --git a/gui/components/VPNMouseArea.qml b/gui/components/VPNMouseArea.qml new file mode 100644 index 0000000..44d6465 --- /dev/null +++ b/gui/components/VPNMouseArea.qml @@ -0,0 +1,37 @@ +import QtQuick 2.5 +import "../themes/themes.js" as Theme + +MouseArea { + id: mouseArea + + property var targetEl: parent + property var uiState: Theme.uiState + property var onMouseAreaClicked: function () { + parent.clicked() + } + + //function changeState(stateName) { + // if (mouseArea.hoverEnabled) + // targetEl.state = stateName; + //} + anchors.fill: parent + hoverEnabled: true + cursorShape: !hoverEnabled ? Qt.ForbiddenCursor : Qt.PointingHandCursor + //onPressed: { + // console.debug("button pressed") + //changeState(uiState.statePressed) + //} + //onEntered: changeState(uiState.stateHovered) + //onExited: changeState(uiState.stateDefault) + //onCanceled: changeState(uiState.stateDefault) + + + /* + onReleased: { + if (hoverEnabled) { + changeState(uiState.stateDefault); + onMouseAreaClicked(); + } + } +*/ +} diff --git a/gui/components/VPNState.qml b/gui/components/VPNState.qml new file mode 100644 index 0000000..8856ad4 --- /dev/null +++ b/gui/components/VPNState.qml @@ -0,0 +1,193 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.12 + +import "../themes/themes.js" as Theme + +StateGroup { + + state: ctx ? ctx.status : "off" + + states: [ + State { + name: "initializing" + }, + State { + name: "off" + PropertyChanges { + target: connectionState + text: qsTr("Connection\nUnsecured") + } + PropertyChanges { + target: statusBoxBackground + border.color: Theme.accentOff + } + PropertyChanges { + target: connectionImage + source: "../resources/spy.gif" + //anchors.right: parent.right + //anchors.rightMargin: -8 + // XXX need to nulify horizontalcenter somehow, + // it gets fixed to parent.center + } + PropertyChanges { + target: toggleVPN + text: qsTr("Turn on") + } + PropertyChanges { + target: systray + icon.source: icons["off"] + } + PropertyChanges { + target: systray.statusItem + text: toHuman("off") + } + StateChangeScript { + script: { + + } + } + }, + State { + name: "on" + PropertyChanges { + target: connectionState + text: qsTr("Connection\nSecure") + } + PropertyChanges { + target: statusBoxBackground + border.color: Theme.accentOn + } + PropertyChanges { + target: connectionImage + source: "../resources/riseup-icon.svg" + // TODO need to offset the logo or increase the image + // to fixed height + height: 120 + } + PropertyChanges { + target: spacerPreImg + visible: true + } + PropertyChanges { + target: spacerPostImg + visible: true + } + PropertyChanges { + target: toggleVPN + text: qsTr("Turn off") + } + PropertyChanges { + target: systray + tooltip: toHuman("on") + icon.source: icons["on"] + } + PropertyChanges { + target: systray.statusItem + text: toHuman("on") + } + StateChangeScript { + script: { + + // TODO check donation + //if (needsDonate && !shownDonate) { + // donate.visible = true; + // shownDonate = true; + // backend.donateSeen(); + //} + } + } + }, + State { + name: "starting" + //when: toggleVPN.pressed == 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 + text: qsTr("Cancel") + } + PropertyChanges { + target: systray + tooltip: toHuman("connecting") + icon.source: icons["wait"] + } + PropertyChanges { + target: systray.statusItem + text: toHuman("connecting") + } + StateChangeScript { + script: { + + } + } + }, + State { + name: "stopping" + PropertyChanges { + target: connectionState + text: "Switching\nOff" + } + PropertyChanges { + target: statusBoxBackground + border.color: Theme.accentConnecting + } + PropertyChanges { + target: systray + tooltip: toHuman("stopping") + icon.source: icons["wait"] + } + PropertyChanges { + target: systray.statusItem + text: toHuman("stopping") + } + }, + State { + name: "failed" + // TODO + } + ] + + + /* + transitions: Transition { + from: "off" + to: "starting" + reversible: true + + ParallelAnimation { + ColorAnimation { duration: 500 } + } + } + */ + function toHuman(st) { + switch (st) { + case "off": + //: %1 -> application name + return ctx ? qsTr("%1 off").arg(ctx.appName) : qsTr("off") + case "on": + //: %1 -> application name + return qsTr("%1 on").arg(ctx.appName) + case "connecting": + //: %1 -> application name + return qsTr("Connecting to %1").arg(ctx.appName) + case "stopping": + //: %1 -> application name + return qsTr("Stopping %1").arg(ctx.appName) + case "failed": + //: %1 -> application name + return qsTr("%1 blocking internet").arg( + ctx.appName) // TODO failed is not handled yet + } + } +} diff --git a/gui/components/VPNToggle.qml b/gui/components/VPNToggle.qml new file mode 100644 index 0000000..4773869 --- /dev/null +++ b/gui/components/VPNToggle.qml @@ -0,0 +1,338 @@ + + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import QtQuick 2.5 +import QtQuick.Controls 2.5 + +import "../themes/themes.js" as Theme + +VPNButtonBase { + id: toggleButton + + property var connectionRetryOverX: VPNController.connectionRetry > 1 + property var toggleColor: Theme.vpnToggleDisconnected + property var toolTipTitle: "" + Accessible.name: toolTipTitle + + function handleClick() { + toolTip.close() + if (VPNController.state !== VPNController.StateOff) { + return VPN.deactivate() + } + + return VPN.activate() + } + + onClicked: handleClick() + + // property in VPNButtonBase {} + visualStateItem: toggle + //state: VPNController.state + height: 32 + width: 60 + radius: 16 + hoverEnabled: false + + onActiveFocusChanged: { + if (!focus && toolTip.visible) { + toolTip.close() + } + } + + + /* + states: [ + State { + name: VPNController.StateInitializing + + PropertyChanges { + target: cursor + anchors.leftMargin: 4 + } + + PropertyChanges { + target: toggle + color: "#9E9E9E" + border.color: Theme.white + } + + PropertyChanges { + target: toggleButton + toggleColor: Theme.vpnToggleDisconnected + } + + }, + State { + name: VPNController.StateOff + + PropertyChanges { + target: cursor + anchors.leftMargin: 4 + } + + PropertyChanges { + target: toggle + color: "#9E9E9E" + border.color: Theme.white + } + + PropertyChanges { + target: toggleButton + //% "Turn VPN on" + toolTipTitle: qsTrId("vpn.toggle.on") + } + + }, + State { + name: VPNController.StateConnecting + + PropertyChanges { + target: cursor + anchors.leftMargin: 32 + color: "#998DB2" + } + + PropertyChanges { + target: toggle + color: "#387E8A" + border.color: Theme.ink + } + + PropertyChanges { + target: toggleButton + //% "Turn VPN off" + toolTipTitle: qsTrId("vpn.toggle.off") + toggleColor: Theme.vpnToggleConnected + } + + }, + State { + name: VPNController.StateConfirming + + PropertyChanges { + target: cursor + anchors.leftMargin: 32 + color: connectionRetryOverX ? "#FFFFFF" : "#998DB2" + } + + PropertyChanges { + target: toggle + color: "#387E8A" + border.color: Theme.ink + } + + PropertyChanges { + target: toggleButton + //% "Turn VPN off" + toolTipTitle: qsTrId("vpn.toggle.off") + toggleColor: Theme.vpnToggleConnected + } + + }, + State { + name: VPNController.StateOn + + PropertyChanges { + target: cursor + anchors.leftMargin: 32 + } + + PropertyChanges { + target: toggle + color: "#3FE1B0" + border.color: Theme.ink + } + + PropertyChanges { + target: toggleButton + toolTipTitle: qsTrId("vpn.toggle.off") + toggleColor: Theme.vpnToggleConnected + } + + }, + State { + name: VPNController.StateDisconnecting + + PropertyChanges { + target: cursor + anchors.leftMargin: 4 + } + + PropertyChanges { + target: toggle + color: "#CECECE" + border.color: Theme.white + } + + PropertyChanges { + target: toggleButton + toolTipTitle: qsTrId("vpn.toggle.on") + toggleColor: Theme.vpnToggleDisconnected + } + + }, + State { + name: VPNController.StateSwitching + + PropertyChanges { + target: cursor + anchors.leftMargin: 32 + color: "#998DB2" + } + + PropertyChanges { + target: toggle + color: "#387E8A" + border.color: Theme.ink + } + + PropertyChanges { + target: toggleButton + toggleColor: Theme.vpnToggleConnected + } + + } + ] + transitions: [ + Transition { + ParallelAnimation { + NumberAnimation { + target: cursor + property: "anchors.leftMargin" + duration: 200 + } + + ColorAnimation { + target: cursor + duration: 200 + } + + } + + } + ] + + // Focus rings + VPNFocusBorder { + id: focusHandler + + anchors.fill: toggle + anchors.margins: -4 + radius: height / 2 + border.color: toggleColor.focusBorder + color: "transparent" + opacity: toggleButton.activeFocus && (VPNController.state === VPNController.StateOn || VPNController.state === VPNController.StateOff) ? 1 : 0 + + VPNFocusOutline { + id: vpnFocusOutline + + anchors.fill: focusHandler + focusedComponent: focusHandler + setMargins: -6 + radius: height / 2 + border.width: 7 + color: "transparent" + border.color: toggleColor.focusOutline + opacity: 0.25 + } + + } + + // Faint outline visible on hover and press + Rectangle { + id: hoverPressHandler + + color: "#C2C2C2" + state: toggle.state + opacity: { + if (state === uiState.stateDefault || toggleButton.activeFocus) + return 0; + + if (state === uiState.stateHovered) + return 0.2; + + if (state === uiState.statePressed) + return 0.3; + + } + z: -1 + anchors.fill: toggle + radius: height / 2 + anchors.margins: -5 + + PropertyAnimation on opacity { + duration: 200 + } + + } + */ + function toggleClickable() { + return VPN.state === VPN.StateMain + && (VPNController.state === VPNController.StateOn + || VPNController.state === VPNController.StateOff + || (VPNController.state === VPNController.StateConfirming + && connectionRetryOverX)) + } + + // Toggle background color changes on hover and press + VPNUIStates { + itemToFocus: toggleButton + itemToAnchor: toggle + colorScheme: toggleColor + radius: height / 2 + setMargins: -7 + showFocusRings: false + opacity: toggleClickable() ? 1 : 0 + z: 1 + + Behavior on opacity { + PropertyAnimation { + property: "opacity" + duration: 100 + } + } + } + + Rectangle { + id: cursor + + height: 24 + width: 24 + radius: 12 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 4 + z: 2 + } + + VPNMouseArea { + id: mouseArea + + targetEl: toggle + anchors.fill: toggle + hoverEnabled: toggleClickable() + cursorShape: Qt.PointingHandCursor + } + + VPNToolTip { + id: toolTip + text: toolTipTitle + } + + background: Rectangle { + id: toggle + + Component.onCompleted: state = uiState.stateDefault + border.width: 0 + anchors.fill: toggleButton + radius: height / 2 + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/gui/components/VerticalSpacer.qml b/gui/components/VerticalSpacer.qml new file mode 100644 index 0000000..455e4da --- /dev/null +++ b/gui/components/VerticalSpacer.qml @@ -0,0 +1,6 @@ +import QtQuick 2.0 + +Rectangle { + color: "transparent" + width: parent.width +} diff --git a/gui/gui.qrc b/gui/gui.qrc index 3099727..dde4c8f 100644 --- a/gui/gui.qrc +++ b/gui/gui.qrc @@ -1,7 +1,50 @@ - qml/main.qml + main.qml + + themes/themes.js + components/MainView.qml + components/Splash.qml + components/Home.qml + components/Header.qml + components/Footer.qml + components/StatusBox.qml + components/Spinner.qml + components/Systray.qml + components/Help.qml + components/Locations.qml + components/Preferences.qml + components/BoldLabel.qml + components/LightLabel.qml + components/VPNButtonBase.qml + components/VPNMouseArea.qml + components/VerticalSpacer.qml + components/Icon.qml + components/MaterialButton.qml + components/VPNState.qml + resources/icon-noshield.svg + resources/location.svg + resources/settings.svg + resources/power.svg + resources/close.svg + resources/donate.svg + resources/tools.svg + resources/help.svg + resources/about.svg + resources/bridge.png + resources/gear-fill.svg + resources/reception-0.svg + resources/reception-2.svg + resources/reception-4.svg + resources/arrow-left.svg + resources/globe.svg + resources/birds.svg + resources/riseup-icon.svg + resources/spy.gif + resources/quit.svg + + qml/VpnState.qml qml/AboutDialog.qml qml/DonateDialog.qml @@ -14,6 +57,7 @@ qml/VPNSwitch.qml qml/BridgesItem.qml qml/logic.js + assets/icon/png/black/vpn_off.png assets/icon/png/black/vpn_on.png diff --git a/gui/main.cpp b/gui/main.cpp index d10cb51..d136c3c 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -137,9 +138,8 @@ int main(int argc, char **argv) { exit(0); } - if (hideSystray) { + if (hideSystray) qDebug() << "Not showing systray icon because --no-systray option is set."; - } if (installHelpers) { qDebug() << "Will try to install helpers with sudo"; @@ -159,11 +159,13 @@ int main(int argc, char **argv) { availableSystray = false; } + /* set window icon */ + app.setWindowIcon(QIcon(":/vendor/icon.svg")); + + /* load translations */ QTranslator translator; translator.load(QLocale(), QLatin1String("main"), QLatin1String("_"), QLatin1String(":/i18n")); app.installTranslator(&translator); - /* set window icon */ - app.setWindowIcon(QIcon(":/vendor/icon.svg")); QQmlApplicationEngine engine; @@ -172,6 +174,7 @@ int main(int argc, char **argv) { QJsonModel *model = new QJsonModel; QString desktop = QString::fromStdString(getEnv("XDG_CURRENT_DESKTOP")); + QString debug = QString::fromStdString(getEnv("DEBUG")); /* the backend handler has slots for calling back to Go when triggered by signals in Qml. */ @@ -185,8 +188,10 @@ int main(int argc, char **argv) { /* set some useful flags */ ctx->setContextProperty("systrayVisible", !hideSystray); ctx->setContextProperty("systrayAvailable", availableSystray); + ctx->setContextProperty("qmlDebug", debug == "1"); - engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + QQuickStyle::setStyle("Material"); + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); /* connect the jsonChanged signal explicitely. In the lambda, we reload the json in the model every time we receive an @@ -215,9 +220,11 @@ int main(int argc, char **argv) { obfs4, disAutostart, toGoStr(startVPN)); /* if requested, enable web api for controlling the VPN */ - if (webAPI) { + if (webAPI) EnableWebAPI(toGoStr(webPort)); - }; + + if (engine.rootObjects().isEmpty()) + return -1; /* kick off your shoes, put your feet up */ return app.exec(); diff --git a/gui/main.qml b/gui/main.qml new file mode 100644 index 0000000..16677a2 --- /dev/null +++ b/gui/main.qml @@ -0,0 +1,116 @@ +/* + TODO (ui rewrite) + - [x] add systray + - [x] systray status + - [x] splash screen + - [ ] splash delay/transitions + - [ ] nested states + - [ ] splash init errors + - [ ] font: monserrat + - [ ] donation dialog + - [ ] add gateway to systray + - [ ] control actions from systray + - [ ] minimize/hide from systray + - [ ] parse ctx flags (need dialog, etc) + - [ ] gateway selector + - [ ] bridges +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.14 + +import "./components" + +ApplicationWindow { + + id: root + + visible: true + width: 360 + height: 520 + minimumWidth: 300 + maximumWidth: 300 + minimumHeight: 500 + maximumHeight: 500 + + title: ctx ? ctx.appName : "VPN" + Material.accent: Material.Green + + property var ctx + + property var icons: { + "off": "qrc:/assets/icon/png/white/vpn_off.png", + "on": "qrc:/assets/icon/png/white/vpn_on.png", + "wait": "qrc:/assets/icon/png/white/vpn_wait_0.png", + "blocked": "qrc:/assets/icon/png/white/vpn_blocked.png" + } + + Loader { + id: loader + asynchronous: true + anchors.fill: parent + } + + Systray { + id: systray + } + + Connections { + target: jsonModel + function onDataChanged() { + ctx = JSON.parse(jsonModel.getJson()) + if (qmlDebug) { + console.debug(jsonModel.getJson()) + } + + // FIXME -- use nested state machines for all these cases. + + //gwSelector.model = Object.keys(ctx.locations) + + /* + if (ctx.donateDialog == 'true') { + Logic.setNeedsDonate(true); + } + if (ctx.loginDialog == 'true') { + console.debug(jsonModel.getJson()) + console.debug("DEBUG: should display login") + login.visible = true + } + if (ctx.loginOk == 'true') { + loginOk.visible = true + } + if (ctx.errors) { + login.visible = false + if (ctx.errors == "nohelpers") { + showInitFailure( + qsTr("Could not find helpers. Please check your installation")) + } else if (ctx.errors == "nopolkit") { + showInitFailure(qsTr("Could not find polkit agent.")) + } else { + showInitFailure() + } + } + if (ctx.donateURL) { + donateItem.visible = true + } + + if (ctx.status == "on") { + gwNextConnectionText.visible = false + gwReconnectText.visible = false + } + */ + } + } + + onSceneGraphError: function(error, msg) { + console.debug("ERROR while initializing scene") + console.debug(msg) + } + + Component.onCompleted: { + loader.source = "components/Splash.qml" + } +} diff --git a/gui/providers/providers.json b/gui/providers/providers.json index 8592ba0..52fcbf9 100644 --- a/gui/providers/providers.json +++ b/gui/providers/providers.json @@ -15,7 +15,7 @@ "apiURL": "https://api.black.riseup.net/", "geolocationAPI": "https://api.black.riseup.net:9001/json", "caCertString": "-----BEGIN CERTIFICATE-----\nMIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl\ndXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE\nAwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw\nNDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM\nEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv\nb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m\nTP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a\n7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE\nLliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY\niw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK\n5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx\nHUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58\nm/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF\nPM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q\nhzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj\nshczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k\nha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu\nf9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD\nVR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB\nAGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v\nqHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/\n3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ\n4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7\n3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch\nTd5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf\nXu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg\ntsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF\ntGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ\nUN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp\n0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO\n-----END CERTIFICATE-----", - "timeStamp": "2021-07-09 20:10:58" + "timeStamp": "2021-08-25 00:56:22" } ] } \ No newline at end of file diff --git a/gui/qml/BackgroundImage.qml b/gui/qml/BackgroundImage.qml index 3071bf4..ba2a8af 100644 --- a/gui/qml/BackgroundImage.qml +++ b/gui/qml/BackgroundImage.qml @@ -3,22 +3,22 @@ import QtQuick.Controls 2.4 Rectangle { - anchors.fill: parent; - anchors.topMargin: 40; + anchors.fill: parent + anchors.topMargin: 40 property var backgroundSrc property var backgroundVisible Image { - source: parent.backgroundSrc; - visible: parent.backgroundVisible; - fillMode: Image.PreserveAspectCrop; - anchors.fill: parent; - opacity: 0.8; + source: parent.backgroundSrc + visible: parent.backgroundVisible + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + opacity: 0.8 } Component.onCompleted: { /* default for riseup, needs customizing */ - backgroundSrc = "qrc:/assets/img/bird.jpg"; + backgroundSrc = "qrc:/assets/img/bird.jpg" } } diff --git a/gui/qml/BridgesItem.qml b/gui/qml/BridgesItem.qml index 1343403..f7ccb0f 100644 --- a/gui/qml/BridgesItem.qml +++ b/gui/qml/BridgesItem.qml @@ -26,15 +26,15 @@ Item { anchors.horizontalCenter: parent.horizontalCenter onClicked: { if (checked) { - Logic.setNeedsReconnect(true); - bridgeReconnect.visible = true; - app.useBridges(true); + Logic.setNeedsReconnect(true) + bridgeReconnect.visible = true + app.useBridges(true) } else { // This would also need a "needs reconnect" for de-selecting bridges the next time. // better to wait and see the new connection widgets though - Logic.setNeedsReconnect(false); - bridgeReconnect.visible = false; - app.useBridges(false); + Logic.setNeedsReconnect(false) + bridgeReconnect.visible = false + app.useBridges(false) } } } @@ -45,7 +45,7 @@ Item { color: "grey" text: qsTr("Select a bridge only if you know that you need it to evade censorship in your country or local network.") anchors.horizontalCenter: parent.horizontalCenter - wrapMode: Text.WordWrap + wrapMode: Text.WordWrap visible: !bridgeReconnect.visible } @@ -56,7 +56,7 @@ Item { color: "red" text: qsTr("An obfs4 bridge will be used the next time you connect to the VPN.") anchors.horizontalCenter: parent.horizontalCenter - wrapMode: Text.WordWrap + wrapMode: Text.WordWrap visible: false } } diff --git a/gui/qml/VPNSwitch.qml b/gui/qml/VPNSwitch.qml index 89c455f..c58836e 100644 --- a/gui/qml/VPNSwitch.qml +++ b/gui/qml/VPNSwitch.qml @@ -2,7 +2,6 @@ import QtQuick 2.9 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.4 - SwitchDelegate { text: qsTr("") diff --git a/gui/qml/VpnState.qml b/gui/qml/VpnState.qml index a4bb779..31910c7 100644 --- a/gui/qml/VpnState.qml +++ b/gui/qml/VpnState.qml @@ -14,7 +14,7 @@ StateGroup { State { name: "off" StateChangeScript { - script: Logic.setStatus("off"); + script: Logic.setStatus("off") } PropertyChanges { target: systray @@ -47,16 +47,15 @@ StateGroup { name: "on" StateChangeScript { script: { - Logic.setNeedsReconnect(false); - brReconnect = false; + Logic.setNeedsReconnect(false) + brReconnect = false if (needsDonate && !shownDonate) { - donate.visible = true; - shownDonate = true; - backend.donateSeen(); + donate.visible = true + shownDonate = true + backend.donateSeen() } } - } PropertyChanges { target: systray diff --git a/gui/qml/main.qml b/gui/qml/main.qml index fef68cf..2c94c80 100644 --- a/gui/qml/main.qml +++ b/gui/qml/main.qml @@ -1,9 +1,10 @@ import QtQuick 2.9 -import QtQuick.Dialogs 1.2 // TODO use native dialogs in labs.platform +import QtQuick.Dialogs 1.2 +// TODO use native dialogs in labs.platform import QtQuick.Layouts 1.12 import QtQuick.Controls 2.4 -import Qt.labs.platform 1.0 +import Qt.labs.platform 1.0 as Labs import "logic.js" as Logic @@ -24,7 +25,7 @@ ApplicationWindow { property var needsDonate property var shownDonate - onSceneGraphError: function(error, msg) { + onSceneGraphError: function (error, msg) { console.debug("ERROR while initializing scene") console.debug(msg) } @@ -116,8 +117,8 @@ ApplicationWindow { anchors.centerIn: parent spacing: 10 - //width: parent.width + //width: parent.width RadioButton { id: autoSelectionButton checked: !isManualLocation() @@ -161,6 +162,7 @@ ApplicationWindow { color: { "#ffffff" // FIXME locations is not defined when we launch + /* const fullness = ctx.locations[modelData] if (fullness >= 0 && fullness < 0.4) { @@ -181,7 +183,6 @@ ApplicationWindow { color: "#000000" } } - } Text { @@ -190,7 +191,7 @@ ApplicationWindow { width: 180 font.pixelSize: 12 color: "green" - wrapMode: Text.WordWrap + wrapMode: Text.WordWrap text: qsTr("Reconnecting to the selected gateway…") visible: false } @@ -201,31 +202,29 @@ ApplicationWindow { width: 180 font.pixelSize: 12 color: "green" - wrapMode: Text.WordWrap + wrapMode: Text.WordWrap text: qsTr("This gateway will be used for next connection.") visible: false } - } // end column } // end item BridgesItem { id: bridgesTab } - } // end stacklayout Connections { target: jsonModel function onDataChanged() { ctx = JSON.parse(jsonModel.getJson()) + // TODO pass QML_DEBUG variable to be hyper-verbose //console.debug(jsonModel.getJson()) - gwSelector.model = Object.keys(ctx.locations) if (ctx.donateDialog == 'true') { - Logic.setNeedsDonate(true); + Logic.setNeedsDonate(true) } if (ctx.loginDialog == 'true') { console.debug(jsonModel.getJson()) @@ -294,11 +293,11 @@ ApplicationWindow { function hasMultipleGateways() { // could also count the gateways - let provider = Logic.getSelectedProvider(providers); + let provider = Logic.getSelectedProvider(providers) if (provider == "riseup") { - return true; + return true } else { - return false; + return false } } @@ -319,35 +318,34 @@ ApplicationWindow { } function showMainWindow() { - bar.currentIndex = 0 - app.visible = true - app.show() - app.raise() + bar.currentIndex = 0 + app.visible = true + app.show() + app.raise() } Component.onCompleted: { Logic.debugInit() loginDone = false allowEmptyPass = Logic.shouldAllowEmptyPass(providers) - needsRestart = false; - shownDonate = false; + needsRestart = false + shownDonate = false /* this is a temporary workaround until general GUI revamp for 0.21.8 */ - let provider = Logic.getSelectedProvider(providers); + let provider = Logic.getSelectedProvider(providers) if (provider == "calyx") { - background.color = "#8EA844"; - background.backgroundVisible = false; - gwSelector.visible = false; - manualSelectionButton.visible = false; + background.color = "#8EA844" + background.backgroundVisible = false + gwSelector.visible = false + manualSelectionButton.visible = false } if (!systrayAvailable) { - app.visible = true - app.raise() + app.visible = true + app.raise() } } - property var icons: { "off": "qrc:/assets/icon/png/white/vpn_off.png", "on": "qrc:/assets/icon/png/white/vpn_on.png", @@ -364,6 +362,7 @@ ApplicationWindow { id: systray visible: systrayVisible + /* the systray menu cannot be buried in a child qml file because * otherwise the ids are not available * from other components @@ -501,9 +500,8 @@ ApplicationWindow { if (Qt.platform.os === "windows") { let appname = ctx ? ctx.appName : "VPN" Logic.showNotification( - ctx, - appname - + " is up and running. Please use system tray icon to control it.") + ctx, appname + + " is up and running. Please use system tray icon to control it.") } } } @@ -560,13 +558,12 @@ ApplicationWindow { } function useBridges(value) { - if (value==true) { + if (value == true) { backend.setTransport("obfs4") } else { backend.setTransport("openvpn") } } - property alias brReconnect:bridgesTab.displayReconnect - + property alias brReconnect: bridgesTab.displayReconnect } diff --git a/gui/qml/readme.txt b/gui/qml/readme.txt new file mode 100644 index 0000000..e7d468c --- /dev/null +++ b/gui/qml/readme.txt @@ -0,0 +1 @@ +This folder will be deprecated. diff --git a/gui/resources/about.svg b/gui/resources/about.svg new file mode 100644 index 0000000..9a47ce3 --- /dev/null +++ b/gui/resources/about.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/arrow-left.svg b/gui/resources/arrow-left.svg new file mode 100644 index 0000000..9d88501 --- /dev/null +++ b/gui/resources/arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/birds.svg b/gui/resources/birds.svg new file mode 100644 index 0000000..cb2a1df --- /dev/null +++ b/gui/resources/birds.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/gui/resources/bridge.png b/gui/resources/bridge.png new file mode 100644 index 0000000..d0a2c80 Binary files /dev/null and b/gui/resources/bridge.png differ diff --git a/gui/resources/close.svg b/gui/resources/close.svg new file mode 100644 index 0000000..ead8e16 --- /dev/null +++ b/gui/resources/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/gui/resources/donate.svg b/gui/resources/donate.svg new file mode 100644 index 0000000..35d7c2b --- /dev/null +++ b/gui/resources/donate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gui/resources/gear-fill.svg b/gui/resources/gear-fill.svg new file mode 100644 index 0000000..2aa36a1 --- /dev/null +++ b/gui/resources/gear-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/globe.svg b/gui/resources/globe.svg new file mode 100644 index 0000000..150a01e --- /dev/null +++ b/gui/resources/globe.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/help.svg b/gui/resources/help.svg new file mode 100644 index 0000000..06569c8 --- /dev/null +++ b/gui/resources/help.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gui/resources/icon-noshield.svg b/gui/resources/icon-noshield.svg new file mode 100644 index 0000000..815a0d6 --- /dev/null +++ b/gui/resources/icon-noshield.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/gui/resources/location.svg b/gui/resources/location.svg new file mode 100644 index 0000000..f738eb6 --- /dev/null +++ b/gui/resources/location.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/power.svg b/gui/resources/power.svg new file mode 100644 index 0000000..158bc3c --- /dev/null +++ b/gui/resources/power.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gui/resources/quit.svg b/gui/resources/quit.svg new file mode 100644 index 0000000..2fbd520 --- /dev/null +++ b/gui/resources/quit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gui/resources/reception-0.svg b/gui/resources/reception-0.svg new file mode 100644 index 0000000..885bf3b --- /dev/null +++ b/gui/resources/reception-0.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/reception-2.svg b/gui/resources/reception-2.svg new file mode 100644 index 0000000..7dca57a --- /dev/null +++ b/gui/resources/reception-2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/reception-4.svg b/gui/resources/reception-4.svg new file mode 100644 index 0000000..611bdf1 --- /dev/null +++ b/gui/resources/reception-4.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/gui/resources/riseup-icon.svg b/gui/resources/riseup-icon.svg new file mode 100644 index 0000000..a19c6c6 --- /dev/null +++ b/gui/resources/riseup-icon.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/gui/resources/riseup-logo.png b/gui/resources/riseup-logo.png new file mode 100644 index 0000000..e68cfe2 Binary files /dev/null and b/gui/resources/riseup-logo.png differ diff --git a/gui/resources/settings.svg b/gui/resources/settings.svg new file mode 100644 index 0000000..0469ed4 --- /dev/null +++ b/gui/resources/settings.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/speed-green.svg b/gui/resources/speed-green.svg new file mode 100644 index 0000000..a25de12 --- /dev/null +++ b/gui/resources/speed-green.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/speed-red.svg b/gui/resources/speed-red.svg new file mode 100644 index 0000000..8ceb858 --- /dev/null +++ b/gui/resources/speed-red.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/speed-yellow.svg b/gui/resources/speed-yellow.svg new file mode 100644 index 0000000..377a2d2 --- /dev/null +++ b/gui/resources/speed-yellow.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/resources/spy.gif b/gui/resources/spy.gif new file mode 100644 index 0000000..733d3eb Binary files /dev/null and b/gui/resources/spy.gif differ diff --git a/gui/resources/tools.svg b/gui/resources/tools.svg new file mode 100644 index 0000000..41c63a1 --- /dev/null +++ b/gui/resources/tools.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gui/themes/themes.js b/gui/themes/themes.js new file mode 100644 index 0000000..265af9a --- /dev/null +++ b/gui/themes/themes.js @@ -0,0 +1,51 @@ +.pragma library + +const borderColor = "#6D6D6E"; +const fontColor = "#6D6D6E"; +const fontColorDark = "#3D3D3D"; +const fontFamily = "Metropolis"; +const fontBoldFamily = "OxaniumBold"; +const fontInterFamily = "InterUI"; +const fontSize = 15; +const fontSizeLarge = 22; +const fontSizeSmall = 13; +const fontSizeSmallest = 11; +const fontWeightBold = 600; +const iconSize = 16; +const labelLineHeight = 22; +const cityListTopMargin = 18; +const controllerInterLineHeight = 18; +const hSpacing = 20; +const vSpacing = 24; +const vSpacingSmall = 16; +const listSpacing = 8; +const maxTextWidth = 296; +const windowMargin = 16; +const popupMargin = 24; +const desktopAppHeight = 520; +const desktopAppWidth = 360; +const darkFocusBorder = fontColor; +const lightFocusBorder = "#d5d3e0"; + +const accentOff = "#af0909"; +const accentConnecting = "#FFCC33"; +const accentOn = "#669933"; + +const blue = "#0060DF"; +const blueHovered = "#0250BB"; +const bluePressed = "#054096"; +const blueDisabled = "#a3c0f3"; +const blueFocusOutline = "#4d0a84ff"; +const blueFocusBorder = "#0a84ff"; + +const blueButton = { + "defaultColor" : blue, + "buttonHovered": blueHovered, + "buttonPressed": bluePressed, + "buttonDisabled": blueDisabled, + "focusBgColor": blue, + "focusOutline": blueFocusOutline, + "focusBorder": blueFocusBorder, +}; + +const bgColor = "white"; diff --git a/pkg/backend/init.go b/pkg/backend/init.go index b7469c1..fcde725 100644 --- a/pkg/backend/init.go +++ b/pkg/backend/init.go @@ -29,10 +29,11 @@ func initializeContext(opts *InitOpts) { DonateDialog: false, Version: version.VERSION, Status: st, + IsReady: false, } errCh := make(chan string) - go trigger(OnStatusChanged) go checkErrors(errCh) + // isReady is set after Bitmask initialization initializeBitmask(errCh, opts) go trigger(OnStatusChanged) ctx.delayCheckForGateways() @@ -86,6 +87,7 @@ func initializeBitmask(errCh chan string, opts *InitOpts) { errCh <- "nopolkit" } ctx.bm = b + ctx.IsReady = true } func setConfigOpts(opts *InitOpts, conf *config.Config) { diff --git a/pkg/backend/status.go b/pkg/backend/status.go index 1ec5c4f..0ffd853 100644 --- a/pkg/backend/status.go +++ b/pkg/backend/status.go @@ -49,6 +49,7 @@ type connectionCtx struct { CurrentLocation string `json:"currentLocation"` CurrentCountry string `json:"currentCountry"` ManualLocation bool `json:"manualLocation"` + IsReady bool `json:"isReady"` bm bitmask.Bitmask autostart bitmask.Autostart cfg *config.Config -- cgit v1.2.3