diff --git a/Modules/TopBar/Vpn.qml b/Modules/TopBar/Vpn.qml index 392be87f..ce929484 100644 --- a/Modules/TopBar/Vpn.qml +++ b/Modules/TopBar/Vpn.qml @@ -77,7 +77,12 @@ Rectangle { Text { id: tooltipText anchors.centerIn: parent - text: VpnService.connected ? ("VPN Connected • " + (VpnService.activeName || "")) : "VPN Disconnected" + text: { + if (!VpnService.connected) return "VPN Disconnected" + const names = VpnService.activeNames || [] + if (names.length <= 1) return "VPN Connected • " + (names[0] || "") + return "VPN Connected • " + names[0] + " +" + (names.length - 1) + } font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText } diff --git a/Modules/TopBar/VpnPopout.qml b/Modules/TopBar/VpnPopout.qml index 0a596bc3..514359a6 100644 --- a/Modules/TopBar/VpnPopout.qml +++ b/Modules/TopBar/VpnPopout.qml @@ -162,32 +162,59 @@ DankPopout { Item { Layout.fillWidth: true; height: 1 } - // Quick connect when not connected - // Rectangle { - // height: 28 - // radius: 14 - // color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight - // visible: !VpnService.connected && VpnService.profiles.length > 0 - // width: 120 - // Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - // border.width: 1 - // border.color: Theme.outlineLight - // - // Row { - // anchors.centerIn: parent - // spacing: Theme.spacingXS - // DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } - // StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } - // } - // - // MouseArea { - // id: quickBtnArea - // anchors.fill: parent - // hoverEnabled: true - // cursorShape: Qt.PointingHandCursor - // onClicked: VpnService.toggle() - // } - // } + // Quick connect + Rectangle { + height: 28 + radius: 14 + color: quickBtnArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + visible: VpnService.profiles.length > 0 + width: 120 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: quickBtnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.toggle() + } + } + + // Disconnect all (visible when any active) + Rectangle { + height: 28 + radius: 14 + color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + visible: VpnService.connected + width: 130 + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + DankIcon { name: "link_off"; size: Theme.fontSizeSmall; color: Theme.surfaceText } + StyledText { text: "Disconnect All"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium } + } + + MouseArea { + id: discAllArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: VpnService.disconnectAllActive() + } + } } Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } @@ -255,13 +282,7 @@ DankPopout { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.uuid === VpnService.activeUuid) { - VpnService.disconnect(modelData.uuid) - } else { - VpnService.connect(modelData.uuid) - } - } + onClicked: VpnService.toggle(modelData.uuid) } } } diff --git a/Services/VpnService.qml b/Services/VpnService.qml index 7204c573..f06c6b56 100644 --- a/Services/VpnService.qml +++ b/Services/VpnService.qml @@ -18,8 +18,9 @@ Singleton { // [{ name, uuid, type }] property var profiles: [] - // Enforce single active VPN at a time - property bool singleActive: true + // 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 @@ -160,11 +161,12 @@ Singleton { } function toggle(uuid) { - if (root.activeUuid && (uuid === undefined || uuid === root.activeUuid)) { - disconnect(root.activeUuid) - } else if (uuid) { - connect(uuid) - } else if (root.profiles.length > 0) { + if (uuid) { + if (isActiveUuid(uuid)) disconnect(uuid) + else connect(uuid) + return + } + if (root.profiles.length > 0) { connect(root.profiles[0].uuid) } } @@ -209,6 +211,14 @@ Singleton { } } + 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