1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 15:32:50 -05:00

net: switch to native VPN backend

This commit is contained in:
bbedward
2025-10-25 11:47:46 -04:00
parent 607b5320fd
commit 032777e32e
9 changed files with 234 additions and 304 deletions

View File

@@ -216,8 +216,8 @@ Item {
Connections { Connections {
target: NetworkService target: NetworkService
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason) { function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason) wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
} }
} }

View File

@@ -21,6 +21,11 @@ DankModal {
property var promptFields: [] property var promptFields: []
property string promptSetting: "" property string promptSetting: ""
property bool isVpnPrompt: false
property string connectionName: ""
property string vpnServiceType: ""
property string connectionType: ""
function show(ssid) { function show(ssid) {
wifiPasswordSSID = ssid wifiPasswordSSID = ssid
wifiPasswordInput = "" wifiPasswordInput = ""
@@ -32,6 +37,10 @@ DankModal {
promptReason = "" promptReason = ""
promptFields = [] promptFields = []
promptSetting = "" promptSetting = ""
isVpnPrompt = false
connectionName = ""
vpnServiceType = ""
connectionType = ""
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid) const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false requiresEnterprise = network?.enterprise || false
@@ -48,13 +57,18 @@ DankModal {
}) })
} }
function showFromPrompt(token, ssid, setting, fields, hints, reason) { function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
wifiPasswordSSID = ssid
isPromptMode = true isPromptMode = true
promptToken = token promptToken = token
promptReason = reason promptReason = reason
promptFields = fields || [] promptFields = fields || []
promptSetting = setting || "802-11-wireless-security" promptSetting = setting || "802-11-wireless-security"
connectionType = connType || "802-11-wireless"
connectionName = connName || ssid || ""
vpnServiceType = vpnService || ""
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
requiresEnterprise = setting === "802-1x" requiresEnterprise = setting === "802-1x"
@@ -163,7 +177,12 @@ DankModal {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: I18n.tr("Connect to Wi-Fi") text: {
if (isVpnPrompt) {
return I18n.tr("Connect to VPN")
}
return I18n.tr("Connect to Wi-Fi")
}
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
@@ -175,6 +194,9 @@ DankModal {
StyledText { StyledText {
text: { text: {
if (isVpnPrompt) {
return I18n.tr("Enter password for ") + wifiPasswordSSID
}
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ") const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
return prefix + wifiPasswordSSID return prefix + wifiPasswordSSID
} }
@@ -218,7 +240,7 @@ DankModal {
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1 border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise visible: requiresEnterprise && !isVpnPrompt
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -271,7 +293,7 @@ DankModal {
textColor: Theme.surfaceText textColor: Theme.surfaceText
text: wifiPasswordInput text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: requiresEnterprise ? I18n.tr("Password") : "" placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
backgroundColor: "transparent" backgroundColor: "transparent"
focus: !requiresEnterprise focus: !requiresEnterprise
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
@@ -281,7 +303,9 @@ DankModal {
onAccepted: () => { onAccepted: () => {
if (isPromptMode) { if (isPromptMode) {
const secrets = {} const secrets = {}
if (promptSetting === "802-11-wireless-security") { if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") { } else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text if (usernameInput.text) secrets["identity"] = usernameInput.text
@@ -340,7 +364,7 @@ DankModal {
} }
Rectangle { Rectangle {
visible: requiresEnterprise visible: requiresEnterprise && !isVpnPrompt
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -372,7 +396,7 @@ DankModal {
} }
Rectangle { Rectangle {
visible: requiresEnterprise visible: requiresEnterprise && !isVpnPrompt
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -495,7 +519,12 @@ DankModal {
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0 enabled: {
if (isVpnPrompt) {
return passwordInput.text.length > 0
}
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
}
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
StyledText { StyledText {
@@ -518,7 +547,9 @@ DankModal {
onClicked: () => { onClicked: () => {
if (isPromptMode) { if (isPromptMode) {
const secrets = {} const secrets = {}
if (promptSetting === "802-11-wireless-security") { if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") { } else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text if (usernameInput.text) secrets["identity"] = usernameInput.text

View File

@@ -10,26 +10,26 @@ PluginComponent {
id: root id: root
Ref { Ref {
service: VpnService service: DMSNetworkService
} }
ccWidgetIcon: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN" ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: { ccWidgetSecondaryText: {
if (!VpnService.connected) if (!DMSNetworkService.connected)
return "Disconnected" return "Disconnected"
const names = VpnService.activeNames || [] const names = DMSNetworkService.activeNames || []
if (names.length <= 1) if (names.length <= 1)
return names[0] || "Connected" return names[0] || "Connected"
return names[0] + " +" + (names.length - 1) return names[0] + " +" + (names.length - 1)
} }
ccWidgetIsActive: VpnService.connected ccWidgetIsActive: DMSNetworkService.connected
onCcWidgetToggled: { onCcWidgetToggled: {
if (VpnService.connected) { if (DMSNetworkService.connected) {
VpnService.disconnectAllActive() DMSNetworkService.disconnectAllActive()
} else if (VpnService.profiles.length > 0) { } else if (DMSNetworkService.profiles.length > 0) {
VpnService.connect(VpnService.profiles[0].uuid) DMSNetworkService.connect(DMSNetworkService.profiles[0].uuid)
} }
} }
@@ -52,9 +52,9 @@ PluginComponent {
StyledText { StyledText {
text: { text: {
if (!VpnService.connected) if (!DMSNetworkService.connected)
return "Active: None" return "Active: None"
const names = VpnService.activeNames || [] const names = DMSNetworkService.activeNames || []
if (names.length <= 1) if (names.length <= 1)
return "Active: " + (names[0] || "VPN") return "Active: " + (names[0] || "VPN")
return "Active: " + names[0] + " +" + (names.length - 1) return "Active: " + names[0] + " +" + (names.length - 1)
@@ -72,7 +72,7 @@ PluginComponent {
height: 28 height: 28
radius: 14 radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: VpnService.connected visible: DMSNetworkService.connected
width: 110 width: 110
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
@@ -99,7 +99,7 @@ PluginComponent {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: VpnService.disconnectAllActive() onClicked: DMSNetworkService.disconnectAllActive()
} }
} }
} }
@@ -123,7 +123,7 @@ PluginComponent {
Item { Item {
width: parent.width width: parent.width
height: VpnService.profiles.length === 0 ? 120 : 0 height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0 visible: height > 0
Column { Column {
@@ -154,7 +154,7 @@ PluginComponent {
} }
Repeater { Repeater {
model: VpnService.profiles model: DMSNetworkService.profiles
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
@@ -162,9 +162,9 @@ PluginComponent {
width: parent ? parent.width : 300 width: parent ? parent.width : 300
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1 border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
@@ -174,9 +174,9 @@ PluginComponent {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off" name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -187,7 +187,7 @@ PluginComponent {
StyledText { StyledText {
text: modelData.name text: modelData.name
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
} }
StyledText { StyledText {
@@ -234,7 +234,7 @@ PluginComponent {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: VpnService.toggle(modelData.uuid) onClicked: DMSNetworkService.toggle(modelData.uuid)
} }
} }
} }

View File

@@ -143,8 +143,8 @@ QtObject {
"description": "VPN connections", "description": "VPN connections",
"icon": "vpn_key", "icon": "vpn_key",
"type": "builtin_plugin", "type": "builtin_plugin",
"enabled": VpnService.available, "enabled": DMSNetworkService.available,
"warning": !VpnService.available ? "VPN not available" : undefined, "warning": !DMSNetworkService.available ? "VPN not available" : undefined,
"isBuiltinPlugin": true "isBuiltinPlugin": true
}] }]

View File

@@ -14,7 +14,7 @@ DankPopout {
id: root id: root
Ref { Ref {
service: VpnService service: DMSNetworkService
} }
property var triggerScreen: null property var triggerScreen: null
@@ -161,11 +161,11 @@ DankPopout {
StyledText { StyledText {
text: { text: {
if (!VpnService.connected) { if (!DMSNetworkService.connected) {
return "Active: None"; return "Active: None";
} }
const names = VpnService.activeNames || []; const names = DMSNetworkService.activeNames || [];
if (names.length <= 1) { if (names.length <= 1) {
return "Active: " + (names[0] || "VPN"); return "Active: " + (names[0] || "VPN");
} }
@@ -193,7 +193,7 @@ DankPopout {
height: 28 height: 28
radius: 14 radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: VpnService.connected visible: DMSNetworkService.connected
width: 130 width: 130
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
border.width: 0 border.width: 0
@@ -224,7 +224,7 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: VpnService.disconnectAllActive() onClicked: DMSNetworkService.disconnectAllActive()
} }
} }
@@ -251,7 +251,7 @@ DankPopout {
Item { Item {
width: parent.width width: parent.width
height: VpnService.profiles.length === 0 ? 120 : 0 height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0 visible: height > 0
Column { Column {
@@ -284,7 +284,7 @@ DankPopout {
} }
Repeater { Repeater {
model: VpnService.profiles model: DMSNetworkService.profiles
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
@@ -292,9 +292,9 @@ DankPopout {
width: parent ? parent.width : 300 width: parent ? parent.width : 300
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight) color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1 border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
@@ -304,9 +304,9 @@ DankPopout {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: VpnService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off" name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -317,7 +317,7 @@ DankPopout {
StyledText { StyledText {
text: modelData.name text: modelData.name
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
} }
StyledText { StyledText {
@@ -392,7 +392,7 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: VpnService.toggle(modelData.uuid) onClicked: DMSNetworkService.toggle(modelData.uuid)
} }
} }

View File

@@ -9,7 +9,7 @@ BasePill {
id: root id: root
Ref { Ref {
service: VpnService service: DMSNetworkService
} }
property var popoutTarget: null property var popoutTarget: null
@@ -24,9 +24,9 @@ BasePill {
DankIcon { DankIcon {
id: icon id: icon
name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") name: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
size: Theme.barIconSize(root.barThickness, -4) size: Theme.barIconSize(root.barThickness, -4)
color: VpnService.connected ? Theme.primary : Theme.surfaceText color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
@@ -59,10 +59,10 @@ BasePill {
tooltipLoader.active = true tooltipLoader.active = true
if (tooltipLoader.item) { if (tooltipLoader.item) {
let tooltipText = "" let tooltipText = ""
if (!VpnService.connected) { if (!DMSNetworkService.connected) {
tooltipText = "VPN Disconnected" tooltipText = "VPN Disconnected"
} else { } else {
const names = VpnService.activeNames || [] const names = DMSNetworkService.activeNames || []
if (names.length <= 1) { if (names.length <= 1) {
tooltipText = "VPN Connected • " + (names[0] || "") tooltipText = "VPN Connected • " + (names[0] || "")
} else { } else {

View File

@@ -69,6 +69,24 @@ Singleton {
property string wifiPassword: "" property string wifiPassword: ""
property string forgetSSID: "" property string forgetSSID: ""
property var vpnProfiles: []
property var vpnActive: []
property bool vpnAvailable: false
property bool vpnIsBusy: false
property alias profiles: root.vpnProfiles
property alias activeConnections: root.vpnActive
property var activeUuids: vpnActive.map(v => v.uuid).filter(u => !!u)
property var activeNames: vpnActive.map(v => v.name).filter(n => !!n)
property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : ""
property string activeName: activeNames.length > 0 ? activeNames[0] : ""
property string activeDevice: vpnActive.length > 0 ? (vpnActive[0].device || "") : ""
property string activeState: vpnActive.length > 0 ? (vpnActive[0].state || "") : ""
property bool vpnConnected: activeUuids.length > 0
property alias available: root.vpnAvailable
property alias isBusy: root.vpnIsBusy
property alias connected: root.vpnConnected
property string networkInfoSSID: "" property string networkInfoSSID: ""
property string networkInfoDetails: "" property string networkInfoDetails: ""
property bool networkInfoLoading: false property bool networkInfoLoading: false
@@ -94,7 +112,7 @@ Singleton {
signal networksUpdated signal networksUpdated
signal connectionChanged signal connectionChanged
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason) signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason, string connType, string connName, string vpnService)
readonly property string socketPath: Quickshell.env("DMS_SOCKET") readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -164,7 +182,11 @@ Singleton {
credentialsReason = data.reason || "Credentials required" credentialsReason = data.reason || "Credentials required"
credentialsRequested = true credentialsRequested = true
credentialsNeeded(credentialsToken, credentialsSSID, credentialsSetting, credentialsFields, credentialsHints, credentialsReason) const connType = data.connType || ""
const connName = data.name || data.connectionId || ""
const vpnService = data.vpnService || ""
credentialsNeeded(credentialsToken, credentialsSSID, credentialsSetting, credentialsFields, credentialsHints, credentialsReason, connType, connName, vpnService)
} }
function addRef() { function addRef() {
@@ -202,6 +224,7 @@ Singleton {
const previousConnectingSSID = connectingSSID const previousConnectingSSID = connectingSSID
backend = state.backend || "" backend = state.backend || ""
vpnAvailable = networkAvailable && backend === "networkmanager"
networkStatus = state.networkStatus || "disconnected" networkStatus = state.networkStatus || "disconnected"
primaryConnection = state.primaryConnection || "" primaryConnection = state.primaryConnection || ""
@@ -244,6 +267,12 @@ Singleton {
networksUpdated() networksUpdated()
} }
if (state.vpnProfiles) {
vpnProfiles = state.vpnProfiles
}
vpnActive = state.vpnActive || []
userPreference = state.preference || "auto" userPreference = state.preference || "auto"
isConnecting = state.isConnecting || false isConnecting = state.isConnecting || false
connectingSSID = state.connectingSSID || "" connectingSSID = state.connectingSSID || ""
@@ -677,6 +706,122 @@ Singleton {
} }
} }
function refreshVpnProfiles() {
if (!vpnAvailable) return
DMSService.sendRequest("network.vpn.profiles", null, response => {
if (response.result) {
vpnProfiles = response.result
}
})
}
function refreshVpnActive() {
if (!vpnAvailable) return
DMSService.sendRequest("network.vpn.active", null, response => {
if (response.result) {
vpnActive = response.result
}
})
}
function connectVpn(uuidOrName, singleActive = false) {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
const params = {
uuidOrName: uuidOrName,
singleActive: singleActive
}
DMSService.sendRequest("network.vpn.connect", params, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to connect VPN"))
} else {
Qt.callLater(() => getState())
}
})
}
function connect(uuidOrName, singleActive = false) {
connectVpn(uuidOrName, singleActive)
}
function disconnectVpn(uuidOrName) {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
const params = {
uuidOrName: uuidOrName
}
DMSService.sendRequest("network.vpn.disconnect", params, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to disconnect VPN"))
} else {
Qt.callLater(() => getState())
}
})
}
function disconnect(uuidOrName) {
disconnectVpn(uuidOrName)
}
function disconnectAllVpns() {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
DMSService.sendRequest("network.vpn.disconnectAll", null, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to disconnect VPNs"))
} else {
Qt.callLater(() => getState())
}
})
}
function disconnectAllActive() {
disconnectAllVpns()
}
function toggleVpn(uuid) {
if (uuid) {
if (isActiveVpnUuid(uuid)) {
disconnectVpn(uuid)
} else {
connectVpn(uuid)
}
return
}
if (vpnProfiles.length > 0) {
connectVpn(vpnProfiles[0].uuid)
}
}
function toggle(uuid) {
toggleVpn(uuid)
}
function isActiveVpnUuid(uuid) {
return activeUuids && activeUuids.indexOf(uuid) !== -1
}
function isActiveUuid(uuid) {
return isActiveVpnUuid(uuid)
}
function refreshNetworkState() { function refreshNetworkState() {
if (networkAvailable) { if (networkAvailable) {
getState() getState()

View File

@@ -80,7 +80,7 @@ Singleton {
signal networksUpdated signal networksUpdated
signal connectionChanged signal connectionChanged
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason) signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason, string connType, string connName, string vpnService)
property bool usingLegacy: false property bool usingLegacy: false
property var activeService: null property var activeService: null

View File

@@ -1,246 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
// Minimal VPN controller backed by NetworkManager (nmcli + D-Bus monitor)
Singleton {
id: root
property int refCount: 0
onRefCountChanged: {
console.log("VpnService: refCount changed to", refCount)
if (refCount > 0 && !nmMonitor.running) {
console.log("VpnService: Starting nmMonitor")
nmMonitor.running = true
refreshAll()
} else if (refCount === 0 && nmMonitor.running) {
console.log("VpnService: Stopping nmMonitor")
nmMonitor.running = false
}
}
// State
property bool available: true
property bool isBusy: false
property string errorMessage: ""
// Profiles discovered on the system
// [{ name, uuid, type }]
property var profiles: []
// Allow multiple active VPNs (set true to allow concurrent connections)
// Default: allow multiple, to align with NetworkManager capability
property bool singleActive: false
// Active VPN connections (may be multiple)
// Full list and convenience projections
property var activeConnections: [] // [{ name, uuid, device, state }]
property var activeUuids: []
property var activeNames: []
// Back-compat single values (first active if present)
property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : ""
property string activeName: activeNames.length > 0 ? activeNames[0] : ""
property string activeDevice: activeConnections.length > 0 ? (activeConnections[0].device || "") : ""
property string activeState: activeConnections.length > 0 ? (activeConnections[0].state || "") : ""
property bool connected: activeUuids.length > 0
// Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.)
function refreshAll() {
listProfiles()
refreshActive()
}
// Monitor NetworkManager changes and refresh on activity
Process {
id: nmMonitor
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.includes("ActiveConnection") || line.includes("PropertiesChanged") || line.includes("StateChanged")) {
refreshAll()
}
}
}
}
// Query all VPN profiles
function listProfiles() {
getProfiles.running = true
}
Process {
id: getProfiles
command: ["bash", "-lc", "nmcli -t -f NAME,UUID,TYPE connection show | while IFS=: read -r name uuid type; do case \"$type\" in vpn) svc=$(nmcli -g vpn.service-type connection show uuid \"$uuid\" 2>/dev/null); echo \"$name:$uuid:$type:$svc\" ;; wireguard) echo \"$name:$uuid:$type:\" ;; *) : ;; esac; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().length ? text.trim().split('\n') : []
const out = []
for (const line of lines) {
const parts = line.split(':')
if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
const svc = parts.length >= 4 ? parts[3] : ""
out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc })
}
}
root.profiles = out
}
}
}
// Query active VPN connection
function refreshActive() {
getActive.running = true
}
Process {
id: getActive
command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().length ? text.trim().split('\n') : []
let act = []
for (const line of lines) {
const parts = line.split(':')
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] })
}
}
root.activeConnections = act
root.activeUuids = act.map(a => a.uuid).filter(u => !!u)
root.activeNames = act.map(a => a.name).filter(n => !!n)
}
}
}
function isActiveUuid(uuid) {
return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1
}
function _looksLikeUuid(s) {
// Very loose check for UUID pattern
return s && s.indexOf('-') !== -1 && s.length >= 8
}
function connect(uuidOrName) {
if (root.isBusy) return
root.isBusy = true
root.errorMessage = ""
if (root.singleActive) {
// Bring down all active VPNs, then bring up the requested one
const isUuid = _looksLikeUuid(uuidOrName)
const escaped = ('' + uuidOrName).replace(/'/g, "'\\''")
const upCmd = isUuid ? `nmcli connection up uuid '${escaped}'` : `nmcli connection up id '${escaped}'`
const script = `set -e\n` +
`nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done\n` +
upCmd + `\n`
vpnSwitch.command = ["bash", "-lc", script]
vpnSwitch.running = true
} else {
if (_looksLikeUuid(uuidOrName)) {
vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName]
} else {
vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName]
}
vpnUp.running = true
}
}
function disconnect(uuidOrName) {
if (root.isBusy) return
root.isBusy = true
root.errorMessage = ""
if (_looksLikeUuid(uuidOrName)) {
vpnDown.command = ["nmcli", "connection", "down", "uuid", uuidOrName]
} else {
vpnDown.command = ["nmcli", "connection", "down", "id", uuidOrName]
}
vpnDown.running = true
}
function toggle(uuid) {
if (uuid) {
if (isActiveUuid(uuid)) disconnect(uuid)
else connect(uuid)
return
}
if (root.profiles.length > 0) {
connect(root.profiles[0].uuid)
}
}
Process {
id: vpnUp
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
if (!text.toLowerCase().includes("successfully")) {
root.errorMessage = text.trim()
}
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to connect VPN"
}
}
}
Process {
id: vpnDown
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
if (!text.toLowerCase().includes("deactivated") && !text.toLowerCase().includes("successfully")) {
root.errorMessage = text.trim()
}
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to disconnect VPN"
}
}
}
function disconnectAllActive() {
if (root.isBusy) return
root.isBusy = true
const script = `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done`
vpnSwitch.command = ["bash", "-lc", script]
vpnSwitch.running = true
}
// Sequenced down/up using a single shell for exclusive switch
Process {
id: vpnSwitch
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to switch VPN"
}
}
}
}