mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
VPN: enforce single-active by default; gracefully handle multi-active state; header summary fix; popout rows are full-row actions with correct active highlighting
This commit is contained in:
@@ -149,7 +149,12 @@ DankPopout {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None"
|
text: {
|
||||||
|
if (!VpnService.connected) return "Active: None"
|
||||||
|
const names = VpnService.activeNames || []
|
||||||
|
if (names.length <= 1) return "Active: " + (names[0] || "VPN")
|
||||||
|
return "Active: " + names[0] + " +" + (names.length - 1)
|
||||||
|
}
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -218,9 +223,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 : (modelData.uuid === VpnService.activeUuid ? Theme.primaryPressed : Theme.surfaceLight)
|
color: rowArea.containsMouse ? Theme.primaryHoverLight : (VpnService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
|
||||||
border.width: modelData.uuid === VpnService.activeUuid ? 2 : 1
|
border.width: VpnService.isActiveUuid(modelData.uuid) ? 2 : 1
|
||||||
border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.outlineLight
|
border.color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -232,7 +237,7 @@ DankPopout {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off"
|
name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off"
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText
|
color: VpnService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,20 @@ Singleton {
|
|||||||
// [{ name, uuid, type }]
|
// [{ name, uuid, type }]
|
||||||
property var profiles: []
|
property var profiles: []
|
||||||
|
|
||||||
// Active VPN connection (if any)
|
// Enforce single active VPN at a time
|
||||||
property string activeUuid: ""
|
property bool singleActive: true
|
||||||
property string activeName: ""
|
|
||||||
property string activeDevice: ""
|
// Active VPN connections (may be multiple)
|
||||||
property string activeState: "" // activating, activated, deactivating
|
// Full list and convenience projections
|
||||||
property bool connected: activeUuid !== "" && activeState === "activated"
|
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.)
|
// Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.)
|
||||||
|
|
||||||
@@ -92,28 +100,24 @@ Singleton {
|
|||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const lines = text.trim().length ? text.trim().split('\n') : []
|
const lines = text.trim().length ? text.trim().split('\n') : []
|
||||||
let found = false
|
let act = []
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const parts = line.split(':')
|
const parts = line.split(':')
|
||||||
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
|
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
|
||||||
root.activeName = parts[0]
|
act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] })
|
||||||
root.activeUuid = parts[1]
|
|
||||||
root.activeDevice = parts[3]
|
|
||||||
root.activeState = parts[4]
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
root.activeConnections = act
|
||||||
root.activeName = ""
|
root.activeUuids = act.map(a => a.uuid).filter(u => !!u)
|
||||||
root.activeUuid = ""
|
root.activeNames = act.map(a => a.name).filter(n => !!n)
|
||||||
root.activeDevice = ""
|
|
||||||
root.activeState = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isActiveUuid(uuid) {
|
||||||
|
return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
function _looksLikeUuid(s) {
|
function _looksLikeUuid(s) {
|
||||||
// Very loose check for UUID pattern
|
// Very loose check for UUID pattern
|
||||||
return s && s.indexOf('-') !== -1 && s.length >= 8
|
return s && s.indexOf('-') !== -1 && s.length >= 8
|
||||||
@@ -123,12 +127,24 @@ Singleton {
|
|||||||
if (root.isBusy) return
|
if (root.isBusy) return
|
||||||
root.isBusy = true
|
root.isBusy = true
|
||||||
root.errorMessage = ""
|
root.errorMessage = ""
|
||||||
if (_looksLikeUuid(uuidOrName)) {
|
if (root.singleActive) {
|
||||||
vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName]
|
// 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 {
|
} else {
|
||||||
vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName]
|
if (_looksLikeUuid(uuidOrName)) {
|
||||||
|
vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName]
|
||||||
|
} else {
|
||||||
|
vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName]
|
||||||
|
}
|
||||||
|
vpnUp.running = true
|
||||||
}
|
}
|
||||||
vpnUp.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect(uuidOrName) {
|
function disconnect(uuidOrName) {
|
||||||
@@ -192,4 +208,22 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user