From 10dc86a5dc48dc802574adabc94d254ad11068db Mon Sep 17 00:00:00 2001 From: bbedward Date: Sun, 14 Dec 2025 16:12:31 -0500 Subject: [PATCH] vpn: optim cc and dankbar widget --- .../BuiltinPlugins/VpnWidget.qml | 2 +- .../ControlCenter/Components/DetailHost.qml | 13 +- quickshell/Widgets/VpnDetailContent.qml | 366 +++--------------- quickshell/Widgets/VpnProfileDelegate.qml | 275 +++++++++++++ 4 files changed, 339 insertions(+), 317 deletions(-) create mode 100644 quickshell/Widgets/VpnProfileDelegate.qml diff --git a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml index 57850c40..b8d2a78f 100644 --- a/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml +++ b/quickshell/Modules/ControlCenter/BuiltinPlugins/VpnWidget.qml @@ -27,7 +27,7 @@ PluginComponent { ccDetailContent: Component { VpnDetailContent { - listHeight: 180 + listHeight: 260 } } } diff --git a/quickshell/Modules/ControlCenter/Components/DetailHost.qml b/quickshell/Modules/ControlCenter/Components/DetailHost.qml index 7ce6201d..7826b4e1 100644 --- a/quickshell/Modules/ControlCenter/Components/DetailHost.qml +++ b/quickshell/Modules/ControlCenter/Components/DetailHost.qml @@ -18,13 +18,16 @@ Item { function getDetailHeight(section) { const maxAvailable = parent ? parent.height - Theme.spacingS : 9999; - if (section === "wifi") + switch (true) { + case section === "wifi": + case section === "bluetooth": + case section === "builtin_vpn": return Math.min(350, maxAvailable); - if (section === "bluetooth") - return Math.min(350, maxAvailable); - if (section.startsWith("brightnessSlider_")) + case section.startsWith("brightnessSlider_"): return Math.min(400, maxAvailable); - return Math.min(250, maxAvailable); + default: + return Math.min(250, maxAvailable); + } } Loader { diff --git a/quickshell/Widgets/VpnDetailContent.qml b/quickshell/Widgets/VpnDetailContent.qml index b439bf1b..fa579a0b 100644 --- a/quickshell/Widgets/VpnDetailContent.qml +++ b/quickshell/Widgets/VpnDetailContent.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import Quickshell import qs.Common import qs.Modals.Common import qs.Modals.FileBrowser @@ -13,7 +14,7 @@ Rectangle { property string expandedUuid: "" property int listHeight: 180 - implicitHeight: contentColumn.implicitHeight + Theme.spacingM * 2 + implicitHeight: 32 + 1 + listHeight + Theme.spacingS * 4 + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) @@ -153,328 +154,71 @@ Rectangle { color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) } - DankFlickable { + Item { width: parent.width height: root.listHeight - contentHeight: listCol.height - clip: true Column { - id: listCol - width: parent.width - spacing: 4 + anchors.centerIn: parent + spacing: Theme.spacingS + visible: DMSNetworkService.profiles.length === 0 - Item { - width: parent.width - height: DMSNetworkService.profiles.length === 0 ? 100 : 0 - visible: height > 0 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: "vpn_key_off" - size: 36 - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("No VPN profiles") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("Click Import to add a .ovpn or .conf") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - } + DankIcon { + name: "vpn_key_off" + size: 36 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter } - Repeater { - model: DMSNetworkService.profiles + StyledText { + text: I18n.tr("No VPN profiles") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } - delegate: Rectangle { - id: profileRow - required property var modelData - required property int index + StyledText { + text: I18n.tr("Click Import to add a .ovpn or .conf") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } - readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid) - readonly property bool isExpanded: root.expandedUuid === modelData.uuid - readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse - readonly property var configData: isExpanded ? VPNService.editConfig : null + DankListView { + id: vpnListView + anchors.fill: parent + visible: DMSNetworkService.profiles.length > 0 + spacing: 4 + cacheBuffer: 200 + clip: true - width: listCol.width - height: isExpanded ? 46 + expandedContent.height : 46 - radius: Theme.cornerRadius - color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight) - border.width: isActive ? 2 : 1 - border.color: isActive ? Theme.primary : Theme.outlineLight - opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 - clip: true + model: ScriptModel { + values: DMSNetworkService.profiles + objectProp: "uuid" + } - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.OutQuad - } - } - - MouseArea { - id: rowArea - anchors.fill: parent - hoverEnabled: true - cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor - enabled: !DMSNetworkService.isBusy - onClicked: DMSNetworkService.toggle(modelData.uuid) - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingS - spacing: Theme.spacingS - - Row { - width: parent.width - height: 46 - Theme.spacingS * 2 - spacing: Theme.spacingS - - DankIcon { - name: isActive ? "vpn_lock" : "vpn_key_off" - size: 20 - color: isActive ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: 1 - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4 - - StyledText { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: isActive ? Theme.primary : Theme.surfaceText - elide: Text.ElideRight - wrapMode: Text.NoWrap - width: parent.width - } - - StyledText { - text: VPNService.getVpnTypeFromProfile(modelData) - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - wrapMode: Text.NoWrap - width: parent.width - elide: Text.ElideRight - } - } - - Item { - width: Theme.spacingXS - height: 1 - } - - Rectangle { - id: expandBtnRect - width: 28 - height: 28 - radius: 14 - color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent" - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - anchors.centerIn: parent - name: isExpanded ? "expand_less" : "expand_more" - size: 18 - color: Theme.surfaceText - } - - MouseArea { - id: expandBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (isExpanded) { - root.expandedUuid = ""; - } else { - root.expandedUuid = modelData.uuid; - VPNService.getConfig(modelData.uuid); - } - } - } - } - - Rectangle { - id: deleteBtnRect - width: 28 - height: 28 - radius: 14 - color: deleteBtn.containsMouse ? Theme.errorHover : "transparent" - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - anchors.centerIn: parent - name: "delete" - size: 18 - color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText - } - - MouseArea { - id: deleteBtn - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - deleteConfirm.showWithOptions({ - title: I18n.tr("Delete VPN"), - message: I18n.tr("Delete \"") + modelData.name + "\"?", - confirmText: I18n.tr("Delete"), - confirmColor: Theme.error, - onConfirm: () => VPNService.deleteVpn(modelData.uuid) - }); - } - } - } - } - - Column { - id: expandedContent - width: parent.width - spacing: Theme.spacingXS - visible: isExpanded - - Rectangle { - width: parent.width - height: 1 - color: Theme.outlineLight - } - - Item { - width: parent.width - height: VPNService.configLoading ? 40 : 0 - visible: VPNService.configLoading - - Row { - anchors.centerIn: parent - spacing: Theme.spacingS - - DankIcon { - name: "sync" - size: 16 - color: Theme.surfaceVariantText - } - - StyledText { - text: I18n.tr("Loading...") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - } - } - - Flow { - width: parent.width - spacing: Theme.spacingXS - visible: !VPNService.configLoading && configData - - Repeater { - model: { - if (!configData) - return []; - const fields = []; - const data = configData.data || {}; - - if (data.remote) - fields.push({ - label: I18n.tr("Server"), - value: data.remote - }); - if (configData.username || data.username) - fields.push({ - label: I18n.tr("Username"), - value: configData.username || data.username - }); - if (data.cipher) - fields.push({ - label: I18n.tr("Cipher"), - value: data.cipher - }); - if (data.auth) - fields.push({ - label: I18n.tr("Auth"), - value: data.auth - }); - if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no") - fields.push({ - label: I18n.tr("Protocol"), - value: data["proto-tcp"] === "yes" ? "TCP" : "UDP" - }); - if (data["tunnel-mtu"]) - fields.push({ - label: I18n.tr("MTU"), - value: data["tunnel-mtu"] - }); - if (data["connection-type"]) - fields.push({ - label: I18n.tr("Auth Type"), - value: data["connection-type"] - }); - fields.push({ - label: I18n.tr("Autoconnect"), - value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No") - }); - - return fields; - } - - delegate: Rectangle { - required property var modelData - required property int index - - width: fieldContent.width + Theme.spacingM * 2 - height: 32 - radius: Theme.cornerRadius - 2 - color: Theme.surfaceContainerHigh - border.width: 1 - border.color: Theme.outlineLight - - Row { - id: fieldContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: modelData.label + ":" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: modelData.value - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Item { - width: 1 - height: Theme.spacingXS - } - } + delegate: VpnProfileDelegate { + required property var modelData + width: vpnListView.width + profile: modelData + isExpanded: root.expandedUuid === modelData.uuid + onToggleExpand: { + if (root.expandedUuid === modelData.uuid) { + root.expandedUuid = ""; + return; } + root.expandedUuid = modelData.uuid; + VPNService.getConfig(modelData.uuid); + } + onDeleteRequested: { + deleteConfirm.showWithOptions({ + "title": I18n.tr("Delete VPN"), + "message": I18n.tr("Delete \"") + modelData.name + "\"?", + "confirmText": I18n.tr("Delete"), + "confirmColor": Theme.error, + "onConfirm": () => VPNService.deleteVpn(modelData.uuid) + }); } } } diff --git a/quickshell/Widgets/VpnProfileDelegate.qml b/quickshell/Widgets/VpnProfileDelegate.qml new file mode 100644 index 00000000..b1fa0099 --- /dev/null +++ b/quickshell/Widgets/VpnProfileDelegate.qml @@ -0,0 +1,275 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + required property var profile + property bool isExpanded: false + + signal toggleExpand + signal deleteRequested + + readonly property bool isActive: DMSNetworkService.activeUuids?.includes(profile?.uuid) ?? false + readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse + readonly property var configData: isExpanded ? VPNService.editConfig : null + readonly property var configFields: buildConfigFields() + + height: isExpanded ? 46 + expandedContent.height : 46 + radius: Theme.cornerRadius + color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight) + border.width: isActive ? 2 : 1 + border.color: isActive ? Theme.primary : Theme.outlineLight + opacity: DMSNetworkService.isBusy ? 0.5 : 1.0 + clip: true + + function buildConfigFields() { + if (!configData) + return []; + const fields = []; + const data = configData.data || {}; + if (data.remote) + fields.push({ + "key": "server", + "label": I18n.tr("Server"), + "value": data.remote + }); + if (configData.username || data.username) + fields.push({ + "key": "user", + "label": I18n.tr("Username"), + "value": configData.username || data.username + }); + if (data.cipher) + fields.push({ + "key": "cipher", + "label": I18n.tr("Cipher"), + "value": data.cipher + }); + if (data.auth) + fields.push({ + "key": "auth", + "label": I18n.tr("Auth"), + "value": data.auth + }); + if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no") + fields.push({ + "key": "proto", + "label": I18n.tr("Protocol"), + "value": data["proto-tcp"] === "yes" ? "TCP" : "UDP" + }); + if (data["tunnel-mtu"]) + fields.push({ + "key": "mtu", + "label": I18n.tr("MTU"), + "value": data["tunnel-mtu"] + }); + if (data["connection-type"]) + fields.push({ + "key": "conntype", + "label": I18n.tr("Auth Type"), + "value": data["connection-type"] + }); + fields.push({ + "key": "auto", + "label": I18n.tr("Autoconnect"), + "value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No") + }); + return fields; + } + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + MouseArea { + id: rowArea + anchors.fill: parent + hoverEnabled: true + cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor + enabled: !DMSNetworkService.isBusy + onClicked: DMSNetworkService.toggle(profile.uuid) + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + height: 46 - Theme.spacingS * 2 + spacing: Theme.spacingS + + DankIcon { + name: isActive ? "vpn_lock" : "vpn_key_off" + size: 20 + color: isActive ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 1 + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4 + + StyledText { + text: profile?.name ?? "" + font.pixelSize: Theme.fontSizeMedium + color: isActive ? Theme.primary : Theme.surfaceText + elide: Text.ElideRight + wrapMode: Text.NoWrap + width: parent.width + } + + StyledText { + text: VPNService.getVpnTypeFromProfile(profile) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + wrapMode: Text.NoWrap + width: parent.width + elide: Text.ElideRight + } + } + + Item { + width: Theme.spacingXS + height: 1 + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: expandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleExpand() + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: deleteBtn.containsMouse ? Theme.errorHover : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.deleteRequested() + } + } + } + + Column { + id: expandedContent + width: parent.width + spacing: Theme.spacingXS + visible: isExpanded + + Rectangle { + width: parent.width + height: 1 + color: Theme.outlineLight + } + + Item { + width: parent.width + height: VPNService.configLoading ? 40 : 0 + visible: VPNService.configLoading + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "sync" + size: 16 + color: Theme.surfaceVariantText + } + + StyledText { + text: I18n.tr("Loading...") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Flow { + width: parent.width + spacing: Theme.spacingXS + visible: !VPNService.configLoading && configData + + Repeater { + model: configFields + + delegate: Rectangle { + required property var modelData + + width: fieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: fieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Item { + width: 1 + height: Theme.spacingXS + } + } + } +}