diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 0571c17..2b06c6f 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -13,8 +13,6 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 - visible: Settings.data.network.wifiEnabled - sizeRatio: 0.8 Component.onCompleted: { diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 484f9ee..98fde74 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -14,16 +14,11 @@ NPanel { panelHeight: 500 * scaling panelKeyboardFocus: true - property string passwordPromptSsid: "" + property string passwordSsid: "" property string passwordInput: "" - property bool showPasswordPrompt: false - property string expandedNetwork: "" // Track which network shows options + property string expandedSsid: "" - onOpened: { - if (Settings.data.network.wifiEnabled) { - NetworkService.refreshNetworks() - } - } + onOpened: NetworkService.scan() panelContent: Rectangle { color: Color.transparent @@ -39,9 +34,9 @@ NPanel { spacing: Style.marginM * scaling NIcon { - text: "wifi" + text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary + color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant } NText { @@ -50,15 +45,21 @@ NPanel { font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true - Layout.leftMargin: Style.marginS * scaling + } + + NToggle { + id: wifiSwitch + checked: Settings.data.network.wifiEnabled + onToggled: checked => NetworkService.setWifiEnabled(checked) + baseSize: Style.baseWidgetSize * 0.7 * scaling } NIconButton { icon: "refresh" - tooltipText: "Refresh networks" + tooltipText: "Refresh" sizeRatio: 0.8 - enabled: Settings.data.network.wifiEnabled - onClicked: NetworkService.refreshNetworks() + enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning + onClicked: NetworkService.scan() } NIconButton { @@ -73,17 +74,18 @@ NPanel { Layout.fillWidth: true } - // Error banner + // Error message Rectangle { - visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 + visible: NetworkService.lastError.length > 0 Layout.fillWidth: true - Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2) + Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2) color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1) radius: Style.radiusS * scaling border.width: Math.max(1, Style.borderS * scaling) border.color: Color.mError RowLayout { + id: errorRow anchors.fill: parent anchors.margins: Style.marginM * scaling spacing: Style.marginS * scaling @@ -95,8 +97,7 @@ NPanel { } NText { - id: errorText - text: NetworkService.connectError + text: NetworkService.lastError color: Color.mError font.pointSize: Style.fontSizeS * scaling wrapMode: Text.Wrap @@ -106,301 +107,287 @@ NPanel { NIconButton { icon: "close" sizeRatio: 0.6 - onClicked: { - NetworkService.connectStatus = "" - NetworkService.connectError = "" - } + onClicked: NetworkService.lastError = "" } } } - ScrollView { + // Main content area + Rectangle { Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - clip: true - contentWidth: availableWidth + color: Color.transparent + // WiFi disabled state ColumnLayout { - width: parent.width - spacing: Style.marginM * scaling + visible: !Settings.data.network.wifiEnabled + anchors.fill: parent + spacing: Style.marginL * scaling - // Loading state - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 - spacing: Style.marginM * scaling + Item { Layout.fillHeight: true } - NBusyIndicator { - running: true - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Scanning for networks..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "wifi_off" + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter } - // WiFi disabled state + NText { + text: "Wi-Fi is disabled" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable Wi-Fi to see available networks" + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { Layout.fillHeight: true } + } + + // Scanning state + ColumnLayout { + visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL * scaling + + Item { Layout.fillHeight: true } + + NBusyIndicator { + running: true + color: Color.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { Layout.fillHeight: true } + } + + // Networks list container + ScrollView { + visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) + anchors.fill: parent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + clip: true + ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: !Settings.data.network.wifiEnabled + width: parent.width spacing: Style.marginM * scaling - NIcon { - text: "wifi_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "WiFi is disabled" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NButton { - text: "Enable WiFi" - icon: "wifi" - Layout.alignment: Qt.AlignHCenter - onClicked: { - Settings.data.network.wifiEnabled = true - Settings.save() - NetworkService.setWifiEnabled(true) + // Network list + Repeater { + model: { + if (!Settings.data.network.wifiEnabled) return [] + + const nets = Object.values(NetworkService.networks) + return nets.sort((a, b) => { + if (a.connected !== b.connected) return b.connected - a.connected + return b.signal - a.signal + }) } - } - } - - // Network list - Repeater { - model: { - if (!Settings.data.network.wifiEnabled || NetworkService.isLoading) - return [] - - // Sort networks: connected first, then by signal strength - const nets = Object.values(NetworkService.networks) - return nets.sort((a, b) => { - if (a.connected && !b.connected) - return -1 - if (!a.connected && b.connected) - return 1 - return b.signal - a.signal - }) - } - - Item { - Layout.fillWidth: true - implicitHeight: networkRect.implicitHeight Rectangle { - id: networkRect - width: parent.width - implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2) + Layout.fillWidth: true + implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, - 0.05) : Color.mSurface + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline - clip: true ColumnLayout { - id: networkContent + id: netColumn width: parent.width - (Style.marginM * scaling * 2) x: Style.marginM * scaling y: Style.marginM * scaling - spacing: Style.marginM * scaling + spacing: Style.marginS * scaling - // Main network row + // Main row RowLayout { Layout.fillWidth: true spacing: Style.marginS * scaling - // Signal icon NIcon { text: NetworkService.signalIcon(modelData.signal) font.pointSize: Style.fontSizeXXL * scaling color: modelData.connected ? Color.mPrimary : Color.mOnSurface } - // Network info ColumnLayout { Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - spacing: 0 + spacing: 2 * scaling NText { - text: modelData.ssid || "Unknown Network" + text: modelData.ssid font.pointSize: Style.fontSizeNormal * scaling font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium - elide: Text.ElideRight color: Color.mOnSurface + elide: Text.ElideRight Layout.fillWidth: true } - NText { - text: { - const security = modelData.security - && modelData.security !== "--" ? modelData.security : "Open" - const signal = `${modelData.signal}%` - return `${signal} • ${security}` - } - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } - } - - // Right-aligned items container - RowLayout { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - spacing: Style.marginS * scaling - - // Connected badge - Rectangle { - visible: modelData.connected - color: Color.mPrimary - radius: width * 0.5 - width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2) - height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2) + RowLayout { + spacing: Style.marginXS * scaling NText { - id: connectedLabel - anchors.centerIn: parent - text: "Connected" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - // Saved badge - clickable - Rectangle { - visible: modelData.cached && !modelData.connected - color: Color.mSurfaceVariant - radius: width * 0.5 - width: savedLabel.implicitWidth + (Style.marginS * scaling * 2) - height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2) - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1) - onExited: parent.color = Color.mSurfaceVariant - onClicked: { - expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid - showPasswordPrompt = false - } - } - - NText { - id: savedLabel - anchors.centerIn: parent - text: "Saved" + text: `${modelData.signal}%` font.pointSize: Style.fontSizeXXS * scaling color: Color.mOnSurfaceVariant } - } - // Loading indicator - NBusyIndicator { - visible: NetworkService.connectingSsid === modelData.ssid - running: NetworkService.connectingSsid === modelData.ssid - color: Color.mPrimary - size: Style.baseWidgetSize * 0.6 * scaling - } + NText { + text: "•" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - // Action buttons - RowLayout { - spacing: Style.marginXS * scaling - visible: NetworkService.connectingSsid !== modelData.ssid + NText { + text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - NButton { - visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt) - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - text: modelData.existing ? "Connect" : (NetworkService.isSecured( - modelData.security) ? "Password" : "Connect") - onClicked: { - if (modelData.existing || !NetworkService.isSecured(modelData.security)) { - NetworkService.connectNetwork(modelData.ssid, modelData.security) - } else { - expandedNetwork = modelData.ssid - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" - Qt.callLater(() => passwordInputField.forceActiveFocus()) - } + Item { + Layout.preferredWidth: Style.marginXXS * scaling + } + + + Rectangle { + visible: modelData.connected + color: Color.mPrimary + radius: height * 0.5 + width: connectedText.implicitWidth + (Style.marginS * scaling * 2) + height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: connectedText + anchors.centerIn: parent + text: "Connected" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary } } - NButton { - visible: modelData.connected - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - backgroundColor: Color.mError - text: "Disconnect" - onClicked: NetworkService.disconnectNetwork(modelData.ssid) + Rectangle { + visible: modelData.cached && !modelData.connected + color: Color.transparent + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: height * 0.5 + width: savedText.implicitWidth + (Style.marginS * scaling * 2) + height: savedText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: savedText + anchors.centerIn: parent + text: "Saved" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } } } } + + // Action area + RowLayout { + spacing: Style.marginXS * scaling + + NBusyIndicator { + visible: NetworkService.connectingTo === modelData.ssid + running: visible + color: Color.mPrimary + size: Style.baseWidgetSize * 0.5 * scaling + } + + NButton { + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid + text: { + if (modelData.existing || modelData.cached) return "Connect" + if (!NetworkService.isSecured(modelData.security)) return "Connect" + return "Password" + } + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + onClicked: { + if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { + NetworkService.connect(modelData.ssid) + } else { + passwordSsid = modelData.ssid + passwordInput = "" + expandedSsid = "" + } + } + } + + NButton { + visible: modelData.connected + text: "Disconnect" + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + backgroundColor: Color.mError + onClicked: NetworkService.disconnect(modelData.ssid) + } + + NIconButton { + visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid + icon: "more_vert" + sizeRatio: 0.7 + onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid + } + } } - // Password input section + // Password input Rectangle { - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + visible: passwordSsid === modelData.ssid Layout.fillWidth: true - implicitHeight: visible ? 50 * scaling : 0 + height: 40 * scaling color: Color.mSurfaceVariant radius: Style.radiusS * scaling RowLayout { anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling + anchors.margins: Style.marginXS * scaling + spacing: Style.marginXS * scaling Rectangle { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusS * scaling + radius: Style.radiusXS * scaling color: Color.mSurface - border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline + border.color: pwdInput.activeFocus ? Color.mPrimary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) TextInput { - id: passwordInputField + id: pwdInput anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - height: parent.height + anchors.margins: Style.marginS * scaling text: passwordInput - font.pointSize: Style.fontSizeM * scaling + font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt - selectByMouse: true echoMode: TextInput.Password - passwordCharacter: "●" + selectByMouse: true + focus: visible onTextChanged: passwordInput = text onAccepted: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" + if (text) { + NetworkService.connect(passwordSsid, text) + passwordSsid = "" + passwordInput = "" } } @@ -416,56 +403,68 @@ NPanel { NButton { text: "Connect" - icon: "check" - fontSize: Style.fontSizeXS * scaling + fontSize: Style.fontSizeXXS * scaling enabled: passwordInput.length > 0 - outlined: !enabled onClicked: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" - } + NetworkService.connect(passwordSsid, passwordInput) + passwordSsid = "" + passwordInput = "" } } NIconButton { icon: "close" - tooltipText: "Cancel" - sizeRatio: 0.9 + sizeRatio: 0.6 onClicked: { - showPasswordPrompt = false - expandedNetwork = "" + passwordSsid = "" passwordInput = "" } } } } - // Forget network option - appears when saved badge is clicked - RowLayout { - visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid - && !showPasswordPrompt + // Options menu + Rectangle { + visible: expandedSsid === modelData.ssid Layout.fillWidth: true - Layout.topMargin: Style.marginXS * scaling - spacing: Style.marginS * scaling + height: forgetRow.implicitHeight + Style.marginS * 2 + color: Color.mSurfaceVariant + radius: Style.radiusS * scaling + border.width: Math.max(1, Style.borderS * scaling) + border.color: Color.mError - Item { - Layout.fillWidth: true - } + RowLayout { + id: forgetRow + anchors.fill: parent + anchors.margins: Style.marginS * scaling - NButton { - id: forgetButton - text: "Forget Network" - icon: "delete_outline" - fontSize: Style.fontSizeXXS * scaling - backgroundColor: Color.mError - textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary - outlined: !forgetButton.hovered - Layout.preferredHeight: 28 * scaling - onClicked: { - NetworkService.forgetNetwork(modelData.ssid) - expandedNetwork = "" + NIcon { + text: "delete_outline" + font.pointSize: Style.fontSizeM * scaling + color: Color.mError + } + + NText { + text: "Forget this network?" + font.pointSize: Style.fontSizeS * scaling + color: Color.mError + Layout.fillWidth: true + } + + NButton { + text: "Forget" + fontSize: Style.fontSizeXXS * scaling + backgroundColor: Color.mError + onClicked: { + NetworkService.forget(modelData.ssid) + expandedSsid = "" + } + } + + NIconButton { + icon: "close" + sizeRatio: 0.6 + onClicked: expandedSsid = "" } } } @@ -473,38 +472,40 @@ NPanel { } } } + } - // No networks found - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 - spacing: Style.marginM * scaling + // Empty state when no networks + ColumnLayout { + visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL * scaling - NIcon { - text: "wifi_find" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + Item { Layout.fillHeight: true } - NText { - text: "No networks found" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NButton { - text: "Refresh" - icon: "refresh" - Layout.alignment: Qt.AlignHCenter - onClicked: NetworkService.refreshNetworks() - } + NIcon { + text: "wifi_find" + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter } + + NText { + text: "No networks found" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NButton { + text: "Scan Again" + icon: "refresh" + Layout.alignment: Qt.AlignHCenter + onClicked: NetworkService.scan() + } + + Item { Layout.fillHeight: true } } } } } -} +} \ No newline at end of file diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index fa34523..4e23c8d 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -8,364 +8,329 @@ import qs.Commons Singleton { id: root - // Core properties + // Core state property var networks: ({}) - property string connectingSsid: "" - property string connectStatus: "" - property string connectStatusSsid: "" - property string connectError: "" - property bool isLoading: false - property bool ethernet: false - property int retryCount: 0 - property int maxRetries: 3 - - // File path for persistent storage + property bool scanning: false + property bool connecting: false + property string connectingTo: "" + property string lastError: "" + + // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" + readonly property string cachedLastConnected: cacheAdapter.lastConnected + readonly property var cachedNetworks: cacheAdapter.knownNetworks - // Stable properties for UI - readonly property alias cache: adapter - readonly property string lastConnectedNetwork: adapter.lastConnected - - // File-based persistent storage + // Cache file handling FileView { id: cacheFileView path: root.cacheFile - onAdapterUpdated: saveTimer.start() - onLoaded: { - Logger.log("Network", "Loaded network cache from disk") - // Try to auto-connect on startup if WiFi is enabled - if (Settings.data.network.wifiEnabled && adapter.lastConnected) { - autoConnectTimer.start() - } - } - onLoadFailed: function (error) { - Logger.log("Network", "No existing cache found, creating new one") - // Initialize with empty data - adapter.knownNetworks = ({}) - adapter.lastConnected = "" - } - + JsonAdapter { - id: adapter + id: cacheAdapter property var knownNetworks: ({}) property string lastConnected: "" - property int lastRefresh: 0 + } + + onLoadFailed: { + cacheAdapter.knownNetworks = ({}) + cacheAdapter.lastConnected = "" } } - // Save timer to batch writes + Component.onCompleted: { + Logger.log("Network", "Service initialized") + syncWifiState() + if (Settings.data.network.wifiEnabled) { + scan() + } + } + + // Save cache with debounce Timer { - id: saveTimer - running: false + id: saveDebounce interval: 1000 onTriggered: cacheFileView.writeAdapter() } - Component.onCompleted: { - Logger.log("Network", "Service started") - - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } - - // Signal strength icon mapping - function signalIcon(signal) { - const levels = [{ - "threshold": 80, - "icon": "network_wifi" - }, { - "threshold": 60, - "icon": "network_wifi_3_bar" - }, { - "threshold": 40, - "icon": "network_wifi_2_bar" - }, { - "threshold": 20, - "icon": "network_wifi_1_bar" - }] - - for (const level of levels) { - if (signal >= level.threshold) - return level.icon - } - return "signal_wifi_0_bar" - } - - function isSecured(security) { - return security && security.trim() !== "" && security.trim() !== "--" - } - - // Enhanced refresh with retry logic - function refreshNetworks() { - if (isLoading) - return - - isLoading = true - retryCount = 0 - adapter.lastRefresh = Date.now() - performRefresh() - } - - function performRefresh() { - checkEthernet.running = true - existingNetworkProcess.running = true - } - - // Retry mechanism for failed operations - function retryRefresh() { - if (retryCount < maxRetries) { - retryCount++ - Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`) - retryTimer.start() - } else { - isLoading = false - connectError = "Failed to refresh networks after multiple attempts" - } + function saveCache() { + saveDebounce.restart() } + // Single refresh timer for periodic scans Timer { - id: retryTimer - interval: 1000 * retryCount // Progressive backoff - repeat: false - onTriggered: performRefresh() + id: refreshTimer + interval: 30000 + running: Settings.data.network.wifiEnabled && !scanning + repeat: true + onTriggered: scan() } + // Delayed scan timer for WiFi enable Timer { - id: autoConnectTimer - interval: 3000 - repeat: false - onTriggered: { - if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { - Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`) - connectToExisting(adapter.lastConnected) - } - } + id: delayedScanTimer + interval: 7000 + onTriggered: scan() } - // Forget network function - function forgetNetwork(ssid) { - Logger.log("Network", `Forgetting network: ${ssid}`) - - // Remove from cache - let known = adapter.knownNetworks - delete known[ssid] - adapter.knownNetworks = known - - // Clear last connected if it's this network - if (adapter.lastConnected === ssid) { - adapter.lastConnected = "" - } - - // Save changes - saveTimer.restart() - - // Remove NetworkManager profile - forgetProcess.ssid = ssid - forgetProcess.running = true + // Core functions + function syncWifiState() { + wifiStateProcess.running = true } - Process { - id: forgetProcess - property string ssid: "" - running: false - command: ["nmcli", "connection", "delete", "id", ssid] - - stdout: StdioCollector { - onStreamFinished: { - Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`) - refreshNetworks() - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - if (text.includes("no such connection profile")) { - Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`) - } else { - Logger.warn("Network", `Error forgetting network: ${text}`) - } - refreshNetworks() - } - } - } - } - - // WiFi enable/disable functions function setWifiEnabled(enabled) { - if (enabled) { - isLoading = true - wifiRadioProcess.action = "on" - wifiRadioProcess.running = true + Settings.data.network.wifiEnabled = enabled + + wifiToggleProcess.action = enabled ? "on" : "off" + wifiToggleProcess.running = true + } + + function scan() { + if (scanning) return + + scanning = true + lastError = "" + scanProcess.running = true + } + + function connect(ssid, password = "") { + if (connecting) return + + connecting = true + connectingTo = ssid + lastError = "" + + // Check if we have a saved connection + if (networks[ssid]?.existing || cachedNetworks[ssid]) { + connectProcess.mode = "saved" + connectProcess.ssid = ssid + connectProcess.password = "" } else { - // Save current connection for later - for (const ssid in networks) { - if (networks[ssid].connected) { - adapter.lastConnected = ssid - saveTimer.restart() - disconnectNetwork(ssid) - break - } - } - - wifiRadioProcess.action = "off" - wifiRadioProcess.running = true + connectProcess.mode = "new" + connectProcess.ssid = ssid + connectProcess.password = password } - } - - // Unified WiFi radio control - Process { - id: wifiRadioProcess - property string action: "on" - running: false - command: ["nmcli", "radio", "wifi", action] - - onRunningChanged: { - if (!running) { - if (action === "on") { - wifiEnableTimer.start() - } else { - root.networks = ({}) - root.isLoading = false - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`) - } - } - } - } - - Timer { - id: wifiEnableTimer - interval: 2000 - repeat: false - onTriggered: { - refreshNetworks() - if (adapter.lastConnected) { - reconnectTimer.start() - } - } - } - - Timer { - id: reconnectTimer - interval: 3000 - repeat: false - onTriggered: { - if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { - connectToExisting(adapter.lastConnected) - } - } - } - - // Connection management - function connectNetwork(ssid, security) { - connectingSsid = ssid - connectStatus = "" - connectStatusSsid = ssid - connectError = "" - - // Check if profile exists - if (networks[ssid]?.existing) { - connectToExisting(ssid) - return - } - - // Check cache for known network - const known = adapter.knownNetworks[ssid] - if (known?.profileName) { - connectToExisting(known.profileName) - return - } - - // New connection - need password for secured networks - if (isSecured(security)) { - // Password will be provided through submitPassword - return - } - - // Open network - connect directly - createAndConnect(ssid, "", security) - } - - function submitPassword(ssid, password) { - const security = networks[ssid]?.security || "" - createAndConnect(ssid, password, security) - } - - function connectToExisting(ssid) { - connectingSsid = ssid - upConnectionProcess.profileName = ssid - upConnectionProcess.running = true - } - - function createAndConnect(ssid, password, security) { - connectingSsid = ssid - - connectProcess.ssid = ssid - connectProcess.password = password - connectProcess.isSecured = isSecured(security) + connectProcess.running = true } - function disconnectNetwork(ssid) { + function disconnect(ssid) { disconnectProcess.ssid = ssid disconnectProcess.running = true } - // Connection process - Process { - id: connectProcess - property string ssid: "" - property string password: "" - property bool isSecured: false - running: false - - command: { - const cmd = ["nmcli", "device", "wifi", "connect", ssid] - if (isSecured && password) { - cmd.push("password", password) - } - return cmd + function forget(ssid) { + // Remove from cache + let known = cacheAdapter.knownNetworks + delete known[ssid] + cacheAdapter.knownNetworks = known + + if (cacheAdapter.lastConnected === ssid) { + cacheAdapter.lastConnected = "" } + + saveCache() + + // Remove from system + forgetProcess.ssid = ssid + forgetProcess.running = true + } + // Helper functions + function signalIcon(signal) { + if (signal >= 80) return "network_wifi" + if (signal >= 60) return "network_wifi_3_bar" + if (signal >= 40) return "network_wifi_2_bar" + if (signal >= 20) return "network_wifi_1_bar" + return "signal_wifi_0_bar" + } + + function isSecured(security) { + return security && security !== "--" && security.trim() !== "" + } + + // Processes + Process { + id: wifiStateProcess + running: false + command: ["nmcli", "radio", "wifi"] + stdout: StdioCollector { onStreamFinished: { - handleConnectionSuccess(connectProcess.ssid) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - handleConnectionError(connectProcess.ssid, text) + const enabled = text.trim() === "enabled" + if (Settings.data.network.wifiEnabled !== enabled) { + Settings.data.network.wifiEnabled = enabled } } } } Process { - id: upConnectionProcess - property string profileName: "" + id: wifiToggleProcess + property string action: "on" running: false - command: ["nmcli", "connection", "up", "id", profileName] - - stdout: StdioCollector { - onStreamFinished: { - handleConnectionSuccess(upConnectionProcess.profileName) + command: ["nmcli", "radio", "wifi", action] + + onRunningChanged: { + if (!running) { + if (action === "on") { + // Clear networks immediately and start delayed scan + root.networks = ({}) + delayedScanTimer.restart() + } else { + root.networks = ({}) + } } } - + stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - handleConnectionError(upConnectionProcess.profileName, text) + Logger.warn("Network", "WiFi toggle error: " + text) + } + } + } + } + + Process { + id: scanProcess + running: false + command: ["sh", "-c", ` + # Get existing profiles + profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1) + + # Get WiFi networks + nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do + ssid=$(echo "$line" | cut -d: -f1) + security=$(echo "$line" | cut -d: -f2) + signal=$(echo "$line" | cut -d: -f3) + in_use=$(echo "$line" | cut -d: -f4) + + # Skip empty SSIDs + if [ -z "$ssid" ]; then + continue + fi + + existing=false + if echo "$profiles" | grep -q "^$ssid$"; then + existing=true + fi + + echo "$ssid|$security|$signal|$in_use|$existing" + done + `] + + stdout: StdioCollector { + onStreamFinished: { + const nets = {} + const lines = text.split("\n").filter(l => l.trim()) + + for (const line of lines) { + const parts = line.split("|") + if (parts.length < 5) continue + + const ssid = parts[0] + if (!ssid || ssid.trim() === "") continue + + const network = { + ssid: ssid, + security: parts[1] || "--", + signal: parseInt(parts[2]) || 0, + connected: parts[3] === "*", + existing: parts[4] === "true", + cached: ssid in cacheAdapter.knownNetworks + } + + // Track connected network + if (network.connected && cacheAdapter.lastConnected !== ssid) { + cacheAdapter.lastConnected = ssid + saveCache() + } + + // Keep best signal for duplicate SSIDs + if (!nets[ssid] || network.signal > nets[ssid].signal) { + nets[ssid] = network + } + } + + root.networks = nets + root.scanning = false + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.scanning = false + if (text.trim()) { + Logger.warn("Network", "Scan error: " + text) + // If scan fails, set a short retry + if (Settings.data.network.wifiEnabled) { + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + } + } + } + + Process { + id: connectProcess + property string mode: "new" + property string ssid: "" + property string password: "" + running: false + + command: { + if (mode === "saved") { + return ["nmcli", "connection", "up", "id", ssid] + } else { + const cmd = ["nmcli", "device", "wifi", "connect", ssid] + if (password) { + cmd.push("password", password) + } + return cmd + } + } + + stdout: StdioCollector { + onStreamFinished: { + // Success - update cache + let known = cacheAdapter.knownNetworks + known[connectProcess.ssid] = { + profileName: connectProcess.ssid, + lastConnected: Date.now() + } + cacheAdapter.knownNetworks = known + cacheAdapter.lastConnected = connectProcess.ssid + saveCache() + + root.connecting = false + root.connectingTo = "" + Logger.log("Network", "Connected to " + connectProcess.ssid) + + // Rescan to update status + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.connecting = false + root.connectingTo = "" + + if (text.trim()) { + // Parse common errors + if (text.includes("Secrets were required") || text.includes("no secrets provided")) { + root.lastError = "Incorrect password" + } else if (text.includes("No network with SSID")) { + root.lastError = "Network not found" + } else if (text.includes("Timeout")) { + root.lastError = "Connection timeout" + } else { + root.lastError = text.split("\n")[0].trim() + } + + Logger.warn("Network", "Connect error: " + text) } } } @@ -376,222 +341,26 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "down", "id", ssid] - + onRunningChanged: { if (!running) { - connectingSsid = "" - connectStatus = "" - connectStatusSsid = "" - connectError = "" - refreshNetworks() - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", `Disconnect warning: ${text}`) - } - } - } - } - - // Connection result handlers - function handleConnectionSuccess(ssid) { - connectingSsid = "" - connectStatus = "success" - connectStatusSsid = ssid - connectError = "" - - // Update cache - let known = adapter.knownNetworks - known[ssid] = { - "profileName": ssid, - "lastConnected": Date.now(), - "autoConnect": true - } - adapter.knownNetworks = known - adapter.lastConnected = ssid - saveTimer.restart() - - Logger.log("Network", `Successfully connected to ${ssid}`) - refreshNetworks() - } - - function handleConnectionError(ssid, error) { - connectingSsid = "" - connectStatus = "error" - connectStatusSsid = ssid - connectError = parseError(error) - - Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`) - } - - function parseError(error) { - // Simplify common error messages - if (error.includes("Secrets were required") || error.includes("no secrets provided")) { - return "Incorrect password" - } - if (error.includes("No network with SSID")) { - return "Network not found" - } - if (error.includes("Connection activation failed")) { - return "Connection failed. Please try again." - } - if (error.includes("Timeout")) { - return "Connection timeout. Network may be out of range." - } - // Return first line only - return error.split("\n")[0].trim() - } - - // Network scanning processes - Process { - id: existingNetworkProcess - running: false - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - - stdout: StdioCollector { - onStreamFinished: { - const profiles = {} - const lines = text.split("\n").filter(l => l.trim()) - - for (const line of lines) { - const parts = line.split(":") - const name = parts[0] - const type = parts[1] - if (name && type === "802-11-wireless") { - profiles[name] = { - "ssid": name, - "type": type - } - } - } - - scanProcess.existingProfiles = profiles - scanProcess.running = true - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error listing connections:", text) - retryRefresh() - } + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() } } } Process { - id: scanProcess - property var existingProfiles: ({}) + id: forgetProcess + property string ssid: "" running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - - stdout: StdioCollector { - onStreamFinished: { - const networksMap = {} - const lines = text.split("\n").filter(l => l.trim()) - - for (const line of lines) { - const parts = line.split(":") - if (parts.length < 4) - continue - - const ssid = parts[0] - const security = parts[1] - const signalStr = parts[2] - const inUse = parts[3] - if (!ssid) - continue - - const signal = parseInt(signalStr) || 0 - const connected = inUse === "*" - - // Update last connected if we find the connected network - if (connected && adapter.lastConnected !== ssid) { - adapter.lastConnected = ssid - saveTimer.restart() - } - - // Merge with existing or create new - if (!networksMap[ssid] || signal > networksMap[ssid].signal) { - networksMap[ssid] = { - "ssid": ssid, - "security": security || "--", - "signal": signal, - "connected": connected, - "existing": ssid in scanProcess.existingProfiles, - "cached": ssid in adapter.knownNetworks - } - } - } - - root.networks = networksMap - root.isLoading = false - scanProcess.existingProfiles = {} - - //Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error scanning networks:", text) - retryRefresh() - } + command: ["nmcli", "connection", "delete", "id", ssid] + + onRunningChanged: { + if (!running) { + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() } } } - - Process { - id: checkEthernet - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - - stdout: StdioCollector { - onStreamFinished: { - root.ethernet = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" && parts[2] === "connected" - }) - } - } - } - - // Auto-refresh timer - Timer { - interval: 30000 // 30 seconds - running: Settings.data.network.wifiEnabled && !isLoading - repeat: true - onTriggered: { - // Only refresh if we should - const now = Date.now() - const timeSinceLastRefresh = now - adapter.lastRefresh - - // Refresh if: connected, or it's been more than 30 seconds - if (hasActiveConnection || timeSinceLastRefresh > 30000) { - refreshNetworks() - } - } - } - - property bool hasActiveConnection: { - return Object.values(networks).some(net => net.connected) - } - - // Menu state management - function onMenuOpened() { - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } - - function onMenuClosed() { - // Clean up temporary states - connectStatus = "" - connectError = "" - } -} +} \ No newline at end of file