1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Add NetworkManager VPN integration: VpnService + Control Center detail; move to TopBar VPN widget with popout; fix logs and parsing; default top bar item 'vpn' added; minor layout fixes

This commit is contained in:
Jon Rogers
2025-08-30 13:57:47 -04:00
parent e55c97185a
commit 952e5604d9
8 changed files with 595 additions and 1 deletions

View File

@@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
implicitHeight: 220
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: VpnService.connected ? ("Active: " + (VpnService.activeName || "VPN")) : "Active: None"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Item { width: 10; height: 1 }
Rectangle {
height: 28
radius: 14
color: toggleMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
visible: VpnService.connected || VpnService.profiles.length > 0
width: 80
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon { name: VpnService.connected ? "link_off" : "link"; size: Theme.fontSizeSmall; color: Theme.surfaceText }
StyledText { text: VpnService.connected ? "Disconnect" : "Connect"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; font.weight: Font.Medium }
}
MouseArea {
id: toggleMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: VpnService.toggle()
}
}
}
Rectangle { height: 1; width: parent.width; color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) }
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: VpnService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon { name: "playlist_remove"; size: 36; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter }
StyledText { text: "No VPN profiles found"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter }
StyledText { text: "Add a VPN in NetworkManager"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText; anchors.horizontalCenter: parent.horizontalCenter }
}
}
Repeater {
model: VpnService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 40
radius: Theme.cornerRadius
color: rowMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
border.width: 1
border.color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: modelData.uuid === VpnService.activeUuid ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: modelData.uuid === VpnService.activeUuid ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Rectangle {
height: 28
radius: 14
color: actMouse.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
width: 100
StyledText {
anchors.centerIn: parent
text: modelData.uuid === VpnService.activeUuid ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: modelData.uuid === VpnService.activeUuid ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: actMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.uuid === VpnService.activeUuid) {
VpnService.disconnect(modelData.uuid)
} else {
VpnService.connect(modelData.uuid)
}
}
}
}
}
MouseArea {
id: rowMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
}
}
Item { height: 1; width: 1 }
}
}
}
}

View File

@@ -114,6 +114,12 @@ Item {
"description": "Battery level and power management",
"icon": "battery_std",
"enabled": true
}, {
"id": "vpn",
"text": "VPN",
"description": "VPN status and quick connect",
"icon": "vpn_lock",
"enabled": true
}, {
"id": "idleInhibitor",
"text": "Idle Inhibitor",

View File

@@ -339,6 +339,8 @@ PanelWindow {
return DgopService.dgopAvailable
case "keyboard_layout_name":
return true
case "vpn":
return true
default:
return false
}
@@ -390,6 +392,8 @@ PanelWindow {
return networkComponent
case "keyboard_layout_name":
return keyboardLayoutNameComponent
case "vpn":
return vpnComponent
default:
return null
}
@@ -1022,6 +1026,35 @@ PanelWindow {
}
}
Component {
id: vpnComponent
Vpn {
widgetHeight: root.widgetHeight
barHeight: root.effectiveBarHeight
section: {
if (parent && parent.parent === leftSection)
return "left"
if (parent && parent.parent === rightSection)
return "right"
if (parent && parent.parent === centerSection)
return "center"
return "right"
}
popupTarget: {
vpnPopoutLoader.active = true
return vpnPopoutLoader.item
}
parentScreen: root.screen
onToggleVpnPopup: {
vpnPopoutLoader.active = true
if (vpnPopoutLoader.item) {
vpnPopoutLoader.item.toggle()
}
}
}
}
Component {
id: controlCenterButtonComponent

106
Modules/TopBar/Vpn.qml Normal file
View File

@@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
// Passed in by TopBar
property int widgetHeight: 28
property int barHeight: 32
property string section: "right"
property var popupTarget: null
property var parentScreen: null
signal toggleVpnPopup()
width: Math.max(24, contentRow.implicitWidth + Theme.spacingXS * 2)
height: widgetHeight
Row {
id: contentRow
anchors.fill: parent
anchors.margins: 0
spacing: Theme.spacingXS
Rectangle {
anchors.fill: parent
radius: 6
color: "transparent"
}
DankIcon {
id: icon
name: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
size: Theme.iconSize - 6
color: VpnService.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: VpnService.isBusy
loops: Animation.Infinite
from: 0
to: 360
duration: 900
}
}
Text {
id: label
text: VpnService.connected ? (VpnService.activeName || "VPN") : "VPN"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: VpnService.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: true
elide: Text.ElideRight
}
}
MouseArea {
id: clickArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX, barHeight + Theme.spacingXS, width, section, currentScreen)
}
root.toggleVpnPopup()
}
}
Rectangle {
id: tooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.surfaceVariantAlpha
border.width: 1
visible: clickArea.containsMouse && !(popupTarget && popupTarget.shouldBeVisible)
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: clickArea.containsMouse ? 1 : 0
Text {
id: tooltipText
anchors.centerIn: parent
text: VpnService.connected ? ("VPN Connected • " + (VpnService.activeName || "")) : "VPN Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing }
}
}
}

View File

@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Widgets
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Details 1.0 as Details
DankPopout {
id: root
property string triggerSection: "right"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 420
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 260
triggerX: Screen.width - 400 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingS
triggerWidth: 70
positioning: "center"
WlrLayershell.namespace: "quickshell-vpn"
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
content: Component {
Rectangle {
id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 1
antialiasing: true
smooth: true
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Item {
width: parent.width
height: 28
StyledText {
text: "VPN"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Details.VpnDetail {
width: parent.width
}
}
}
}
}