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
|
||||
},
|
||||
"vpn": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"provider": [
|
||||
{
|
||||
"name": "wireguard",
|
||||
"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 EthernetState ethernet: EthernetState {}
|
||||
readonly property LauncherState launcher: LauncherState {}
|
||||
readonly property VpnState vpn: VpnState {}
|
||||
|
||||
onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))
|
||||
onActiveIndexChanged: if (panes[activeIndex]) active = panes[activeIndex]
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ 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 QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
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 {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Current connection")
|
||||
|
|
@ -94,5 +135,38 @@ ColumnLayout {
|
|||
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 {
|
||||
id: ethernetListSection
|
||||
|
||||
|
|
@ -154,14 +172,16 @@ Item {
|
|||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var ethernetPane: root.session.ethernet.active
|
||||
property var wirelessPane: root.session.network.active
|
||||
property var pane: ethernetPane || wirelessPane
|
||||
property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")
|
||||
property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null
|
||||
property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null
|
||||
property var wirelessPane: root.session && root.session.network ? root.session.network.active : null
|
||||
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 nextComponent: settingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
if (vpnPane) return vpnDetailsComponent;
|
||||
if (ethernetPane) return ethernetDetailsComponent;
|
||||
if (wirelessPane) return wirelessDetailsComponent;
|
||||
return settingsComponent;
|
||||
|
|
@ -173,28 +193,44 @@ Item {
|
|||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.ethernet
|
||||
target: root.session && root.session.vpn ? root.session.vpn : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear wireless when ethernet is selected
|
||||
if (root.session.ethernet.active && root.session.network.active) {
|
||||
root.session.network.active = null;
|
||||
return; // Let the network.onActiveChanged handle the update
|
||||
// Clear others when VPN is selected
|
||||
if (root.session && root.session.vpn && root.session.vpn.active) {
|
||||
if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null;
|
||||
if (root.session.network && root.session.network.active) root.session.network.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
// paneId will automatically update via property binding
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.network
|
||||
target: root.session && root.session.ethernet ? root.session.ethernet : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear ethernet when wireless is selected
|
||||
if (root.session.network.active && root.session.ethernet.active) {
|
||||
root.session.ethernet.active = null;
|
||||
return; // Let the ethernet.onActiveChanged handle the update
|
||||
// Clear others when ethernet is selected
|
||||
if (root.session && root.session.ethernet && root.session.ethernet.active) {
|
||||
if (root.session.vpn && root.session.vpn.active) root.session.vpn.active = null;
|
||||
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();
|
||||
// paneId will automatically update via property binding
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +244,7 @@ Item {
|
|||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
asynchronous: true
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
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"
|
||||
checked: VPN.connected
|
||||
enabled: !VPN.connecting
|
||||
visible: VPN.enabled
|
||||
visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
|
||||
onClicked: VPN.toggle()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ Singleton {
|
|||
property bool connected: false
|
||||
|
||||
readonly property bool connecting: connectProc.running || disconnectProc.running
|
||||
readonly property bool enabled: Config.utilities.vpn.enabled
|
||||
readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard"
|
||||
readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false)
|
||||
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 string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
||||
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue