controlcenter: added VPN settings & management (#1095)
* feat: add VPN settings and management UI - Add VPN configuration UI - Update VPN toggle visibility to check enabled providers * controlcenter: VPN modal transitions & cleanup * controlcenter: VPN modal styling * controlcenter: VPN modal scrim * controlcenter: VPN modal padding * controlcenter: VPN modal enter & exit behaviour
This commit is contained in:
parent
d50f6080ec
commit
2ddc367e4e
10 changed files with 1410 additions and 21 deletions
|
|
@ -629,12 +629,13 @@ default, you must create it manually.
|
||||||
"nowPlaying": false
|
"nowPlaying": false
|
||||||
},
|
},
|
||||||
"vpn": {
|
"vpn": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"provider": [
|
"provider": [
|
||||||
{
|
{
|
||||||
"name": "wireguard",
|
"name": "wireguard",
|
||||||
"interface": "your-connection-name",
|
"interface": "your-connection-name",
|
||||||
"displayName": "Wireguard (Your VPN)"
|
"displayName": "Wireguard (Your VPN)",
|
||||||
|
"enabled": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ QtObject {
|
||||||
readonly property NetworkState network: NetworkState {}
|
readonly property NetworkState network: NetworkState {}
|
||||||
readonly property EthernetState ethernet: EthernetState {}
|
readonly property EthernetState ethernet: EthernetState {}
|
||||||
readonly property LauncherState launcher: LauncherState {}
|
readonly property LauncherState launcher: LauncherState {}
|
||||||
|
readonly property VpnState vpn: VpnState {}
|
||||||
|
|
||||||
onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))
|
onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))
|
||||||
onActiveIndexChanged: if (panes[activeIndex]) active = panes[activeIndex]
|
onActiveIndexChanged: if (panes[activeIndex]) active = panes[activeIndex]
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ import ".."
|
||||||
import "../components"
|
import "../components"
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
import qs.components.controls
|
||||||
|
import qs.components.containers
|
||||||
import qs.components.effects
|
import qs.components.effects
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.config
|
import qs.config
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -59,6 +61,45 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
Layout.topMargin: Appearance.spacing.large
|
||||||
|
title: qsTr("VPN")
|
||||||
|
description: qsTr("VPN provider settings")
|
||||||
|
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
label: qsTr("VPN enabled")
|
||||||
|
checked: Config.utilities.vpn.enabled
|
||||||
|
toggle.onToggled: {
|
||||||
|
Config.utilities.vpn.enabled = checked;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
showTopMargin: true
|
||||||
|
label: qsTr("Providers")
|
||||||
|
value: qsTr("%1").arg(Config.utilities.vpn.provider.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Appearance.spacing.normal
|
||||||
|
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||||
|
text: qsTr("⚙ Manage VPN Providers")
|
||||||
|
inactiveColour: Colours.palette.m3secondaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
vpnSettingsDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
Layout.topMargin: Appearance.spacing.large
|
Layout.topMargin: Appearance.spacing.large
|
||||||
title: qsTr("Current connection")
|
title: qsTr("Current connection")
|
||||||
|
|
@ -94,5 +135,38 @@ ColumnLayout {
|
||||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: vpnSettingsDialog
|
||||||
|
|
||||||
|
parent: Overlay.overlay
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(600, parent.width - Appearance.padding.large * 2)
|
||||||
|
height: Math.min(700, parent.height - Appearance.padding.large * 2)
|
||||||
|
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
color: Colours.palette.m3surface
|
||||||
|
radius: Appearance.rounding.large
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.large * 1.5
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
contentHeight: vpnSettingsContent.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
VpnSettings {
|
||||||
|
id: vpnSettingsContent
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
session: root.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,24 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: vpnListSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("VPN")
|
||||||
|
expanded: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
sourceComponent: Component {
|
||||||
|
VpnList {
|
||||||
|
session: root.session
|
||||||
|
showHeader: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CollapsibleSection {
|
CollapsibleSection {
|
||||||
id: ethernetListSection
|
id: ethernetListSection
|
||||||
|
|
||||||
|
|
@ -154,14 +172,16 @@ Item {
|
||||||
Item {
|
Item {
|
||||||
id: rightPaneItem
|
id: rightPaneItem
|
||||||
|
|
||||||
property var ethernetPane: root.session.ethernet.active
|
property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null
|
||||||
property var wirelessPane: root.session.network.active
|
property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null
|
||||||
property var pane: ethernetPane || wirelessPane
|
property var wirelessPane: root.session && root.session.network ? root.session.network.active : null
|
||||||
property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")
|
property var pane: vpnPane || ethernetPane || wirelessPane
|
||||||
|
property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings"))
|
||||||
property Component targetComponent: settingsComponent
|
property Component targetComponent: settingsComponent
|
||||||
property Component nextComponent: settingsComponent
|
property Component nextComponent: settingsComponent
|
||||||
|
|
||||||
function getComponentForPane() {
|
function getComponentForPane() {
|
||||||
|
if (vpnPane) return vpnDetailsComponent;
|
||||||
if (ethernetPane) return ethernetDetailsComponent;
|
if (ethernetPane) return ethernetDetailsComponent;
|
||||||
if (wirelessPane) return wirelessDetailsComponent;
|
if (wirelessPane) return wirelessDetailsComponent;
|
||||||
return settingsComponent;
|
return settingsComponent;
|
||||||
|
|
@ -173,28 +193,44 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.session.ethernet
|
target: root.session && root.session.vpn ? root.session.vpn : null
|
||||||
|
enabled: target !== null
|
||||||
|
|
||||||
function onActiveChanged() {
|
function onActiveChanged() {
|
||||||
// Clear wireless when ethernet is selected
|
// Clear others when VPN is selected
|
||||||
if (root.session.ethernet.active && root.session.network.active) {
|
if (root.session && root.session.vpn && root.session.vpn.active) {
|
||||||
root.session.network.active = null;
|
if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null;
|
||||||
return; // Let the network.onActiveChanged handle the update
|
if (root.session.network && root.session.network.active) root.session.network.active = null;
|
||||||
}
|
}
|
||||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||||
// paneId will automatically update via property binding
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.session.network
|
target: root.session && root.session.ethernet ? root.session.ethernet : null
|
||||||
|
enabled: target !== null
|
||||||
|
|
||||||
function onActiveChanged() {
|
function onActiveChanged() {
|
||||||
// Clear ethernet when wireless is selected
|
// Clear others when ethernet is selected
|
||||||
if (root.session.network.active && root.session.ethernet.active) {
|
if (root.session && root.session.ethernet && root.session.ethernet.active) {
|
||||||
root.session.ethernet.active = null;
|
if (root.session.vpn && root.session.vpn.active) root.session.vpn.active = null;
|
||||||
return; // Let the ethernet.onActiveChanged handle the update
|
if (root.session.network && root.session.network.active) root.session.network.active = null;
|
||||||
|
}
|
||||||
|
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.session && root.session.network ? root.session.network : null
|
||||||
|
enabled: target !== null
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
// Clear others when wireless is selected
|
||||||
|
if (root.session && root.session.network && root.session.network.active) {
|
||||||
|
if (root.session.vpn && root.session.vpn.active) root.session.vpn.active = null;
|
||||||
|
if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null;
|
||||||
}
|
}
|
||||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||||
// paneId will automatically update via property binding
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,6 +244,7 @@ Item {
|
||||||
transformOrigin: Item.Center
|
transformOrigin: Item.Center
|
||||||
clip: false
|
clip: false
|
||||||
|
|
||||||
|
asynchronous: true
|
||||||
sourceComponent: rightPaneItem.targetComponent
|
sourceComponent: rightPaneItem.targetComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,6 +333,29 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: vpnDetailsComponent
|
||||||
|
|
||||||
|
StyledFlickable {
|
||||||
|
id: vpnFlickable
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
contentHeight: vpnDetailsInner.height
|
||||||
|
|
||||||
|
StyledScrollBar.vertical: StyledScrollBar {
|
||||||
|
flickable: vpnFlickable
|
||||||
|
}
|
||||||
|
|
||||||
|
VpnDetails {
|
||||||
|
id: vpnDetailsInner
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
session: root.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WirelessPasswordDialog {
|
WirelessPasswordDialog {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
session: root.session
|
session: root.session
|
||||||
|
|
|
||||||
367
modules/controlcenter/network/VpnDetails.qml
Normal file
367
modules/controlcenter/network/VpnDetails.qml
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import ".."
|
||||||
|
import "../components"
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.components.effects
|
||||||
|
import qs.components.containers
|
||||||
|
import qs.services
|
||||||
|
import qs.config
|
||||||
|
import qs.utils
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
|
DeviceDetails {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Session session
|
||||||
|
readonly property var vpnProvider: root.session.vpn.active
|
||||||
|
readonly property bool providerEnabled: {
|
||||||
|
if (!vpnProvider || vpnProvider.index === undefined) return false;
|
||||||
|
const provider = Config.utilities.vpn.provider[vpnProvider.index];
|
||||||
|
return provider && typeof provider === "object" && provider.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
device: vpnProvider
|
||||||
|
|
||||||
|
headerComponent: Component {
|
||||||
|
ConnectionHeader {
|
||||||
|
icon: "vpn_key"
|
||||||
|
title: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections: [
|
||||||
|
Component {
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
title: qsTr("Connection status")
|
||||||
|
description: qsTr("VPN connection settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
ToggleRow {
|
||||||
|
label: qsTr("Enable this provider")
|
||||||
|
checked: root.providerEnabled
|
||||||
|
toggle.onToggled: {
|
||||||
|
if (!root.vpnProvider) return;
|
||||||
|
const providers = [];
|
||||||
|
const index = root.vpnProvider.index;
|
||||||
|
|
||||||
|
// Copy providers and update enabled state
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
const p = Config.utilities.vpn.provider[i];
|
||||||
|
if (typeof p === "object") {
|
||||||
|
const newProvider = {
|
||||||
|
name: p.name,
|
||||||
|
displayName: p.displayName,
|
||||||
|
interface: p.interface
|
||||||
|
};
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// Enable this one, disable others
|
||||||
|
newProvider.enabled = (i === index);
|
||||||
|
} else {
|
||||||
|
// Just disable this one
|
||||||
|
newProvider.enabled = (i === index) ? false : (p.enabled !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
providers.push(newProvider);
|
||||||
|
} else {
|
||||||
|
providers.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Appearance.spacing.normal
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||||
|
visible: root.providerEnabled
|
||||||
|
enabled: !VPN.connecting
|
||||||
|
inactiveColour: Colours.palette.m3primaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||||
|
text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
VPN.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Edit Provider")
|
||||||
|
inactiveColour: Colours.palette.m3secondaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
editVpnDialog.editIndex = root.vpnProvider.index;
|
||||||
|
editVpnDialog.providerName = root.vpnProvider.name;
|
||||||
|
editVpnDialog.displayName = root.vpnProvider.displayName;
|
||||||
|
editVpnDialog.interfaceName = root.vpnProvider.interface;
|
||||||
|
editVpnDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Delete Provider")
|
||||||
|
inactiveColour: Colours.palette.m3errorContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onErrorContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
if (i !== root.vpnProvider.index) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
root.session.vpn.active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Component {
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
title: qsTr("Provider details")
|
||||||
|
description: qsTr("VPN provider information")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
contentSpacing: Appearance.spacing.small / 2
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
label: qsTr("Provider")
|
||||||
|
value: root.vpnProvider?.name ?? qsTr("Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
showTopMargin: true
|
||||||
|
label: qsTr("Display name")
|
||||||
|
value: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
showTopMargin: true
|
||||||
|
label: qsTr("Interface")
|
||||||
|
value: root.vpnProvider?.interface || qsTr("N/A")
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
showTopMargin: true
|
||||||
|
label: qsTr("Status")
|
||||||
|
value: {
|
||||||
|
if (!root.providerEnabled) return qsTr("Disabled");
|
||||||
|
if (VPN.connecting) return qsTr("Connecting...");
|
||||||
|
if (VPN.connected) return qsTr("Connected");
|
||||||
|
return qsTr("Enabled (Not connected)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyRow {
|
||||||
|
showTopMargin: true
|
||||||
|
label: qsTr("Enabled")
|
||||||
|
value: root.providerEnabled ? qsTr("Yes") : qsTr("No")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Edit VPN Dialog
|
||||||
|
Popup {
|
||||||
|
id: editVpnDialog
|
||||||
|
|
||||||
|
property int editIndex: -1
|
||||||
|
property string providerName: ""
|
||||||
|
property string displayName: ""
|
||||||
|
property string interfaceName: ""
|
||||||
|
|
||||||
|
parent: Overlay.overlay
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||||
|
padding: Appearance.padding.large * 1.5
|
||||||
|
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.7
|
||||||
|
|
||||||
|
enter: Transition {
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Appearance.anim.durations.expressiveFastSpatial; easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial }
|
||||||
|
NumberAnimation { property: "scale"; from: 0.7; to: 1; duration: Appearance.anim.durations.expressiveFastSpatial; easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit: Transition {
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Appearance.anim.durations.expressiveFastSpatial; easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial }
|
||||||
|
NumberAnimation { property: "scale"; from: 1; to: 0.7; duration: Appearance.anim.durations.expressiveFastSpatial; easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWithAnimation(): void {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Overlay.modal: Rectangle {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
color: Colours.palette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: DropShadow {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
radius: 16
|
||||||
|
samples: 33
|
||||||
|
verticalOffset: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Edit VPN Provider")
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
font.weight: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.smaller / 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Display Name")
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 40
|
||||||
|
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
border.width: 1
|
||||||
|
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||||
|
|
||||||
|
Behavior on color { CAnim {} }
|
||||||
|
Behavior on border.color { CAnim {} }
|
||||||
|
|
||||||
|
StyledTextField {
|
||||||
|
id: displayNameField
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Appearance.padding.normal
|
||||||
|
horizontalAlignment: TextInput.AlignLeft
|
||||||
|
text: editVpnDialog.displayName
|
||||||
|
onTextChanged: editVpnDialog.displayName = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.smaller / 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 40
|
||||||
|
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
border.width: 1
|
||||||
|
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||||
|
|
||||||
|
Behavior on color { CAnim {} }
|
||||||
|
Behavior on border.color { CAnim {} }
|
||||||
|
|
||||||
|
StyledTextField {
|
||||||
|
id: interfaceNameField
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Appearance.padding.normal
|
||||||
|
horizontalAlignment: TextInput.AlignLeft
|
||||||
|
text: editVpnDialog.interfaceName
|
||||||
|
onTextChanged: editVpnDialog.interfaceName = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.preferredHeight: Appearance.spacing.normal }
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: editVpnDialog.closeWithAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Save")
|
||||||
|
enabled: editVpnDialog.interfaceName.length > 0
|
||||||
|
inactiveColour: Colours.palette.m3primaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex];
|
||||||
|
const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true;
|
||||||
|
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
if (i === editVpnDialog.editIndex) {
|
||||||
|
providers.push({
|
||||||
|
name: editVpnDialog.providerName,
|
||||||
|
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
|
||||||
|
interface: editVpnDialog.interfaceName,
|
||||||
|
enabled: wasEnabled
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
editVpnDialog.closeWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
646
modules/controlcenter/network/VpnList.qml
Normal file
646
modules/controlcenter/network/VpnList.qml
Normal file
|
|
@ -0,0 +1,646 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import ".."
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.components.containers
|
||||||
|
import qs.components.effects
|
||||||
|
import qs.services
|
||||||
|
import qs.config
|
||||||
|
import qs.utils
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Session session
|
||||||
|
property bool showHeader: true
|
||||||
|
property int pendingSwitchIndex: -1
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: VPN
|
||||||
|
function onConnectedChanged() {
|
||||||
|
if (!VPN.connected && root.pendingSwitchIndex >= 0) {
|
||||||
|
const targetIndex = root.pendingSwitchIndex;
|
||||||
|
root.pendingSwitchIndex = -1;
|
||||||
|
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
const p = Config.utilities.vpn.provider[i];
|
||||||
|
if (typeof p === "object") {
|
||||||
|
const newProvider = {
|
||||||
|
name: p.name,
|
||||||
|
displayName: p.displayName,
|
||||||
|
interface: p.interface,
|
||||||
|
enabled: (i === targetIndex)
|
||||||
|
};
|
||||||
|
providers.push(newProvider);
|
||||||
|
} else {
|
||||||
|
providers.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
VPN.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("+ Add VPN Provider")
|
||||||
|
inactiveColour: Colours.palette.m3primaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
vpnDialog.showProviderSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: contentHeight
|
||||||
|
|
||||||
|
interactive: false
|
||||||
|
spacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||||
|
const isObject = typeof provider === "object";
|
||||||
|
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||||
|
const displayName = isObject ? (provider.displayName || name) : name;
|
||||||
|
const iface = isObject ? (provider.interface || "") : "";
|
||||||
|
const enabled = isObject ? (provider.enabled === true) : false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: index,
|
||||||
|
name: name,
|
||||||
|
displayName: displayName,
|
||||||
|
interface: iface,
|
||||||
|
provider: provider,
|
||||||
|
enabled: enabled
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Component {
|
||||||
|
StyledRect {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: ListView.view ? ListView.view.width : undefined
|
||||||
|
|
||||||
|
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
function onClicked(): void {
|
||||||
|
if (root.session && root.session.vpn) {
|
||||||
|
root.session.vpn.active = modelData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: rowLayout
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Appearance.padding.normal
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off"
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
fill: modelData.enabled && VPN.connected ? 1 : 0
|
||||||
|
color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
|
||||||
|
text: modelData.displayName || qsTr("Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
if (modelData.enabled && VPN.connected) return qsTr("Connected");
|
||||||
|
if (modelData.enabled && VPN.connecting) return qsTr("Connecting...");
|
||||||
|
if (modelData.enabled) return qsTr("Enabled");
|
||||||
|
return qsTr("Disabled");
|
||||||
|
}
|
||||||
|
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
font.weight: modelData.enabled && VPN.connected ? 500 : 400
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||||
|
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0)
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
enabled: !VPN.connecting
|
||||||
|
function onClicked(): void {
|
||||||
|
const clickedIndex = modelData.index;
|
||||||
|
|
||||||
|
if (modelData.enabled) {
|
||||||
|
VPN.toggle();
|
||||||
|
} else {
|
||||||
|
if (VPN.connected) {
|
||||||
|
root.pendingSwitchIndex = clickedIndex;
|
||||||
|
VPN.toggle();
|
||||||
|
} else {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
const p = Config.utilities.vpn.provider[i];
|
||||||
|
if (typeof p === "object") {
|
||||||
|
const newProvider = {
|
||||||
|
name: p.name,
|
||||||
|
displayName: p.displayName,
|
||||||
|
interface: p.interface,
|
||||||
|
enabled: (i === clickedIndex)
|
||||||
|
};
|
||||||
|
providers.push(newProvider);
|
||||||
|
} else {
|
||||||
|
providers.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
VPN.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: connectIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: VPN.connected && modelData.enabled ? "link_off" : "link"
|
||||||
|
color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||||
|
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
function onClicked(): void {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
if (i !== modelData.index) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: deleteIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "delete"
|
||||||
|
color: Colours.palette.m3onSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: vpnDialog
|
||||||
|
|
||||||
|
property string currentState: "selection"
|
||||||
|
property int editIndex: -1
|
||||||
|
property string providerName: ""
|
||||||
|
property string displayName: ""
|
||||||
|
property string interfaceName: ""
|
||||||
|
|
||||||
|
parent: Overlay.overlay
|
||||||
|
x: Math.round((parent.width - width) / 2)
|
||||||
|
y: Math.round((parent.height - height) / 2)
|
||||||
|
implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||||
|
padding: Appearance.padding.large * 1.5
|
||||||
|
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
scale: 0.7
|
||||||
|
|
||||||
|
enter: Transition {
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Appearance.anim.durations.normal; easing.bezierCurve: Appearance.anim.curves.emphasized }
|
||||||
|
NumberAnimation { property: "scale"; from: 0.7; to: 1; duration: Appearance.anim.durations.normal; easing.bezierCurve: Appearance.anim.curves.emphasized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit: Transition {
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Appearance.anim.durations.small; easing.bezierCurve: Appearance.anim.curves.emphasized }
|
||||||
|
NumberAnimation { property: "scale"; from: 1; to: 0.7; duration: Appearance.anim.durations.small; easing.bezierCurve: Appearance.anim.curves.emphasized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProviderSelection(): void {
|
||||||
|
currentState = "selection";
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWithAnimation(): void {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddForm(providerType: string, defaultDisplayName: string): void {
|
||||||
|
editIndex = -1;
|
||||||
|
providerName = providerType;
|
||||||
|
displayName = defaultDisplayName;
|
||||||
|
interfaceName = "";
|
||||||
|
|
||||||
|
if (currentState === "selection") {
|
||||||
|
transitionToForm.start();
|
||||||
|
} else {
|
||||||
|
currentState = "form";
|
||||||
|
isClosing = false;
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditForm(index: int): void {
|
||||||
|
const provider = Config.utilities.vpn.provider[index];
|
||||||
|
const isObject = typeof provider === "object";
|
||||||
|
|
||||||
|
editIndex = index;
|
||||||
|
providerName = isObject ? (provider.name || "custom") : String(provider);
|
||||||
|
displayName = isObject ? (provider.displayName || providerName) : providerName;
|
||||||
|
interfaceName = isObject ? (provider.interface || "") : "";
|
||||||
|
|
||||||
|
currentState = "form";
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
Overlay.modal: Rectangle {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
currentState = "selection";
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: transitionToForm
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
target: selectionContent
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptAction {
|
||||||
|
script: {
|
||||||
|
vpnDialog.currentState = "form";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
target: formContent
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
color: Colours.palette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: DropShadow {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
radius: 16
|
||||||
|
samples: 33
|
||||||
|
verticalOffset: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.anim.durations.normal
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.anim.durations.normal
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: selectionContent
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
visible: vpnDialog.currentState === "selection"
|
||||||
|
opacity: vpnDialog.currentState === "selection" ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Add VPN Provider")
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
font.weight: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Choose a provider to add")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.preferredHeight: Appearance.spacing.small }
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("NetBird")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" });
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
vpnDialog.closeWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Tailscale")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" });
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
vpnDialog.closeWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Cloudflare WARP")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" });
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
vpnDialog.closeWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("WireGuard (Custom)")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: {
|
||||||
|
vpnDialog.showAddForm("wireguard", "WireGuard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.preferredHeight: Appearance.spacing.small }
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
inactiveColour: Colours.palette.m3secondaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||||
|
onClicked: vpnDialog.closeWithAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: formContent
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
visible: vpnDialog.currentState === "form"
|
||||||
|
opacity: vpnDialog.currentState === "form" ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName)
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
font.weight: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.smaller / 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Display Name")
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 40
|
||||||
|
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
border.width: 1
|
||||||
|
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||||
|
|
||||||
|
Behavior on color { CAnim {} }
|
||||||
|
Behavior on border.color { CAnim {} }
|
||||||
|
|
||||||
|
StyledTextField {
|
||||||
|
id: displayNameField
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Appearance.padding.normal
|
||||||
|
horizontalAlignment: TextInput.AlignLeft
|
||||||
|
text: vpnDialog.displayName
|
||||||
|
onTextChanged: vpnDialog.displayName = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.smaller / 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 40
|
||||||
|
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
border.width: 1
|
||||||
|
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||||
|
|
||||||
|
Behavior on color { CAnim {} }
|
||||||
|
Behavior on border.color { CAnim {} }
|
||||||
|
|
||||||
|
StyledTextField {
|
||||||
|
id: interfaceNameField
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Appearance.padding.normal
|
||||||
|
horizontalAlignment: TextInput.AlignLeft
|
||||||
|
text: vpnDialog.interfaceName
|
||||||
|
onTextChanged: vpnDialog.interfaceName = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.preferredHeight: Appearance.spacing.normal }
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
onClicked: vpnDialog.closeWithAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Save")
|
||||||
|
enabled: vpnDialog.interfaceName.length > 0
|
||||||
|
inactiveColour: Colours.palette.m3primaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [];
|
||||||
|
const newProvider = {
|
||||||
|
name: vpnDialog.providerName,
|
||||||
|
displayName: vpnDialog.displayName || vpnDialog.interfaceName,
|
||||||
|
interface: vpnDialog.interfaceName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vpnDialog.editIndex >= 0) {
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
if (i === vpnDialog.editIndex) {
|
||||||
|
providers.push(newProvider);
|
||||||
|
} else {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||||
|
providers.push(Config.utilities.vpn.provider[i]);
|
||||||
|
}
|
||||||
|
providers.push(newProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
vpnDialog.closeWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
232
modules/controlcenter/network/VpnSettings.qml
Normal file
232
modules/controlcenter/network/VpnSettings.qml
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import ".."
|
||||||
|
import "../components"
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.components.containers
|
||||||
|
import qs.components.effects
|
||||||
|
import qs.services
|
||||||
|
import qs.config
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property Session session
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
SettingsHeader {
|
||||||
|
icon: "vpn_key"
|
||||||
|
title: qsTr("VPN Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
Layout.topMargin: Appearance.spacing.large
|
||||||
|
title: qsTr("General")
|
||||||
|
description: qsTr("VPN configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
ToggleRow {
|
||||||
|
label: qsTr("VPN enabled")
|
||||||
|
checked: Config.utilities.vpn.enabled
|
||||||
|
toggle.onToggled: {
|
||||||
|
Config.utilities.vpn.enabled = checked;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
Layout.topMargin: Appearance.spacing.large
|
||||||
|
title: qsTr("Providers")
|
||||||
|
description: qsTr("Manage VPN providers")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
contentSpacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: contentHeight
|
||||||
|
|
||||||
|
interactive: false
|
||||||
|
spacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||||
|
const isObject = typeof provider === "object";
|
||||||
|
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||||
|
const displayName = isObject ? (provider.displayName || name) : name;
|
||||||
|
const iface = isObject ? (provider.interface || "") : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: index,
|
||||||
|
name: name,
|
||||||
|
displayName: displayName,
|
||||||
|
interface: iface,
|
||||||
|
provider: provider,
|
||||||
|
isActive: index === 0
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Component {
|
||||||
|
StyledRect {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: ListView.view ? ListView.view.width : undefined
|
||||||
|
color: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Appearance.padding.normal
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
text: modelData.isActive ? "vpn_key" : "vpn_key_off"
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.displayName
|
||||||
|
font.weight: modelData.isActive ? 500 : 400
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface"))
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: modelData.isActive ? "arrow_downward" : "arrow_upward"
|
||||||
|
visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) {
|
||||||
|
// Move down
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
const temp = providers[index];
|
||||||
|
providers[index] = providers[index + 1];
|
||||||
|
providers[index + 1] = temp;
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
} else if (!modelData.isActive) {
|
||||||
|
// Make active (move to top)
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
const provider = providers.splice(index, 1)[0];
|
||||||
|
providers.unshift(provider);
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "delete"
|
||||||
|
onClicked: {
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
providers.splice(index, 1);
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Appearance.spacing.normal
|
||||||
|
text: qsTr("+ Add Provider")
|
||||||
|
inactiveColour: Colours.palette.m3primaryContainer
|
||||||
|
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
addProviderDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
Layout.topMargin: Appearance.spacing.large
|
||||||
|
title: qsTr("Quick Add")
|
||||||
|
description: qsTr("Add common VPN providers")
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
contentSpacing: Appearance.spacing.smaller
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("+ Add NetBird")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
providers.push({
|
||||||
|
name: "netbird",
|
||||||
|
displayName: "NetBird",
|
||||||
|
interface: "wt0"
|
||||||
|
});
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("+ Add Tailscale")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
providers.push({
|
||||||
|
name: "tailscale",
|
||||||
|
displayName: "Tailscale",
|
||||||
|
interface: "tailscale0"
|
||||||
|
});
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("+ Add Cloudflare WARP")
|
||||||
|
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||||
|
inactiveOnColour: Colours.palette.m3onSurface
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const providers = [...Config.utilities.vpn.provider];
|
||||||
|
providers.push({
|
||||||
|
name: "warp",
|
||||||
|
displayName: "Cloudflare WARP",
|
||||||
|
interface: "CloudflareWARP"
|
||||||
|
});
|
||||||
|
Config.utilities.vpn.provider = providers;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
modules/controlcenter/state/VpnState.qml
Normal file
5
modules/controlcenter/state/VpnState.qml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property var active: null
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ StyledRect {
|
||||||
icon: "vpn_key"
|
icon: "vpn_key"
|
||||||
checked: VPN.connected
|
checked: VPN.connected
|
||||||
enabled: !VPN.connecting
|
enabled: !VPN.connecting
|
||||||
visible: VPN.enabled
|
visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
|
||||||
onClicked: VPN.toggle()
|
onClicked: VPN.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ Singleton {
|
||||||
property bool connected: false
|
property bool connected: false
|
||||||
|
|
||||||
readonly property bool connecting: connectProc.running || disconnectProc.running
|
readonly property bool connecting: connectProc.running || disconnectProc.running
|
||||||
readonly property bool enabled: Config.utilities.vpn.enabled
|
readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
|
||||||
readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard"
|
readonly property var providerInput: {
|
||||||
|
const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false);
|
||||||
|
return enabledProvider || "wireguard";
|
||||||
|
}
|
||||||
readonly property bool isCustomProvider: typeof providerInput === "object"
|
readonly property bool isCustomProvider: typeof providerInput === "object"
|
||||||
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
||||||
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue