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 --- 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 + 21 files changed, 1406 insertions(+) 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 (limited to 'gui/components') 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 +} -- cgit v1.2.3