pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Internal import ZShell.Blobs import ZShell.Services import ZShell.Components import qs.Components import qs.Modules.Settings.Common PageBase { id: root property bool connecting: false property bool failed: false // Security model: index 0 = none, 1 = WPA/WPA2/WPA3 personal. readonly property bool secured: securitySelect.active !== noneItem property bool success: false function submit(): void { const ssid = ssidField.text.trim(); if (ssid.length === 0) { ssidField.isError = true; ssidField.forceActiveFocus(); return; } if (root.secured && passwordField.text.length < 8) { passwordField.isError = true; passwordField.forceActiveFocus(); return; } root.failed = false; root.connecting = true; Nmcli.addHiddenNetwork(ssid, root.secured ? passwordField.text : "", root.secured ? "wpa" : "none", hiddenToggle.checked, result => { root.connecting = false; if (result && result.success) { root.success = true; root.nState.closeSubPage(); } else { root.failed = true; if (root.secured) passwordField.isError = true; // Clean up the half-created profile so a retry starts fresh. Nmcli.forgetNetwork(ssid); } }); } isSubPage: true title: Tr.tr("Add network") ColumnLayout { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top spacing: Tokens.spacing.large width: root.cappedWidth Connections { function onSubPageClosed(): void { if (root.success) return; const ssid = ssidField.text.trim(); if (ssid) Nmcli.forgetNetwork(ssid); } target: root.nState } CustomText { Layout.fillWidth: true Layout.leftMargin: Tokens.padding.extraSmall color: Colours.palette.m3onSurfaceVariant font: Tokens.font.body.small text: Tr.tr("Enter the details below to manually connect to a network.") wrapMode: Text.WordWrap } CustomTextField { id: ssidField Layout.fillWidth: true Layout.topMargin: Tokens.spacing.extraSmall errorText: Tr.tr("Network name is required") inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText leadingIcon: "wifi" // TRANSLATORS: SSID is a protocol term, leave it untranslated placeholderText: Tr.tr("Network name (SSID)") // TRANSLATORS: sample network name, translate the e.g. but the name is a placeholder supportingText: Tr.tr("e.g. MyHiddenNetwork") onAccepted: root.secured ? passwordField.forceActiveFocus() : root.submit() } ToggleRow { id: hiddenToggle checked: true first: true subtext: Tr.tr("Actively probe for a network that doesn't broadcast its name") text: Tr.tr("Hidden network") } SelectRow { id: securitySelect Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing fallbackIcon: "lock" // TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated fallbackText: Tr.tr("WPA/WPA2/WPA3 Personal") // label: Tr.tr("Security") last: !root.secured Behavior on bottomLeftRadius { Anim { type: Anim.DefaultEffects } } Behavior on bottomRightRadius { Anim { type: Anim.DefaultEffects } } menuItems: [ MenuItem { icon: "lock" // TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated text: Tr.tr("WPA/WPA2/WPA3 Personal") }, MenuItem { id: noneItem icon: "lock_open" text: Tr.trCtx("None (open)", "wifi security type") } ] } Item { Layout.bottomMargin: root.secured ? 0 : -parent.spacing Layout.fillWidth: true implicitHeight: root.secured ? passwordField.implicitHeight : 0 opacity: root.secured ? 1 : 0 Behavior on Layout.bottomMargin { Anim { type: Anim.DefaultEffects } } Behavior on implicitHeight { Anim { type: Anim.DefaultEffects } } Behavior on opacity { Anim { type: Anim.DefaultEffects } } CustomTextField { id: passwordField anchors.left: parent.left anchors.right: parent.right echoMode: TextInput.Password enabled: root.secured errorText: root.failed ? Tr.tr("Connection failed — check the password") : Tr.tr("Password must be at least 8 characters") leadingIcon: "key" placeholderText: Tr.tr("Password") // TRANSLATORS: WPA is a protocol name, leave it untranslated supportingText: Tr.tr("WPA passwords are at least 8 characters") onAccepted: root.submit() } } RowLayout { Layout.alignment: Qt.AlignRight Layout.topMargin: Tokens.spacing.extraSmall - parent.spacing spacing: Tokens.spacing.small TextButton { Layout.fillHeight: true horizontalPadding: Tokens.padding.extraLarge isRound: true text: Tr.trCtx("Cancel", "button") type: TextButton.Tonal onClicked: root.nState.closeSubPage() } // Connect button — swaps to a loading spinner while connecting, // matching the Wi-Fi list connect animation. ButtonBase { id: connectBtn implicitHeight: connectMetrics.height + Tokens.padding.medium * 2 implicitWidth: connectMetrics.width + Tokens.padding.extraLarge * 2 // inactiveColour: Colours.palette.m3primary // inactiveOnColour: Colours.palette.m3onPrimary isRound: true shapeMorph: true // stateLayer.disabled: root.connecting || ssidField.text.trim().length === 0 onClicked: { if (!root.connecting && ssidField.text.trim().length > 0) root.submit(); } TextMetrics { id: connectMetrics font: connectBtn.font text: Tr.tr("Connect") } Anim { id: connectContent // anchors.centerIn: parent // inAnimType: Anim.SlowEffects // outAnimType: Anim.SlowEffects // sourceComp: root.connecting ? connectLoadingComp : connectTextComp } Component { id: connectLoadingComp LoadingIndicator { color: connectBtn.onColour implicitSize: Math.round(Tokens.font.body.medium.pointSize * 1.4) } } Component { id: connectTextComp CustomText { color: connectBtn.onColour font: connectBtn.font text: connectMetrics.text } } } } } }