diff options
Diffstat (limited to 'modules/controlcenter/network/VpnList.qml')
| -rw-r--r-- | modules/controlcenter/network/VpnList.qml | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml new file mode 100644 index 0000000..665f8cc --- /dev/null +++ b/modules/controlcenter/network/VpnList.qml @@ -0,0 +1,646 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.components.effects +import qs.services +import qs.config +import qs.utils +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +ColumnLayout { + id: root + + required property Session session + property bool showHeader: true + property int pendingSwitchIndex: -1 + + spacing: Appearance.spacing.normal + + Connections { + target: VPN + function onConnectedChanged() { + if (!VPN.connected && root.pendingSwitchIndex >= 0) { + const targetIndex = root.pendingSwitchIndex; + root.pendingSwitchIndex = -1; + + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + const p = Config.utilities.vpn.provider[i]; + if (typeof p === "object") { + const newProvider = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: (i === targetIndex) + }; + providers.push(newProvider); + } else { + providers.push(p); + } + } + Config.utilities.vpn.provider = providers; + Config.save(); + + Qt.callLater(function() { + VPN.toggle(); + }); + } + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("+ Add VPN Provider") + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + vpnDialog.showProviderSelection(); + } + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + + interactive: false + spacing: Appearance.spacing.smaller + + model: ScriptModel { + values: Config.utilities.vpn.provider.map((provider, index) => { + const isObject = typeof provider === "object"; + const name = isObject ? (provider.name || "custom") : String(provider); + const displayName = isObject ? (provider.displayName || name) : name; + const iface = isObject ? (provider.interface || "") : ""; + const enabled = isObject ? (provider.enabled === true) : false; + + return { + index: index, + name: name, + displayName: displayName, + interface: iface, + provider: provider, + enabled: enabled + }; + }) + } + + delegate: Component { + StyledRect { + required property var modelData + required property int index + + width: ListView.view ? ListView.view.width : undefined + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal + + StateLayer { + function onClicked(): void { + if (root.session && root.session.vpn) { + root.session.vpn.active = modelData; + } + } + } + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" + font.pointSize: Appearance.font.size.large + fill: modelData.enabled && VPN.connected ? 1 : 0 + color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + + text: modelData.displayName || qsTr("Unknown") + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: { + if (modelData.enabled && VPN.connected) return qsTr("Connected"); + if (modelData.enabled && VPN.connecting) return qsTr("Connecting..."); + if (modelData.enabled) return qsTr("Enabled"); + return qsTr("Disabled"); + } + color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.enabled && VPN.connected ? 500 : 400 + elide: Text.ElideRight + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) + + StateLayer { + enabled: !VPN.connecting + function onClicked(): void { + const clickedIndex = modelData.index; + + if (modelData.enabled) { + VPN.toggle(); + } else { + if (VPN.connected) { + root.pendingSwitchIndex = clickedIndex; + VPN.toggle(); + } else { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + const p = Config.utilities.vpn.provider[i]; + if (typeof p === "object") { + const newProvider = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: (i === clickedIndex) + }; + providers.push(newProvider); + } else { + providers.push(p); + } + } + Config.utilities.vpn.provider = providers; + Config.save(); + + Qt.callLater(function() { + VPN.toggle(); + }); + } + } + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + text: VPN.connected && modelData.enabled ? "link_off" : "link" + color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.full + color: "transparent" + + StateLayer { + function onClicked(): void { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + if (i !== modelData.index) { + providers.push(Config.utilities.vpn.provider[i]); + } + } + Config.utilities.vpn.provider = providers; + Config.save(); + } + } + + MaterialIcon { + id: deleteIcon + + anchors.centerIn: parent + text: "delete" + color: Colours.palette.m3onSurface + } + } + } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } + + Popup { + id: vpnDialog + + property string currentState: "selection" + property int editIndex: -1 + property string providerName: "" + property string displayName: "" + property string interfaceName: "" + + parent: Overlay.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) + padding: Appearance.padding.large * 1.5 + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + opacity: 0 + scale: 0.7 + + enter: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Appearance.anim.durations.normal; easing.bezierCurve: Appearance.anim.curves.emphasized } + NumberAnimation { property: "scale"; from: 0.7; to: 1; duration: Appearance.anim.durations.normal; easing.bezierCurve: Appearance.anim.curves.emphasized } + } + } + + exit: Transition { + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Appearance.anim.durations.small; easing.bezierCurve: Appearance.anim.curves.emphasized } + NumberAnimation { property: "scale"; from: 1; to: 0.7; duration: Appearance.anim.durations.small; easing.bezierCurve: Appearance.anim.curves.emphasized } + } + } + + function showProviderSelection(): void { + currentState = "selection"; + open(); + } + + function closeWithAnimation(): void { + close(); + } + + function showAddForm(providerType: string, defaultDisplayName: string): void { + editIndex = -1; + providerName = providerType; + displayName = defaultDisplayName; + interfaceName = ""; + + if (currentState === "selection") { + transitionToForm.start(); + } else { + currentState = "form"; + isClosing = false; + open(); + } + } + + function showEditForm(index: int): void { + const provider = Config.utilities.vpn.provider[index]; + const isObject = typeof provider === "object"; + + editIndex = index; + providerName = isObject ? (provider.name || "custom") : String(provider); + displayName = isObject ? (provider.displayName || providerName) : providerName; + interfaceName = isObject ? (provider.interface || "") : ""; + + currentState = "form"; + open(); + } + + Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) + } + + onClosed: { + currentState = "selection"; + } + + SequentialAnimation { + id: transitionToForm + + ParallelAnimation { + NumberAnimation { + target: selectionContent + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + ScriptAction { + script: { + vpnDialog.currentState = "form"; + } + } + + ParallelAnimation { + NumberAnimation { + target: formContent + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + + background: StyledRect { + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.large + + layer.enabled: true + layer.effect: DropShadow { + color: Qt.rgba(0, 0, 0, 0.3) + radius: 16 + samples: 33 + verticalOffset: 4 + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + + contentItem: Item { + implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight + + Behavior on implicitHeight { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + ColumnLayout { + id: selectionContent + + anchors.fill: parent + spacing: Appearance.spacing.normal + visible: vpnDialog.currentState === "selection" + opacity: vpnDialog.currentState === "selection" ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + StyledText { + text: qsTr("Add VPN Provider") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Choose a provider to add") + wrapMode: Text.WordWrap + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + Item { Layout.preferredHeight: Appearance.spacing.small } + + TextButton { + Layout.fillWidth: true + text: qsTr("NetBird") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + providers.push(Config.utilities.vpn.provider[i]); + } + providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" }); + Config.utilities.vpn.provider = providers; + Config.save(); + vpnDialog.closeWithAnimation(); + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("Tailscale") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + providers.push(Config.utilities.vpn.provider[i]); + } + providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" }); + Config.utilities.vpn.provider = providers; + Config.save(); + vpnDialog.closeWithAnimation(); + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("Cloudflare WARP") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + providers.push(Config.utilities.vpn.provider[i]); + } + providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); + Config.utilities.vpn.provider = providers; + Config.save(); + vpnDialog.closeWithAnimation(); + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("WireGuard (Custom)") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + vpnDialog.showAddForm("wireguard", "WireGuard"); + } + } + + Item { Layout.preferredHeight: Appearance.spacing.small } + + TextButton { + Layout.fillWidth: true + text: qsTr("Cancel") + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + onClicked: vpnDialog.closeWithAnimation() + } + } + + ColumnLayout { + id: formContent + + anchors.fill: parent + spacing: Appearance.spacing.normal + visible: vpnDialog.currentState === "form" + opacity: vpnDialog.currentState === "form" ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + StyledText { + text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + + StyledText { + text: qsTr("Display Name") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + StyledTextField { + id: displayNameField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.displayName + onTextChanged: vpnDialog.displayName = text + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + + StyledText { + text: qsTr("Interface (e.g., wg0, torguard)") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { CAnim {} } + Behavior on border.color { CAnim {} } + + StyledTextField { + id: interfaceNameField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.interfaceName + onTextChanged: vpnDialog.interfaceName = text + } + } + } + + Item { Layout.preferredHeight: Appearance.spacing.normal } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + Layout.fillWidth: true + text: qsTr("Cancel") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: vpnDialog.closeWithAnimation() + } + + TextButton { + Layout.fillWidth: true + text: qsTr("Save") + enabled: vpnDialog.interfaceName.length > 0 + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + const providers = []; + const newProvider = { + name: vpnDialog.providerName, + displayName: vpnDialog.displayName || vpnDialog.interfaceName, + interface: vpnDialog.interfaceName + }; + + if (vpnDialog.editIndex >= 0) { + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + if (i === vpnDialog.editIndex) { + providers.push(newProvider); + } else { + providers.push(Config.utilities.vpn.provider[i]); + } + } + } else { + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + providers.push(Config.utilities.vpn.provider[i]); + } + providers.push(newProvider); + } + + Config.utilities.vpn.provider = providers; + Config.save(); + vpnDialog.closeWithAnimation(); + } + } + } + } + } + } +} |