summaryrefslogtreecommitdiff
path: root/gui/components
diff options
context:
space:
mode:
Diffstat (limited to 'gui/components')
-rw-r--r--gui/components/BoldLabel.qml21
-rw-r--r--gui/components/Footer.qml63
-rw-r--r--gui/components/Header.qml32
-rw-r--r--gui/components/Help.qml32
-rw-r--r--gui/components/Home.qml8
-rw-r--r--gui/components/Icon.qml9
-rw-r--r--gui/components/LightLabel.qml19
-rw-r--r--gui/components/Locations.qml63
-rw-r--r--gui/components/MainView.qml131
-rw-r--r--gui/components/MaterialButton.qml84
-rw-r--r--gui/components/Preferences.qml23
-rw-r--r--gui/components/Spinner.qml63
-rw-r--r--gui/components/Splash.qml64
-rw-r--r--gui/components/StatusBox.qml125
-rw-r--r--gui/components/Style.qml12
-rw-r--r--gui/components/Systray.qml27
-rw-r--r--gui/components/VPNButtonBase.qml56
-rw-r--r--gui/components/VPNMouseArea.qml37
-rw-r--r--gui/components/VPNState.qml193
-rw-r--r--gui/components/VPNToggle.qml338
-rw-r--r--gui/components/VerticalSpacer.qml6
21 files changed, 1406 insertions, 0 deletions
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
+}