pragma ComponentBehavior: Bound import ".." import "../components" import QtQuick import QtQuick.Controls import QtQuick.Layouts import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services import qs.utils DeviceDetails { id: root required property Session session readonly property var vpnProvider: root.session.vpn.active readonly property bool providerEnabled: { if (!vpnProvider || vpnProvider.index === undefined) return false; const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; return provider && typeof provider === "object" && provider.enabled === true; } device: vpnProvider headerComponent: Component { ConnectionHeader { icon: "vpn_key" title: root.vpnProvider?.displayName ?? qsTr("Unknown") } } sections: [ Component { ColumnLayout { spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection status") description: qsTr("VPN connection settings") } SectionContainer { ToggleRow { label: qsTr("Enable this provider") checked: root.providerEnabled toggle.onToggled: { if (!root.vpnProvider) return; const providers = []; const index = root.vpnProvider.index; // Copy providers and update enabled state for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, displayName: p.displayName, interface: p.interface }; if (checked) { // Enable this one, disable others newProvider.enabled = (i === index); } else { // Just disable this one newProvider.enabled = (i === index) ? false : (p.enabled !== false); } if (p.connectCmd && p.connectCmd.length > 0) { newProvider.connectCmd = p.connectCmd; } if (p.disconnectCmd && p.disconnectCmd.length > 0) { newProvider.disconnectCmd = p.disconnectCmd; } providers.push(newProvider); } else { providers.push(p); } } GlobalConfig.utilities.vpn.provider = providers; } } RowLayout { Layout.fillWidth: true Layout.topMargin: Tokens.spacing.medium spacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 visible: root.providerEnabled enabled: !VPN.connecting inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect") onClicked: { VPN.toggle(); } } TextButton { Layout.fillWidth: true text: qsTr("Edit Provider") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.displayName = root.vpnProvider.displayName; editVpnDialog.interfaceName = root.vpnProvider.interface; editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; editVpnDialog.open(); } } TextButton { Layout.fillWidth: true text: qsTr("Delete Provider") inactiveColour: Colours.palette.m3errorContainer inactiveOnColour: Colours.palette.m3onErrorContainer onClicked: { const providers = []; for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== root.vpnProvider.index) { providers.push(GlobalConfig.utilities.vpn.provider[i]); } } GlobalConfig.utilities.vpn.provider = providers; root.session.vpn.active = null; } } } TextButton { Layout.fillWidth: true Layout.topMargin: Tokens.spacing.medium visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" text: qsTr("Open Login Page") inactiveColour: Colours.palette.m3tertiaryContainer inactiveOnColour: Colours.palette.m3onTertiaryContainer onClicked: { Qt.openUrlExternally(VPN.status.authUrl); } } StyledText { Layout.fillWidth: true Layout.topMargin: Tokens.spacing.medium visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" text: qsTr("Click 'Connect' to generate authentication URL") font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap } } } }, Component { ColumnLayout { spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Provider details") description: qsTr("VPN provider information") } SectionContainer { contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Provider") value: root.vpnProvider?.name ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Display name") value: root.vpnProvider?.displayName ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Interface") value: root.vpnProvider?.interface || qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Status") value: { if (!root.providerEnabled) return qsTr("Disabled"); if (VPN.connecting) return qsTr("Connecting..."); switch (VPN.status.state) { case "connected": return qsTr("Connected"); case "disconnected": return qsTr("Disconnected"); case "connecting": return qsTr("Connecting..."); case "needs-auth": return qsTr("Authentication required"); case "error": return qsTr("Error"); default: return qsTr("Unknown"); } } } PropertyRow { visible: VPN.status.reason !== "" showTopMargin: true label: qsTr("Details") value: VPN.status.reason } PropertyRow { showTopMargin: true label: qsTr("Enabled") value: root.providerEnabled ? qsTr("Yes") : qsTr("No") } } } } ] // Edit VPN Dialog Popup { id: editVpnDialog property int editIndex: -1 property string providerName: "" property string displayName: "" property string interfaceName: "" property string connectCmd: "" property string disconnectCmd: "" function closeWithAnimation(): void { close(); } parent: Overlay.overlay anchors.centerIn: parent width: Math.min(400, parent.width - Tokens.padding.extraLargeIncreased) padding: Tokens.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside opacity: 0 scale: 0.7 enter: Transition { Anim { property: "opacity" from: 0 to: 1 type: Anim.FastSpatial } Anim { property: "scale" from: 0.7 to: 1 type: Anim.FastSpatial } } exit: Transition { Anim { property: "opacity" from: 1 to: 0 type: Anim.FastSpatial } Anim { property: "scale" from: 1 to: 0.7 type: Anim.FastSpatial } } Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Tokens.rounding.extraLarge Elevation { anchors.fill: parent radius: parent.radius level: 3 z: -1 } } contentItem: ColumnLayout { spacing: Tokens.spacing.medium StyledText { text: qsTr("Edit VPN Provider") font: Tokens.font.body.builders.large.weight(Font.Medium).build() } ColumnLayout { Layout.fillWidth: true spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Display Name") font: Tokens.font.body.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: Tokens.rounding.medium 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 - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.displayName onTextChanged: editVpnDialog.displayName = text } } } ColumnLayout { Layout.fillWidth: true spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") font: Tokens.font.body.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: Tokens.rounding.medium 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 - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.interfaceName onTextChanged: editVpnDialog.interfaceName = text } } } ColumnLayout { Layout.fillWidth: true spacing: Tokens.spacing.medium / 2 visible: editVpnDialog.connectCmd.length > 0 StyledText { text: qsTr("Connect Command") font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Tokens.rounding.medium border.width: 1 border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: connectCmdFieldEdit anchors.centerIn: parent width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.connectCmd onTextChanged: editVpnDialog.connectCmd = text } } } ColumnLayout { Layout.fillWidth: true spacing: Tokens.spacing.medium / 2 visible: editVpnDialog.disconnectCmd.length > 0 StyledText { text: qsTr("Disconnect Command") font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Tokens.rounding.medium border.width: 1 border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: disconnectCmdFieldEdit anchors.centerIn: parent width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.disconnectCmd onTextChanged: editVpnDialog.disconnectCmd = text } } } RowLayout { Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true spacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: editVpnDialog.closeWithAnimation() } TextButton { Layout.fillWidth: true text: qsTr("Save") enabled: editVpnDialog.interfaceName.length > 0 inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { const providers = []; const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i === editVpnDialog.editIndex) { const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; const newProvider = { displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, enabled: wasEnabled, interface: editVpnDialog.interfaceName, name: editVpnDialog.providerName }; if (hasCommands) { newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); } providers.push(newProvider); } else { const p = GlobalConfig.utilities.vpn.provider[i]; const reconstructed = { displayName: p.displayName, enabled: p.enabled, interface: p.interface, name: p.name }; if (p.connectCmd && p.connectCmd.length > 0) { reconstructed.connectCmd = p.connectCmd; } if (p.disconnectCmd && p.disconnectCmd.length > 0) { reconstructed.disconnectCmd = p.disconnectCmd; } providers.push(reconstructed); } } GlobalConfig.utilities.vpn.provider = providers; editVpnDialog.closeWithAnimation(); } } } } } }