From 87bdb649f5e88b935297eae3241c5dddb065a809 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Thu, 13 Aug 2026 15:11:40 +0200 Subject: [PATCH] Improve network settings in general alot --- evcharger-app/CMakeLists.txt | 3 +- evcharger-app/i18n/qml_de.ts | 108 ----- evcharger-app/qml/ApiEnumEditItem.qml | 37 ++ evcharger-app/qml/ApiStatusRow.qml | 47 ++ evcharger-app/qml/CellularPage.qml | 341 +++++++------- evcharger-app/qml/ConnectionPage.qml | 24 +- evcharger-app/qml/EditValueItem.qml | 5 +- evcharger-app/qml/EthernetPage.qml | 198 +++++++- evcharger-app/qml/HotspotPage.qml | 67 ++- evcharger-app/qml/WiFiPage.qml | 652 ++++++++++++++++++++++---- evcharger-app/qml/WiFiScanPage.qml | 414 ---------------- evcharger-app/qml/qmldir | 3 +- 12 files changed, 1113 insertions(+), 786 deletions(-) create mode 100644 evcharger-app/qml/ApiEnumEditItem.qml create mode 100644 evcharger-app/qml/ApiStatusRow.qml delete mode 100644 evcharger-app/qml/WiFiScanPage.qml diff --git a/evcharger-app/CMakeLists.txt b/evcharger-app/CMakeLists.txt index 0aa634d..d00fe15 100644 --- a/evcharger-app/CMakeLists.txt +++ b/evcharger-app/CMakeLists.txt @@ -58,6 +58,8 @@ qt_add_qml_module(evcharger-app qml/AnimatedStackLayout.qml qml/AnimatedStackView.qml qml/ApiKeyValueItem.qml + qml/ApiStatusRow.qml + qml/ApiEnumEditItem.qml qml/ApiSettingsPage.qml qml/AppInstance.qml qml/AppSettingsPage.qml @@ -143,7 +145,6 @@ qt_add_qml_module(evcharger-app qml/WhiteSwipeDelegate.qml qml/WiFiErrorsPage.qml qml/WiFiPage.qml - qml/WiFiScanPage.qml RESOURCES qml/icons/Charger.svg qml/icons/ChargerV3.svg diff --git a/evcharger-app/i18n/qml_de.ts b/evcharger-app/i18n/qml_de.ts index 0eca4c5..ed44160 100644 --- a/evcharger-app/i18n/qml_de.ts +++ b/evcharger-app/i18n/qml_de.ts @@ -3087,114 +3087,6 @@ Pmf erforderlich - - WiFiScanPage - - - Wi-Fi Scan Result - WLAN Scan Result - - - - SSID: - SSID: - - - - Authmode: - Authentifizierungsart: - - - - OPEN - - - - - WEP - - - - - WPA_PSK - - - - - WPA2_PSK - - - - - WPA_WPA2_PSK - - - - - WPA2_ENTERPRISE - - - - - WPA3_PSK - - - - - WPA2_WPA3_PSK - - - - - WAPI_PSK - - - - - OWE - - - - - WPA3_ENT_192 - - - - - WPA3_EXT_PSK - - - - - WPA3_EXT_PSK_MIXED_MODE - - - - - Pairwise cipher: - Paarweise Cipher: - - - - Group cipher: - Gruppen Cipher: - - - - RSSI: - RSSI: - - - - Channel: - Kanal: - - - - BSSID: - BSSID: - - main diff --git a/evcharger-app/qml/ApiEnumEditItem.qml b/evcharger-app/qml/ApiEnumEditItem.qml new file mode 100644 index 0000000..08377fe --- /dev/null +++ b/evcharger-app/qml/ApiEnumEditItem.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Controls +import EVChargerApp + +EditValueItem { + id: item + + required property var choices + + function textForValue(value) { + for (let i = 0; i < choices.length; ++i) { + if (choices[i].value === value) + return choices[i].text + } + return value === null || value === undefined ? qsTr("Automatic") : String(value) + } + + valueText: textForValue(value) + editableItem: ComboBox { + id: comboBox + property var value + + model: item.choices + textRole: "text" + valueRole: "value" + + function selectValue() { + const index = indexOfValue(value) + if (index >= 0) + currentIndex = index + } + + Component.onCompleted: selectValue() + onValueChanged: selectValue() + onActivated: value = currentValue + } +} diff --git a/evcharger-app/qml/ApiStatusRow.qml b/evcharger-app/qml/ApiStatusRow.qml new file mode 100644 index 0000000..61bd67a --- /dev/null +++ b/evcharger-app/qml/ApiStatusRow.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import EVChargerApp + +RowLayout { + id: row + + required property string apiKey + required property string label + property var valueFormatter: null + property alias value: apiValue.value + property alias exists: apiValue.exists + + visible: apiValue.exists + + function defaultValueText(value) { + if (value === null || value === undefined || value === "") + return "—" + if (typeof value === "object") + return JSON.stringify(value) + return String(value) + } + + ApiKeyValueHelper { + id: apiValue + deviceConnection: theDeviceConnection + apiKey: row.apiKey + } + + Label { + Layout.preferredWidth: 125 + Layout.maximumWidth: 125 + text: row.label + font.bold: true + wrapMode: Text.Wrap + } + + Text { + Layout.fillWidth: true + text: row.valueFormatter === null + ? row.defaultValueText(apiValue.value) + : row.valueFormatter(apiValue.value) + wrapMode: Text.Wrap + selectable: true + } +} diff --git a/evcharger-app/qml/CellularPage.qml b/evcharger-app/qml/CellularPage.qml index b5a0082..a0b9189 100644 --- a/evcharger-app/qml/CellularPage.qml +++ b/evcharger-app/qml/CellularPage.qml @@ -4,214 +4,237 @@ import QtQuick.Layouts import EVChargerApp NavigationPage { + id: cellularPage + title: qsTr("Cellular") + function technologyName(value) { + switch (value) { + case 0: return qsTr("No service") + case 1: return qsTr("GSM") + case 2: return qsTr("GPRS") + case 3: return qsTr("EDGE") + case 4: return qsTr("WCDMA") + case 5: return qsTr("HSDPA") + case 6: return qsTr("HSUPA") + case 7: return qsTr("HSPA") + case 8: return qsTr("LTE") + default: return qsTr("Disconnected") + } + } + + function signalText(value) { + if (value === null || value === undefined || value.rssi === null || value.rssi === undefined) + return qsTr("Unavailable") + return value.rssi + " dBm" + (value.ber === null || value.ber === undefined + ? "" : " • BER " + value.ber) + } + ConfirmingOnOffSwitch { apiKey: "cen" + text: checked ? qsTr("Cellular on") : qsTr("Cellular off") dialogTitle: qsTr("Are you sure?") - dialogText: qsTr("Disabling Cellular could make your device unreachable from the cloud!") + dialogText: qsTr("Disabling cellular could make your device unreachable from the cloud!") + } + + WhiteBox { + Layout.fillWidth: true + implicitHeight: modemStatus.implicitHeight + 20 + + ColumnLayout { + id: modemStatus + anchors.fill: parent + anchors.margins: 10 + + Text { + Layout.fillWidth: true + text: qsTr("Mobile network") + font.bold: true + } + + ApiStatusRow { + apiKey: "ccm" + label: qsTr("Connection") + valueFormatter: function(value) { return technologyName(value) } + } + + ApiStatusRow { + apiKey: "mon" + label: qsTr("Operator") + valueFormatter: function(value) { + return value || qsTr("No operator") + } + } + + ApiStatusRow { + apiKey: "msq" + label: qsTr("Signal") + valueFormatter: function(value) { return signalText(value) } + } + + ApiStatusRow { + apiKey: "mia" + label: qsTr("IP address") + valueFormatter: function(value) { + return value && value.ip ? value.ip : qsTr("No address") + } + } + } + } + + Text { + Layout.fillWidth: true + text: qsTr("Connection settings") + font.bold: true } EditValueItem { text: qsTr("APN") apiKey: "moap" + visible: exists fullWidth: true editableItem: TextField { - id: textField0 - property alias value: textField0.text + id: apnInput + property alias value: apnInput.text + placeholderText: qsTr("Access point name") } } + ApiEnumEditItem { + text: qsTr("SIM") + apiKey: "simi" + visible: exists + choices: [ + { value: 0, text: qsTr("Embedded SIM") }, + { value: 1, text: qsTr("SIM card slot") } + ] + } + EditValueItem { - text: qsTr("PIN") + text: qsTr("SIM PIN") apiKey: "mopi" + visible: exists + valueText: value ? qsTr("Configured") : qsTr("Not configured") + editValueTransform: function() { return "" } fullWidth: true editableItem: TextField { - id: textField1 - property alias value: textField1.text + id: pinInput + property alias value: pinInput.text + echoMode: TextInput.Password + inputMethodHints: Qt.ImhDigitsOnly + placeholderText: qsTr("Enter a new PIN") } } - // TODO auth Type + ApiEnumEditItem { + text: qsTr("Preferred network") + apiKey: "csm" + visible: exists + choices: [ + { value: null, text: qsTr("Automatic") }, + { value: 0, text: qsTr("2G only") }, + { value: 1, text: qsTr("4G only") } + ] + } + + ApiEnumEditItem { + id: authenticationType + text: qsTr("Authentication") + apiKey: "mat" + visible: exists + choices: [ + { value: 0, text: qsTr("None") }, + { value: 1, text: qsTr("PAP") }, + { value: 2, text: qsTr("CHAP") }, + { value: 4, text: qsTr("MSCHAP") }, + { value: 8, text: qsTr("MSCHAP v2") }, + { value: 16, text: qsTr("EAP") } + ] + } EditValueItem { text: qsTr("Username") apiKey: "mau" + visible: exists && authenticationType.value !== 0 fullWidth: true editableItem: TextField { - id: textField2 - property alias value: textField2.text + id: usernameInput + property alias value: usernameInput.text } } EditValueItem { text: qsTr("Password") apiKey: "mpap" + visible: exists && authenticationType.value !== 0 + valueText: value ? qsTr("Configured") : qsTr("Not configured") + editValueTransform: function() { return "" } fullWidth: true editableItem: TextField { - id: textField3 - property alias value: textField3.text + id: passwordInput + property alias value: passwordInput.text + echoMode: TextInput.Password + placeholderText: qsTr("Enter a new password") } } - WhiteItemDelegate { - id: delegate - Layout.fillWidth: true - - text: qsTr("Show technical details of LTE modem") - - onClicked: { - gridLayout.visible = true - delegate.text = null - delegate.implicitWidth = Qt.binding(() => gridLayout.implicitWidth + gridLayout.anchors.leftMargin + gridLayout.anchors.rightMargin) - delegate.implicitHeight = Qt.binding(() => gridLayout.implicitHeight + gridLayout.anchors.topMargin + gridLayout.anchors.bottomMargin) + EditValueItem { + text: qsTr("Forced operator") + apiKey: "mopn" + visible: exists + valueText: value ? value : qsTr("Automatic") + fullWidth: true + editableItem: TextField { + id: operatorInput + property alias value: operatorInput.text + placeholderText: qsTr("Automatic when empty") } + } - GridLayout { - id: gridLayout + Button { + id: technicalDetailsButton + Layout.fillWidth: true + checkable: true + text: checked ? qsTr("Hide modem details") : qsTr("Show modem details") + } - columns: 2 + WhiteBox { + Layout.fillWidth: true + implicitHeight: modemDetails.implicitHeight + 20 + visible: technicalDetailsButton.checked + + ColumnLayout { + id: modemDetails anchors.fill: parent - anchors.leftMargin: 5 - anchors.topMargin: 5 - anchors.rightMargin: 5 - anchors.bottomMargin: 5 - visible: false + anchors.margins: 10 - ApiKeyValueHelper { - id: misApiKey - deviceConnection: theDeviceConnection - apiKey: "mis" - } - - Label { - text: qsTr("Initialization status:") + Text { + Layout.fillWidth: true + text: qsTr("Modem details") font.bold: true } - Text { - text: JSON.stringify(misApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } + ApiStatusRow { apiKey: "mis"; label: qsTr("Initialization") } + ApiStatusRow { apiKey: "mps"; label: qsTr("SIM status") } + ApiStatusRow { apiKey: "imei"; label: qsTr("IMEI") } + ApiStatusRow { apiKey: "imsi"; label: qsTr("IMSI") } + ApiStatusRow { apiKey: "iccid"; label: qsTr("ICCID") } + ApiStatusRow { apiKey: "msor"; label: qsTr("Operator selection") } + ApiStatusRow { apiKey: "mas"; label: qsTr("Startup commands") } - ApiKeyValueHelper { - id: imeiApiKey - deviceConnection: theDeviceConnection - apiKey: "imei" - } - - Label { - text: qsTr("IMEI:") - font.bold: true - } - - Text { - text: JSON.stringify(imeiApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: imsiApiKey - deviceConnection: theDeviceConnection - apiKey: "imsi" - } - - Label { - text: qsTr("IMSI:") - font.bold: true - } - - Text { - text: JSON.stringify(imsiApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: mpsApiKey - deviceConnection: theDeviceConnection - apiKey: "mps" - } - - Label { - text: qsTr("Pin status:") - font.bold: true - } - - Text { - text: JSON.stringify(mpsApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: monApiKey - deviceConnection: theDeviceConnection - apiKey: "mon" - } - - Label { - text: qsTr("Operator name:") - font.bold: true - } - - Text { - text: JSON.stringify(monApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: msqApiKey - deviceConnection: theDeviceConnection - apiKey: "msq" - } - - Label { - text: qsTr("Signal quality:") - font.bold: true - } - - Text { - text: JSON.stringify(msqApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: ccmApiKey - deviceConnection: theDeviceConnection - apiKey: "ccm" - } - - Label { - text: qsTr("Current system mode:") - font.bold: true - } - - Text { - text: JSON.stringify(ccmApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - ApiKeyValueHelper { - id: miaApiKey - deviceConnection: theDeviceConnection + ApiStatusRow { apiKey: "mia" - } - - Label { - text: qsTr("IP Address:") - font.bold: true - } - - Text { - text: JSON.stringify(miaApiKey.value, null, 2) - wrapMode: Text.Wrap - Layout.fillWidth: true + label: qsTr("Addressing") + valueFormatter: function(value) { + if (value === null || value === undefined) + return qsTr("Not connected") + const values = [] + if (value.ip) values.push(value.ip) + if (value.subnet) values.push(qsTr("subnet %1").arg(value.subnet)) + if (value.gw) values.push(qsTr("gateway %1").arg(value.gw)) + if (value.ipv6) values.push(value.ipv6) + return values.length > 0 ? values.join(" • ") : qsTr("No address") + } } } } diff --git a/evcharger-app/qml/ConnectionPage.qml b/evcharger-app/qml/ConnectionPage.qml index 52f9b36..cce1b5e 100644 --- a/evcharger-app/qml/ConnectionPage.qml +++ b/evcharger-app/qml/ConnectionPage.qml @@ -9,6 +9,7 @@ NavigationPage { NavigationItem { iconSource: "material-icons/grid_guides.svg" title: qsTr("Wi-Fi") + description: qsTr("Join and manage wireless networks") component: Component { WiFiPage { } @@ -16,19 +17,10 @@ NavigationPage { visible: wifiStaApiKeyValueHelper.exists } - NavigationItem { - iconSource: "material-icons/grid_guides.svg" - title: qsTr("Cellular") - component: Component { - CellularPage { - } - } - visible: cellularApiKeyValueHelper.exists - } - NavigationItem { iconSource: "material-icons/grid_guides.svg" title: qsTr("Hotspot") + description: qsTr("Configure the device's Wi-Fi access point") component: Component { HotspotPage { } @@ -39,6 +31,7 @@ NavigationPage { NavigationItem { iconSource: "material-icons/grid_guides.svg" title: qsTr("Ethernet") + description: qsTr("Wired network and IP settings") component: Component { EthernetPage { } @@ -46,6 +39,17 @@ NavigationPage { visible: ethernetApiKeyValueHelper.exists } + NavigationItem { + iconSource: "material-icons/grid_guides.svg" + title: qsTr("Cellular") + description: qsTr("SIM, APN and mobile network settings") + component: Component { + CellularPage { + } + } + visible: cellularApiKeyValueHelper.exists + } + NavigationItem { iconSource: "material-icons/grid_guides.svg" title: qsTr("Cloud") diff --git a/evcharger-app/qml/EditValueItem.qml b/evcharger-app/qml/EditValueItem.qml index 8304a77..25d339f 100644 --- a/evcharger-app/qml/EditValueItem.qml +++ b/evcharger-app/qml/EditValueItem.qml @@ -10,7 +10,9 @@ WhiteItemDelegate { property string valueText: apiKeyValueHelper.value property alias editableItem: dialog.contentItem property alias value: apiKeyValueHelper.value + property alias exists: apiKeyValueHelper.exists property bool fullWidth: false + property var editValueTransform: null Layout.fillWidth: true @@ -52,7 +54,7 @@ WhiteItemDelegate { } onClicked: { - editableItem.value = value + editableItem.value = editValueTransform === null ? value : editValueTransform(value) dialog.open() } @@ -72,7 +74,6 @@ WhiteItemDelegate { } onAccepted: { - console.log(editableItem.value) setValueHelper.setValue(editableItem.value) } } diff --git a/evcharger-app/qml/EthernetPage.qml b/evcharger-app/qml/EthernetPage.qml index c1d0af9..488e4b8 100644 --- a/evcharger-app/qml/EthernetPage.qml +++ b/evcharger-app/qml/EthernetPage.qml @@ -3,15 +3,207 @@ import QtQuick.Controls import QtQuick.Layouts NavigationPage { + id: ethernetPage + title: qsTr("Ethernet") + function connectionText(value) { + if (value === null || value === undefined) + return qsTr("Not connected") + return value.ip || qsTr("Connected") + } + + function linkText(value) { + if (value === null || value === undefined) + return qsTr("No link") + const speed = value.speed ? value.speed + " Mbit/s" : qsTr("Unknown speed") + const duplex = value.dup === true || value.dup === 1 ? qsTr("full duplex") + : value.dup === false || value.dup === 0 ? qsTr("half duplex") + : value.dup === null || value.dup === undefined ? "" : String(value.dup) + return duplex ? speed + " • " + duplex : speed + } + + ApiKeyValueHelper { + id: staticIpMode + deviceConnection: theDeviceConnection + apiKey: "euse" + } + + ApiKeyValueHelper { + id: staticDnsMode + deviceConnection: theDeviceConnection + apiKey: "eusd" + } + GeneralOnOffSwitch { apiKey: "ee" + text: checked ? qsTr("Ethernet on") : qsTr("Ethernet off") + } + + WhiteBox { + Layout.fillWidth: true + implicitHeight: connectionStatus.implicitHeight + 20 + + ColumnLayout { + id: connectionStatus + anchors.fill: parent + anchors.margins: 10 + + Text { + Layout.fillWidth: true + text: qsTr("Connection") + font.bold: true + } + + ApiStatusRow { + apiKey: "cce" + label: qsTr("Address") + valueFormatter: function(value) { return connectionText(value) } + } + + ApiStatusRow { + apiKey: "cce" + label: qsTr("Link") + valueFormatter: function(value) { return linkText(value) } + } + + ApiStatusRow { + apiKey: "cce" + label: qsTr("Gateway") + valueFormatter: function(value) { + return value && value.gw ? value.gw : "—" + } + } + } } Text { - text: "TODO" - - Layout.fillHeight: true + Layout.fillWidth: true + text: qsTr("IPv4 configuration") + font.bold: true } + + GeneralOnOffSwitch { + apiKey: "euse" + text: checked ? qsTr("Static IP address") : qsTr("Automatic IP address (DHCP)") + } + + EditValueItem { + text: qsTr("IP address") + apiKey: "esi" + visible: exists && staticIpMode.value === true + fullWidth: true + editableItem: TextField { + id: ipAddressInput + property alias value: ipAddressInput.text + inputMethodHints: Qt.ImhPreferNumbers + placeholderText: "192.168.1.10" + } + } + + EditValueItem { + text: qsTr("Subnet mask") + apiKey: "ess" + visible: exists && staticIpMode.value === true + fullWidth: true + editableItem: TextField { + id: subnetInput + property alias value: subnetInput.text + inputMethodHints: Qt.ImhPreferNumbers + placeholderText: "255.255.255.0" + } + } + + EditValueItem { + text: qsTr("Gateway") + apiKey: "esg" + visible: exists && staticIpMode.value === true + fullWidth: true + editableItem: TextField { + id: gatewayInput + property alias value: gatewayInput.text + inputMethodHints: Qt.ImhPreferNumbers + placeholderText: "192.168.1.1" + } + } + + GeneralOnOffSwitch { + apiKey: "eusd" + text: checked ? qsTr("Static DNS servers") : qsTr("Automatic DNS servers") + } + + EditValueItem { + text: qsTr("Primary DNS") + apiKey: "esd0" + visible: exists && staticDnsMode.value === true + fullWidth: true + editableItem: TextField { + id: dns0Input + property alias value: dns0Input.text + inputMethodHints: Qt.ImhPreferNumbers + placeholderText: "1.1.1.1" + } + } + + EditValueItem { + text: qsTr("Secondary DNS") + apiKey: "esd1" + visible: exists && staticDnsMode.value === true + fullWidth: true + editableItem: TextField { + id: dns1Input + property alias value: dns1Input.text + inputMethodHints: Qt.ImhPreferNumbers + placeholderText: "8.8.8.8" + } + } + + EditValueItem { + text: qsTr("Additional DNS") + apiKey: "esd2" + visible: exists && staticDnsMode.value === true + fullWidth: true + editableItem: TextField { + id: dns2Input + property alias value: dns2Input.text + inputMethodHints: Qt.ImhPreferNumbers + } + } + + WhiteBox { + Layout.fillWidth: true + implicitHeight: ethernetDetails.implicitHeight + 20 + visible: ethernetHostname.exists || ethernetMac.exists || ethernetInit.exists + + ColumnLayout { + id: ethernetDetails + anchors.fill: parent + anchors.margins: 10 + + Text { + Layout.fillWidth: true + text: qsTr("Interface details") + font.bold: true + } + + ApiStatusRow { + id: ethernetHostname + apiKey: "hste" + label: qsTr("Hostname") + } + + ApiStatusRow { + id: ethernetMac + apiKey: "mace" + label: qsTr("MAC address") + } + + ApiStatusRow { + id: ethernetInit + apiKey: "eis" + label: qsTr("Initialization") + } + } + } + } diff --git a/evcharger-app/qml/HotspotPage.qml b/evcharger-app/qml/HotspotPage.qml index 7385deb..854b493 100644 --- a/evcharger-app/qml/HotspotPage.qml +++ b/evcharger-app/qml/HotspotPage.qml @@ -3,44 +3,105 @@ import QtQuick.Controls import QtQuick.Layouts NavigationPage { + id: hotspotPage + title: qsTr("Hotspot") GeneralOnOffSwitch { apiKey: "wae" + text: checked ? qsTr("Hotspot on") : qsTr("Hotspot off") + } + + Text { + Layout.fillWidth: true + text: qsTr("Configuration") + font.bold: true } GeneralOnOffSwitch { apiKey: "wda" - text: qsTr("Disable AP when online") + text: qsTr("Turn hotspot off when another internet connection is available") } EditValueItem { text: qsTr("SSID") apiKey: "wan" + visible: exists fullWidth: true editableItem: TextField { id: textField0 property alias value: textField0.text + placeholderText: qsTr("Hotspot name") } } EditValueItem { text: qsTr("Password") apiKey: "wak" + visible: exists + valueText: value ? qsTr("Configured") : qsTr("Open network") + editValueTransform: function() { return "" } fullWidth: true editableItem: TextField { id: textField1 property alias value: textField1.text + echoMode: TextInput.Password + placeholderText: qsTr("Enter a new password") } } + ApiEnumEditItem { + text: qsTr("Security") + apiKey: "waap" + visible: exists + choices: [ + { value: 0, text: qsTr("Open") }, + { value: 2, text: qsTr("WPA") }, + { value: 3, text: qsTr("WPA2") }, + { value: 4, text: qsTr("WPA/WPA2") }, + { value: 6, text: qsTr("WPA3") }, + { value: 7, text: qsTr("WPA2/WPA3") } + ] + } + EditValueItem { text: qsTr("Channel") valueText: Qt.locale().toString(value) apiKey: "wapc" + visible: exists editableItem: SpinBox { - from: 0 - to: 15 + from: 1 + to: 14 + } + } + + WhiteBox { + Layout.fillWidth: true + implicitHeight: hotspotStatus.implicitHeight + 20 + visible: hotspotHostname.exists || hotspotMac.exists + + ColumnLayout { + id: hotspotStatus + anchors.fill: parent + anchors.margins: 10 + + Text { + Layout.fillWidth: true + text: qsTr("Hotspot identity") + font.bold: true + } + + ApiStatusRow { + id: hotspotHostname + apiKey: "hsta" + label: qsTr("Hostname") + } + + ApiStatusRow { + id: hotspotMac + apiKey: "maca" + label: qsTr("MAC address") + } } } } diff --git a/evcharger-app/qml/WiFiPage.qml b/evcharger-app/qml/WiFiPage.qml index b9b63b4..990c3f1 100644 --- a/evcharger-app/qml/WiFiPage.qml +++ b/evcharger-app/qml/WiFiPage.qml @@ -4,127 +4,428 @@ import QtQuick.Layouts import EVChargerApp NavigationPage { + id: wifiPage + + property var selectedNetwork: null + property int selectedWifiSlot: -1 + title: qsTr("Wi-Fi") + function configuredNetworks() { + const result = [] + const wifis = configuredWifis.value + if (wifis === null || wifis === undefined) + return result + + for (let i = 0; i < wifis.length; ++i) { + const wifi = wifis[i] + if (wifi !== null && wifi !== undefined && wifi.ssid) { + result.push({ + slot: i, + ssid: wifi.ssid, + useStaticIp: wifi.useStaticIp === true, + staticIp: wifi.staticIp + }) + } + } + return result + } + + function isConfigured(ssid) { + const networks = configuredNetworks() + for (let i = 0; i < networks.length; ++i) { + if (networks[i].ssid === ssid) + return true + } + return false + } + + function firstFreeWifiSlot() { + const wifis = configuredWifis.value + if (wifis === null || wifis === undefined) + return -2 + + for (let i = 0; i < wifis.length; ++i) { + if (wifis[i] === null || wifis[i] === undefined || !wifis[i].ssid) + return i + } + return -1 + } + + function availableNetworks() { + const result = [] + const scan = wifiScanResult.value + if (scan === null || scan === undefined) + return result + + // Android presents an SSID once. Keep the strongest BSSID when the + // same network was observed through multiple access points. + for (let i = 0; i < scan.length; ++i) { + const candidate = scan[i] + if (candidate === null || candidate === undefined || !candidate.ssid) + continue + + let existing = -1 + for (let j = 0; j < result.length; ++j) { + if (result[j].ssid === candidate.ssid) { + existing = j + break + } + } + + if (existing < 0) + result.push(candidate) + else if ((candidate.rssi ?? -1000) > (result[existing].rssi ?? -1000)) + result[existing] = candidate + } + + result.sort((left, right) => (right.rssi ?? -1000) - (left.rssi ?? -1000)) + return result + } + + function securityName(encryptionType) { + switch (encryptionType) { + case 0: return qsTr("Open") + case 1: return qsTr("WEP") + case 2: return qsTr("WPA") + case 3: return qsTr("WPA2") + case 4: return qsTr("WPA/WPA2") + case 5: return qsTr("WPA2 Enterprise") + case 6: return qsTr("WPA3") + case 7: return qsTr("WPA2/WPA3") + case 8: return qsTr("WAPI") + case 9: return qsTr("OWE") + case 10: return qsTr("WPA3 Enterprise") + case 11: return qsTr("WPA3 External PSK") + case 12: return qsTr("WPA3 External PSK mixed") + default: return qsTr("Unknown security") + } + } + + function needsPassword(network) { + return network.encryptionType !== 0 && network.encryptionType !== 9 + } + + function signalBars(rssi) { + if (rssi >= -55) + return "▂▄▆█" + if (rssi >= -67) + return "▂▄▆" + if (rssi >= -75) + return "▂▄" + return "▂" + } + + function stationStatusName(status) { + switch (status) { + case 0: return qsTr("Idle") + case 1: return qsTr("Network not found") + case 2: return qsTr("Scan completed") + case 3: return qsTr("Connected") + case 4: return qsTr("Connection failed") + case 5: return qsTr("Connection lost") + case 6: return qsTr("Disconnected") + case 7: return qsTr("Connecting") + case 8: return qsTr("Disconnecting") + case 9: return qsTr("Wi-Fi unavailable") + case 10: return qsTr("Waiting for IP address") + default: return qsTr("Unknown") + } + } + + function scanStatusName(status) { + switch (status) { + case 0: return qsTr("Not scanned yet") + case 1: return qsTr("Scanning") + case 2: return qsTr("Finished") + case 3: return qsTr("Failed") + default: return qsTr("Unknown") + } + } + + function selectNetwork(network) { + if (isConfigured(network.ssid)) + return + + const slot = firstFreeWifiSlot() + if (slot === -2) { + noSlotDialog.text = qsTr("The Wi-Fi configuration has not been received yet.") + noSlotDialog.open() + return + } + if (slot < 0) { + noSlotDialog.text = qsTr("All Wi-Fi configuration slots are occupied. Delete a configured network first.") + noSlotDialog.open() + return + } + + selectedNetwork = network + selectedWifiSlot = slot + passwordInput.clear() + showPassword.checked = false + passwordDialog.open() + } + + function provisionSelectedNetwork() { + if (selectedNetwork === null || selectedWifiSlot < 0) + return + + const value = [] + for (let i = 0; i < selectedWifiSlot; ++i) + value.push(null) + value.push({ + ssid: selectedNetwork.ssid, + key: needsPassword(selectedNetwork) ? passwordInput.text : "" + }) + provisionWifi.setValue(value) + } + + ApiKeyValueHelper { + id: configuredWifis + deviceConnection: theDeviceConnection + apiKey: "wifis" + } + + ApiKeyValueHelper { + id: wifiScanResult + deviceConnection: theDeviceConnection + apiKey: "scan" + } + + SetValueHelper { + id: provisionWifi + deviceConnection: theDeviceConnection + apiKey: "wifis" + } + + SetValueHelper { + id: deleteWifi + deviceConnection: theDeviceConnection + apiKey: "delw" + } + + SendMessageHelper { + id: triggerScan + deviceConnection: theDeviceConnection + } + ConfirmingOnOffSwitch { apiKey: "wen" dialogTitle: qsTr("Are you sure?") dialogText: qsTr("Disabling Wi-Fi could make your device unreachable from your local homenetwork or the cloud!") } - GridLayout { - columns: 2 + WhiteBox { + Layout.fillWidth: true + implicitHeight: connectionStatusColumn.implicitHeight + 20 - Text { - text: qsTr("Status:") - } + ColumnLayout { + id: connectionStatusColumn + anchors.fill: parent + anchors.margins: 10 - Text { - Layout.fillWidth: true - - ApiKeyValueHelper { - id: staStatus - deviceConnection: theDeviceConnection - apiKey: "wst" + Text { + Layout.fillWidth: true + text: qsTr("Connection") + font.bold: true } - text: { - switch (staStatus.value) - { - case 0: return "IDLE_STATUS" - case 1: return "NO_SSID_AVAIL" - case 2: return "SCAN_COMPLETED" - case 3: return "CONNECTED" - case 4: return "CONNECT_FAILED" - case 5: return "CONNECTION_LOST" - case 6: return "DISCONNECTED" - case 7: return "CONNECTING" - case 8: return "DISCONNECTING" - case 9: return "NO_SHIELD" - case 10: return "WAITING_FOR_IP" - default: return staStatus.value + ApiStatusRow { + apiKey: "wst" + label: qsTr("Status") + valueFormatter: function(value) { return stationStatusName(value) } + } + + ApiStatusRow { + apiKey: "ccw" + label: qsTr("Network") + valueFormatter: function(value) { + if (value === null || value === undefined) + return qsTr("Not connected") + const address = value.ip ? " • " + value.ip : "" + return (value.ssid || qsTr("Unknown network")) + address + } + } + + ApiStatusRow { + apiKey: "rssi" + label: qsTr("Signal") + valueFormatter: function(value) { + return value === null || value === undefined + ? "—" + : signalBars(value) + " " + value + " dBm" + } + } + + ApiStatusRow { + apiKey: "scas" + label: qsTr("Wi-Fi scan") + valueFormatter: function(value) { return scanStatusName(value) } + } + + ApiStatusRow { + apiKey: "scaa" + label: qsTr("Last scan") + valueFormatter: function(value) { + return value === null || value === undefined + ? qsTr("Never") + : formatDuration(value - rebootTime.value) } } } + } - Text { - text: qsTr("Scan-Status:") - } + RowLayout { + Layout.fillWidth: true Text { Layout.fillWidth: true + text: qsTr("Configured networks") + font.bold: true + } + + BusyIndicator { + visible: provisionWifi.pending || deleteWifi.pending || triggerScan.pending + } + + Button { + text: scanStatus.value === 1 ? qsTr("Scanning…") : qsTr("Scan again") + enabled: !triggerScan.pending && scanStatus.value !== 1 + onClicked: triggerScan.sendMessage({type: "wifiScan"}) ApiKeyValueHelper { id: scanStatus deviceConnection: theDeviceConnection apiKey: "scas" } - - text: { - switch (scanStatus.value) - { - case 0: return "None" - case 1: return "Scanning" - case 2: return "Finished" - case 3: return "Failed" - default: return scanStatus.value - } - } - } - - Text { - text: qsTr("Scan-Age:") - } - - Text { - Layout.fillWidth: true - - ApiKeyValueHelper { - id: scanAgeStatus - deviceConnection: theDeviceConnection - apiKey: "scaa" - } - - text: scanAgeStatus.value != null ? formatDuration(scanAgeStatus.value - rebootTime.value) : null } } - RowLayout { - SendMessageButton { + Text { + Layout.fillWidth: true + text: qsTr("No configured networks") + visible: configuredRepeater.count === 0 + color: "gray" + } + + Repeater { + id: configuredRepeater + model: configuredNetworks() + + delegate: WhiteBox { + required property var modelData Layout.fillWidth: true - text: qsTr("Trigger scan") - messageType: "wifiScan" - } + implicitHeight: configuredRow.implicitHeight + 20 - Button { - ApiKeyValueHelper { - id: wifiScanResult - deviceConnection: theDeviceConnection - apiKey: "scan" - } + RowLayout { + id: configuredRow + anchors.fill: parent + anchors.margins: 10 - ApiKeyValueHelper { - id: configuredWifis - deviceConnection: theDeviceConnection - apiKey: "wifis" - } + ColumnLayout { + Layout.fillWidth: true - text: qsTr("(%0) Available networks").arg(wifiScanResult.value == null ? 0 : wifiScanResult.value.length) - onClicked: stackView.push(wiFiScanPageComponent, { - wifiScanResult: wifiScanResult, - configuredWifis: configuredWifis - }) - enabled: wifiScanResult.value != null + Text { + Layout.fillWidth: true + text: modelData.ssid + font.bold: true + elide: Text.ElideRight + } - Component { - id: wiFiScanPageComponent + Text { + text: modelData.useStaticIp + ? qsTr("Static IP %1 • slot %2").arg(modelData.staticIp).arg(modelData.slot + 1) + : qsTr("Automatic IP • slot %1").arg(modelData.slot + 1) + color: "gray" + } + } - WiFiScanPage { + Button { + text: qsTr("Delete") + enabled: !deleteWifi.pending + onClicked: { + selectedWifiSlot = modelData.slot + deleteDialog.networkName = modelData.ssid + deleteDialog.open() + } } } } } + Text { + Layout.fillWidth: true + Layout.topMargin: 10 + text: qsTr("Available networks") + font.bold: true + } + + Repeater { + model: availableNetworks() + + delegate: WhiteItemDelegate { + required property var modelData + readonly property bool alreadyConfigured: isConfigured(modelData.ssid) + + Layout.fillWidth: true + implicitHeight: availableRow.implicitHeight + 20 + enabled: !alreadyConfigured && !provisionWifi.pending + onClicked: selectNetwork(modelData) + + contentItem: RowLayout { + id: availableRow + + ColumnLayout { + Layout.fillWidth: true + + Text { + Layout.fillWidth: true + text: modelData.ssid + font.bold: true + elide: Text.ElideRight + } + + Text { + Layout.fillWidth: true + text: alreadyConfigured + ? qsTr("Saved • %1").arg(securityName(modelData.encryptionType)) + : securityName(modelData.encryptionType) + color: "gray" + elide: Text.ElideRight + } + } + + Text { + text: signalBars(modelData.rssi) + font.bold: true + Accessible.name: qsTr("Signal strength %1 dBm").arg(modelData.rssi) + } + + Text { + text: alreadyConfigured ? "✓" : ">" + } + } + } + } + + Text { + Layout.fillWidth: true + text: qsTr("No networks found. Start another scan.") + visible: availableNetworks().length === 0 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + } + + RequestStatusText { + Layout.fillWidth: true + request: provisionWifi + } + + RequestStatusText { + Layout.fillWidth: true + request: deleteWifi + } + Button { ApiKeyValueHelper { id: wifiErrorLog @@ -133,7 +434,7 @@ NavigationPage { } text: qsTr("(%0) Wi-Fi Errors").arg(wifiErrorLog.value == null ? 0 : wifiErrorLog.value.length) - onClicked: stackView.push(wiFiErrorsPageComponent, {wifiErrorLog} ) + onClicked: stackView.push(wiFiErrorsPageComponent, {wifiErrorLog}) enabled: wifiErrorLog.value != null Component { @@ -144,10 +445,18 @@ NavigationPage { } } + Text { + Layout.fillWidth: true + Layout.topMargin: 10 + text: qsTr("Advanced Wi-Fi settings") + font.bold: true + } + EditValueItem { text: qsTr("Min RSSI") valueText: Qt.locale().toString(value) apiKey: "wsmr" + visible: exists editableItem: SpinBox { from: -100 to: 0 @@ -156,11 +465,184 @@ NavigationPage { GeneralOnOffSwitch { apiKey: "wspc" - text: qsTr("Pmf capable") + text: qsTr("Support protected management frames") } GeneralOnOffSwitch { apiKey: "wspr" - text: qsTr("Pmf required") + text: qsTr("Require protected management frames") + } + + WhiteBox { + Layout.fillWidth: true + implicitHeight: wifiDetailsColumn.implicitHeight + 20 + visible: wifiAddressing.exists || wifiAccessPoint.exists || wifiConnectionProcess.exists + || wifiMac.exists || wifiHostname.exists || wifiErrorCount.exists + + ColumnLayout { + id: wifiDetailsColumn + anchors.fill: parent + anchors.margins: 10 + + Text { + Layout.fillWidth: true + text: qsTr("Technical details") + font.bold: true + } + + ApiStatusRow { + id: wifiAddressing + apiKey: "ccw" + label: qsTr("Addressing") + valueFormatter: function(value) { + if (value === null || value === undefined) + return "—" + const parts = [] + if (value.ip) parts.push(qsTr("IP %1").arg(value.ip)) + if (value.netmask) parts.push(qsTr("mask %1").arg(value.netmask)) + if (value.gw) parts.push(qsTr("gateway %1").arg(value.gw)) + return parts.length > 0 ? parts.join(" • ") : "—" + } + } + + ApiStatusRow { + id: wifiAccessPoint + apiKey: "ccw" + label: qsTr("Access point") + valueFormatter: function(value) { + if (value === null || value === undefined) + return "—" + const channel = value.channel !== undefined ? qsTr("channel %1").arg(value.channel) : "" + return [value.bssid, channel].filter(Boolean).join(" • ") + } + } + + ApiStatusRow { + id: wifiConnectionProcess + apiKey: "wsms" + label: qsTr("Connection process") + valueFormatter: function(value) { + const names = [qsTr("Idle"), qsTr("Scanning"), qsTr("Connecting"), qsTr("Connected")] + return value >= 0 && value < names.length ? names[value] : qsTr("Unknown") + } + } + + ApiStatusRow { + id: wifiMac + apiKey: "macs" + label: qsTr("MAC address") + } + + ApiStatusRow { + id: wifiHostname + apiKey: "hsts" + label: qsTr("Hostname") + } + + ApiStatusRow { + id: wifiErrorCount + apiKey: "wsc" + label: qsTr("Connection errors") + } + } + } + + CenteredDialog { + id: passwordDialog + title: selectedNetwork === null ? qsTr("Connect to Wi-Fi") : selectedNetwork.ssid + standardButtons: Dialog.Ok | Dialog.Cancel + modal: true + focus: true + + onOpened: { + passwordInput.forceActiveFocus() + standardButton(Dialog.Ok).enabled = !needsPassword(selectedNetwork) + } + onAccepted: provisionSelectedNetwork() + + contentItem: ColumnLayout { + Text { + Layout.fillWidth: true + text: selectedNetwork === null ? "" : securityName(selectedNetwork.encryptionType) + color: "gray" + } + + TextField { + id: passwordInput + Layout.minimumWidth: 240 + Layout.fillWidth: true + visible: selectedNetwork !== null && needsPassword(selectedNetwork) + placeholderText: qsTr("Password") + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password + maximumLength: 64 + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + onTextChanged: { + if (passwordDialog.visible) + passwordDialog.standardButton(Dialog.Ok).enabled = text.length >= 8 + } + onAccepted: { + if (text.length >= 8) + passwordDialog.accept() + } + } + + CheckBox { + id: showPassword + visible: passwordInput.visible + text: qsTr("Show password") + } + + Text { + Layout.fillWidth: true + visible: passwordInput.visible && passwordInput.text.length > 0 && passwordInput.text.length < 8 + text: qsTr("The password must contain at least 8 characters.") + color: "red" + wrapMode: Text.Wrap + } + + Text { + Layout.fillWidth: true + visible: selectedNetwork !== null && !needsPassword(selectedNetwork) + text: qsTr("This network does not require a password.") + wrapMode: Text.Wrap + } + + Text { + Layout.fillWidth: true + text: qsTr("The network will be saved in slot %1.").arg(selectedWifiSlot + 1) + wrapMode: Text.Wrap + } + } + } + + CenteredDialog { + id: deleteDialog + property string networkName + + title: qsTr("Delete Wi-Fi network?") + standardButtons: Dialog.Yes | Dialog.No + modal: true + focus: true + onAccepted: deleteWifi.setValue(selectedWifiSlot) + + contentItem: Text { + text: qsTr("Delete %1 and all of its IP and DNS settings?").arg(deleteDialog.networkName) + wrapMode: Text.Wrap + } + } + + CenteredDialog { + id: noSlotDialog + property alias text: noSlotText.text + + title: qsTr("Cannot add Wi-Fi network") + standardButtons: Dialog.Ok + modal: true + focus: true + + contentItem: Text { + id: noSlotText + wrapMode: Text.Wrap + } } } diff --git a/evcharger-app/qml/WiFiScanPage.qml b/evcharger-app/qml/WiFiScanPage.qml deleted file mode 100644 index b2eb331..0000000 --- a/evcharger-app/qml/WiFiScanPage.qml +++ /dev/null @@ -1,414 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import EVChargerApp - -BaseNavigationPage { - property ApiKeyValueHelper wifiScanResult - property ApiKeyValueHelper configuredWifis - property var selectedNetwork: null - property int selectedWifiSlot: -1 - - title: qsTr("Wi-Fi networks") - - function configuredNetworks() { - const result = [] - const wifis = configuredWifis === null ? null : configuredWifis.value - if (wifis === null || wifis === undefined) - return result - - for (let i = 0; i < wifis.length; ++i) { - const wifi = wifis[i] - if (wifi !== null && wifi !== undefined && wifi.ssid) - result.push({slot: i, ssid: wifi.ssid, hasKey: wifi.key === true}) - } - return result - } - - function isConfigured(ssid) { - const networks = configuredNetworks() - for (let i = 0; i < networks.length; ++i) { - if (networks[i].ssid === ssid) - return true - } - return false - } - - function firstFreeWifiSlot() { - const wifis = configuredWifis === null ? null : configuredWifis.value - if (wifis === null || wifis === undefined) - return -2 - - for (let i = 0; i < wifis.length; ++i) { - if (wifis[i] === null || wifis[i] === undefined || !wifis[i].ssid) - return i - } - return -1 - } - - function availableNetworks() { - const result = [] - const scan = wifiScanResult === null ? null : wifiScanResult.value - if (scan === null || scan === undefined) - return result - - // Android presents an SSID once. Keep the strongest BSSID when the - // same network was observed through multiple access points. - for (let i = 0; i < scan.length; ++i) { - const candidate = scan[i] - if (candidate === null || candidate === undefined || !candidate.ssid) - continue - - let existing = -1 - for (let j = 0; j < result.length; ++j) { - if (result[j].ssid === candidate.ssid) { - existing = j - break - } - } - - if (existing < 0) - result.push(candidate) - else if ((candidate.rssi ?? -1000) > (result[existing].rssi ?? -1000)) - result[existing] = candidate - } - - result.sort((left, right) => (right.rssi ?? -1000) - (left.rssi ?? -1000)) - return result - } - - function securityName(encryptionType) { - switch (encryptionType) { - case 0: return qsTr("Open") - case 1: return qsTr("WEP") - case 2: return qsTr("WPA") - case 3: return qsTr("WPA2") - case 4: return qsTr("WPA/WPA2") - case 5: return qsTr("WPA2 Enterprise") - case 6: return qsTr("WPA3") - case 7: return qsTr("WPA2/WPA3") - case 8: return qsTr("WAPI") - case 9: return qsTr("OWE") - case 10: return qsTr("WPA3 Enterprise") - case 11: return qsTr("WPA3 External PSK") - case 12: return qsTr("WPA3 External PSK mixed") - default: return qsTr("Unknown security") - } - } - - function needsPassword(network) { - return network.encryptionType !== 0 && network.encryptionType !== 9 - } - - function signalBars(rssi) { - if (rssi >= -55) - return "▂▄▆█" - if (rssi >= -67) - return "▂▄▆" - if (rssi >= -75) - return "▂▄" - return "▂" - } - - function selectNetwork(network) { - if (isConfigured(network.ssid)) - return - - const slot = firstFreeWifiSlot() - if (slot === -2) { - noSlotDialog.text = qsTr("The Wi-Fi configuration has not been received yet.") - noSlotDialog.open() - return - } - if (slot < 0) { - noSlotDialog.text = qsTr("All Wi-Fi configuration slots are occupied. Delete a configured network first.") - noSlotDialog.open() - return - } - - selectedNetwork = network - selectedWifiSlot = slot - passwordInput.clear() - passwordDialog.open() - } - - function provisionSelectedNetwork() { - if (selectedNetwork === null || selectedWifiSlot < 0) - return - - const value = [] - for (let i = 0; i < selectedWifiSlot; ++i) - value.push(null) - value.push({ - ssid: selectedNetwork.ssid, - key: needsPassword(selectedNetwork) ? passwordInput.text : "" - }) - provisionWifi.setValue(value) - } - - headerItems: RowLayout { - BusyIndicator { - visible: provisionWifi.pending || deleteWifi.pending || triggerScan.pending - } - - Button { - text: qsTr("Scan again") - enabled: !triggerScan.pending - onClicked: triggerScan.sendMessage({type: "wifiScan"}) - } - } - - SetValueHelper { - id: provisionWifi - deviceConnection: theDeviceConnection - apiKey: "wifis" - } - - SetValueHelper { - id: deleteWifi - deviceConnection: theDeviceConnection - apiKey: "delw" - } - - SendMessageHelper { - id: triggerScan - deviceConnection: theDeviceConnection - } - - Flickable { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - contentHeight: networksColumn.implicitHeight - ScrollBar.vertical: ScrollBar {} - - ColumnLayout { - id: networksColumn - width: parent.width - 30 - x: 15 - spacing: 5 - - Text { - Layout.fillWidth: true - text: qsTr("Configured networks") - font.bold: true - visible: configuredRepeater.count > 0 - } - - Repeater { - id: configuredRepeater - model: configuredNetworks() - - delegate: WhiteBox { - required property var modelData - Layout.fillWidth: true - implicitHeight: configuredRow.implicitHeight + 20 - - RowLayout { - id: configuredRow - anchors.fill: parent - anchors.margins: 10 - - ColumnLayout { - Layout.fillWidth: true - - Text { - Layout.fillWidth: true - text: modelData.ssid - font.bold: true - elide: Text.ElideRight - } - - Text { - text: qsTr("Saved in slot %1").arg(modelData.slot + 1) - color: "gray" - } - } - - Button { - text: qsTr("Delete") - enabled: !deleteWifi.pending - onClicked: { - selectedWifiSlot = modelData.slot - deleteDialog.networkName = modelData.ssid - deleteDialog.open() - } - } - } - } - } - - Text { - Layout.fillWidth: true - Layout.topMargin: configuredRepeater.count > 0 ? 10 : 0 - text: qsTr("Available networks") - font.bold: true - } - - Repeater { - model: availableNetworks() - - delegate: WhiteItemDelegate { - required property var modelData - readonly property bool alreadyConfigured: isConfigured(modelData.ssid) - - Layout.fillWidth: true - implicitHeight: availableRow.implicitHeight + 20 - enabled: !alreadyConfigured && !provisionWifi.pending - onClicked: selectNetwork(modelData) - - contentItem: RowLayout { - id: availableRow - - ColumnLayout { - Layout.fillWidth: true - - Text { - Layout.fillWidth: true - text: modelData.ssid - font.bold: true - elide: Text.ElideRight - } - - Text { - Layout.fillWidth: true - text: alreadyConfigured - ? qsTr("Saved • %1").arg(securityName(modelData.encryptionType)) - : securityName(modelData.encryptionType) - color: "gray" - elide: Text.ElideRight - } - } - - Text { - text: signalBars(modelData.rssi) - font.bold: true - Accessible.name: qsTr("Signal strength %1 dBm").arg(modelData.rssi) - } - - Text { - text: alreadyConfigured ? "✓" : ">" - } - } - } - } - - Text { - Layout.fillWidth: true - text: qsTr("No networks found. Start another scan.") - visible: availableNetworks().length === 0 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - } - - RequestStatusText { - Layout.fillWidth: true - request: provisionWifi - } - - RequestStatusText { - Layout.fillWidth: true - request: deleteWifi - } - } - } - - CenteredDialog { - id: passwordDialog - title: selectedNetwork === null ? qsTr("Connect to Wi-Fi") : selectedNetwork.ssid - standardButtons: Dialog.Ok | Dialog.Cancel - modal: true - focus: true - - onOpened: { - passwordInput.forceActiveFocus() - standardButton(Dialog.Ok).enabled = !needsPassword(selectedNetwork) - } - onAccepted: provisionSelectedNetwork() - - contentItem: ColumnLayout { - Text { - Layout.fillWidth: true - text: selectedNetwork === null ? "" : securityName(selectedNetwork.encryptionType) - color: "gray" - } - - TextField { - id: passwordInput - Layout.minimumWidth: 240 - Layout.fillWidth: true - visible: selectedNetwork !== null && needsPassword(selectedNetwork) - placeholderText: qsTr("Password") - echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password - maximumLength: 64 - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText - onTextChanged: { - if (passwordDialog.visible) - passwordDialog.standardButton(Dialog.Ok).enabled = text.length >= 8 - } - onAccepted: { - if (text.length >= 8) - passwordDialog.accept() - } - } - - CheckBox { - id: showPassword - visible: passwordInput.visible - text: qsTr("Show password") - } - - Text { - Layout.fillWidth: true - visible: passwordInput.visible && passwordInput.text.length > 0 && passwordInput.text.length < 8 - text: qsTr("The password must contain at least 8 characters.") - color: "red" - wrapMode: Text.Wrap - } - - Text { - Layout.fillWidth: true - visible: selectedNetwork !== null && !needsPassword(selectedNetwork) - text: qsTr("This network does not require a password.") - wrapMode: Text.Wrap - } - - Text { - Layout.fillWidth: true - text: qsTr("The network will be saved in slot %1.").arg(selectedWifiSlot + 1) - wrapMode: Text.Wrap - } - } - } - - CenteredDialog { - id: deleteDialog - property string networkName - - title: qsTr("Delete Wi-Fi network?") - standardButtons: Dialog.Yes | Dialog.No - modal: true - focus: true - onAccepted: deleteWifi.setValue(selectedWifiSlot) - - contentItem: Text { - text: qsTr("Delete %1 and all of its IP and DNS settings?").arg(deleteDialog.networkName) - wrapMode: Text.Wrap - } - } - - CenteredDialog { - id: noSlotDialog - property alias text: noSlotText.text - - title: qsTr("Cannot add Wi-Fi network") - standardButtons: Dialog.Ok - modal: true - focus: true - - contentItem: Text { - id: noSlotText - wrapMode: Text.Wrap - } - } -} diff --git a/evcharger-app/qml/qmldir b/evcharger-app/qml/qmldir index 22e767f..a9518d6 100644 --- a/evcharger-app/qml/qmldir +++ b/evcharger-app/qml/qmldir @@ -6,6 +6,8 @@ EVChargerApp 1.0 AddDeviceScreen.qml EVChargerApp 1.0 AnimatedStackLayout.qml EVChargerApp 1.0 AnimatedStackView.qml EVChargerApp 1.0 ApiKeyValueItem.qml +EVChargerApp 1.0 ApiStatusRow.qml +EVChargerApp 1.0 ApiEnumEditItem.qml EVChargerApp 1.0 ApiSettingsPage.qml EVChargerApp 1.0 AppInstance.qml EVChargerApp 1.0 AppSettingsPage.qml @@ -91,4 +93,3 @@ EVChargerApp 1.0 WhiteItemDelegate.qml EVChargerApp 1.0 WhiteSwipeDelegate.qml EVChargerApp 1.0 WiFiErrorsPage.qml EVChargerApp 1.0 WiFiPage.qml -EVChargerApp 1.0 WiFiScanPage.qml