controlcenter: wifi passphrase input

fix: dependability issues with nmcli
This commit is contained in:
ATMDA 2025-11-09 21:43:51 -05:00
parent 81a8157b67
commit d8de7c7e4b
5 changed files with 451 additions and 13 deletions

View file

@ -79,17 +79,37 @@ Item {
checked: root.network?.active ?? false
toggle.onToggled: {
if (checked) {
if (root.network.isSecure) {
// TODO: Show password dialog
root.session.network.showPasswordDialog = true;
root.session.network.pendingNetwork = root.network;
// If already connected to a different network, disconnect first
if (Network.active && Network.active.ssid !== root.network.ssid) {
Network.disconnectFromNetwork();
// Wait a moment before connecting to new network
Qt.callLater(() => {
connectToNetwork();
});
} else {
Network.connectToNetwork(root.network.ssid, "");
connectToNetwork();
}
} else {
Network.disconnectFromNetwork();
}
}
function connectToNetwork(): void {
if (root.network.isSecure) {
// Try connecting without password first (in case it's saved)
Network.connectToNetworkWithPasswordCheck(
root.network.ssid,
root.network.isSecure,
() => {
// Callback: connection failed, show password dialog
root.session.network.showPasswordDialog = true;
root.session.network.pendingNetwork = root.network;
}
);
} else {
Network.connectToNetwork(root.network.ssid, "");
}
}
}
}
}

View file

@ -214,15 +214,36 @@ ColumnLayout {
if (modelData.active) {
Network.disconnectFromNetwork();
} else {
// If already connected to a different network, disconnect first
if (Network.active && Network.active.ssid !== modelData.ssid) {
Network.disconnectFromNetwork();
// Wait a moment before connecting to new network
Qt.callLater(() => {
connectToNetwork();
});
} else {
connectToNetwork();
}
}
}
function connectToNetwork(): void {
if (modelData.isSecure) {
// Try connecting without password first (in case it's saved)
Network.connectToNetworkWithPasswordCheck(
modelData.ssid,
modelData.isSecure,
() => {
// Callback: connection failed, show password dialog
root.session.network.showPasswordDialog = true;
root.session.network.pendingNetwork = modelData;
}
);
} else {
Network.connectToNetwork(modelData.ssid, "");
}
}
}
}
MaterialIcon {
id: connectIcon

View file

@ -76,4 +76,10 @@ RowLayout {
}
}
}
PasswordDialog {
anchors.fill: parent
session: root.session
z: 1000
}
}

View file

@ -0,0 +1,241 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.controls
import qs.components.effects
import qs.components.containers
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Session session
readonly property var network: session.network.pendingNetwork
visible: session.network.showPasswordDialog
enabled: visible
focus: visible
Keys.onEscapePressed: {
root.session.network.showPasswordDialog = false;
passwordField.text = "";
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: root.visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
MouseArea {
anchors.fill: parent
onClicked: {
root.session.network.showPasswordDialog = false;
passwordField.text = "";
}
}
}
StyledRect {
id: dialog
anchors.centerIn: parent
implicitWidth: 400
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surface
opacity: root.visible ? 1 : 0
scale: root.visible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
Behavior on scale {
NumberAnimation {
duration: 200
}
}
Keys.onEscapePressed: {
root.session.network.showPasswordDialog = false;
passwordField.text = "";
}
ColumnLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "lock"
font.pointSize: Appearance.font.size.extraLarge * 2
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Enter password")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : ""
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
Item {
Layout.topMargin: Appearance.spacing.large
Layout.fillWidth: true
implicitHeight: passwordField.implicitHeight + Appearance.padding.normal * 2
StyledRect {
anchors.fill: parent
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
border.width: passwordField.activeFocus ? 2 : 1
border.color: passwordField.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline
Behavior on border.color {
CAnim {}
}
}
StyledTextField {
id: passwordField
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
echoMode: TextField.Password
placeholderText: qsTr("Password")
Component.onCompleted: {
if (root.visible) {
forceActiveFocus();
}
}
Connections {
target: root
function onVisibleChanged(): void {
if (root.visible) {
passwordField.forceActiveFocus();
passwordField.text = "";
}
}
}
Keys.onReturnPressed: {
if (connectButton.enabled) {
connectButton.clicked();
}
}
Keys.onEnterPressed: {
if (connectButton.enabled) {
connectButton.clicked();
}
}
}
}
RowLayout {
Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true
spacing: Appearance.spacing.normal
Button {
id: cancelButton
Layout.fillWidth: true
color: Colours.palette.m3secondaryContainer
onColor: Colours.palette.m3onSecondaryContainer
text: qsTr("Cancel")
function onClicked(): void {
root.session.network.showPasswordDialog = false;
passwordField.text = "";
}
}
Button {
id: connectButton
Layout.fillWidth: true
color: Colours.palette.m3primary
onColor: Colours.palette.m3onPrimary
text: qsTr("Connect")
enabled: passwordField.text.length > 0
function onClicked(): void {
if (root.network && passwordField.text.length > 0) {
Network.connectToNetwork(root.network.ssid, passwordField.text);
root.session.network.showPasswordDialog = false;
passwordField.text = "";
}
}
}
}
}
}
component Button: StyledRect {
property color onColor: Colours.palette.m3onSurface
property alias disabled: stateLayer.disabled
property alias text: label.text
property alias enabled: stateLayer.enabled
function onClicked(): void {
}
radius: Appearance.rounding.small
implicitHeight: label.implicitHeight + Appearance.padding.small * 2
opacity: enabled ? 1 : 0.5
StateLayer {
id: stateLayer
enabled: parent.enabled
color: parent.onColor
function onClicked(): void {
if (enabled) {
parent.onClicked();
}
}
}
StyledText {
id: label
anchors.centerIn: parent
animate: true
color: parent.onColor
font.pointSize: Appearance.font.size.normal
}
}
}

View file

@ -26,20 +26,42 @@ Singleton {
rescanProc.running = true;
}
property var pendingConnection: null
signal connectionFailed(string ssid)
function connectToNetwork(ssid: string, password: string): void {
// First try to connect to an existing connection
// If that fails, create a new connection
if (password && password.length > 0) {
connectProc.exec(["nmcli", "device", "wifi", "connect", ssid, "password", password]);
} else {
// Try to connect to existing connection first
// Try to connect to existing connection first (will use saved password if available)
connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]);
}
}
function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var): void {
// For secure networks, try connecting without password first
// If connection succeeds (saved password exists), we're done
// If it fails with password error, callback will be called to show password dialog
if (isSecure) {
root.pendingConnection = { ssid: ssid, callback: callback };
// Try connecting without password - will use saved password if available
connectProc.exec(["nmcli", "device", "wifi", "connect", ssid]);
// Start timer to check if connection succeeded
connectionCheckTimer.start();
} else {
connectToNetwork(ssid, "");
}
}
function disconnectFromNetwork(): void {
if (active) {
// Find the device name first, then disconnect
// Try to disconnect - use connection name if available, otherwise use device
if (active && active.ssid) {
// First try to disconnect by connection name (more reliable)
disconnectByConnectionProc.exec(["nmcli", "connection", "down", active.ssid]);
} else {
// Fallback: disconnect by device
disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]);
}
}
@ -90,18 +112,102 @@ Singleton {
}
}
Timer {
id: connectionCheckTimer
interval: 4000
onTriggered: {
if (root.pendingConnection) {
// Final check - if connection still hasn't succeeded, show password dialog
const connected = root.active && root.active.ssid === root.pendingConnection.ssid;
if (!connected && root.pendingConnection.callback) {
// Connection didn't succeed after multiple checks, show password dialog
const pending = root.pendingConnection;
root.pendingConnection = null;
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
pending.callback();
} else if (connected) {
// Connection succeeded, clear pending
root.pendingConnection = null;
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
}
}
}
}
Timer {
id: immediateCheckTimer
interval: 500
repeat: true
triggeredOnStart: false
property int checkCount: 0
onTriggered: {
if (root.pendingConnection) {
checkCount++;
const connected = root.active && root.active.ssid === root.pendingConnection.ssid;
if (connected) {
// Connection succeeded, stop timers and clear pending
connectionCheckTimer.stop();
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
root.pendingConnection = null;
} else if (checkCount >= 6) {
// Checked 6 times (3 seconds total), connection likely failed
// Stop immediate check, let the main timer handle it
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
}
} else {
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
}
}
}
Process {
id: connectProc
onExited: {
// Refresh network list after connection attempt
getNetworks.running = true;
// Check if connection succeeded after a short delay (network list needs to update)
if (root.pendingConnection) {
immediateCheckTimer.start();
}
}
stdout: SplitParser {
onRead: getNetworks.running = true
}
stderr: StdioCollector {
onStreamFinished: console.warn("Network connection error:", text)
onStreamFinished: {
const error = text.trim();
if (error && error.length > 0) {
// Check for specific errors that indicate password is needed
// Be careful not to match success messages
const needsPassword = (error.includes("Secrets were required") ||
error.includes("No secrets provided") ||
error.includes("802-11-wireless-security.psk") ||
(error.includes("password") && !error.includes("Connection activated")) ||
(error.includes("Secrets") && !error.includes("Connection activated")) ||
(error.includes("802.11") && !error.includes("Connection activated"))) &&
!error.includes("Connection activated") &&
!error.includes("successfully");
if (needsPassword && root.pendingConnection && root.pendingConnection.callback) {
// Connection failed because password is needed - show dialog immediately
connectionCheckTimer.stop();
immediateCheckTimer.stop();
const pending = root.pendingConnection;
root.pendingConnection = null;
pending.callback();
} else if (error && error.length > 0 && !error.includes("Connection activated")) {
// Only log non-success messages
console.warn("Network connection error:", error);
}
}
}
}
}
@ -115,6 +221,36 @@ Singleton {
stdout: SplitParser {
onRead: getNetworks.running = true
}
stderr: StdioCollector {
onStreamFinished: {
const error = text.trim();
if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) {
console.warn("Network device disconnect error:", error);
}
}
}
}
Process {
id: disconnectByConnectionProc
onExited: {
// Refresh network list after disconnection
getNetworks.running = true;
}
stdout: SplitParser {
onRead: getNetworks.running = true
}
stderr: StdioCollector {
onStreamFinished: {
const error = text.trim();
if (error && error.length > 0 && !error.includes("successfully") && !error.includes("disconnected")) {
console.warn("Network connection disconnect error:", error);
// If connection down failed, try device disconnect as fallback
disconnectProc.exec(["nmcli", "device", "disconnect", "wifi"]);
}
}
}
}
Process {
@ -182,6 +318,20 @@ Singleton {
}));
}
}
// Check if pending connection succeeded after network list is fully updated
if (root.pendingConnection) {
Qt.callLater(() => {
const connected = root.active && root.active.ssid === root.pendingConnection.ssid;
if (connected) {
// Connection succeeded, stop timers and clear pending
connectionCheckTimer.stop();
immediateCheckTimer.stop();
immediateCheckTimer.checkCount = 0;
root.pendingConnection = null;
}
});
}
}
}
}