pragma ComponentBehavior: Bound import QtQuick import "Util.js" as Util import "sysinfo.js" as Sys Column { id: root required property Theme theme readonly property Flickable scrollView: networkScroll required property Resources resourceService required property Network networkService required property Bluetooth bluetoothService property var networkState: networkService ? Util.parseJson(networkService.stateJson(), { "networks": [], "knownNetworks": [], "wifi": {}, "compact": {}, "pending": false, "error": "" }) : ({ "networks": [], "knownNetworks": [], "wifi": {}, "compact": {}, "pending": false, "error": "" }) property var bluetoothState: bluetoothService ? Util.parseJson(bluetoothService.stateJson(), { "devices": [], "adapter": {} }) : ({ "devices": [], "adapter": {} }) // ssid whose inline passphrase field is currently revealed ("" = none). // Holds only an ssid (not a secret); the passphrase lives solely in the // TextInput.text and is cleared on submit, never bound into networkState. property string expandedSsid: "" function refreshNetwork(text) { networkState = Util.parseJson(text, networkState); } function refreshBluetooth(text) { bluetoothState = Util.parseJson(text, bluetoothState); } function formatRate(value) { return Sys.formatRate(value); } function toggleWifi() { if (networkService) refreshNetwork(networkService.toggleWifi(!(networkState.wifi && networkState.wifi.enabled))); } function scanWifi() { if (networkService) refreshNetwork(networkService.scan()); } function selectNetwork(id) { if (networkService) refreshNetwork(networkService.select(id)); } function forgetNetwork(id) { if (networkService) refreshNetwork(networkService.forget(id)); } // Secured-connect for a new network: connect(ssid, passphrase) is the // in-process entry (NOT an IPC handler). The passphrase is a transient // argument only — never stored on root / networkState / any logged field. function connectSecured(ssid, passphrase) { if (networkService) refreshNetwork(networkService.connect(ssid, passphrase)); } function disconnectNetwork() { if (networkService) refreshNetwork(networkService.disconnect()); } // Toggle a row's detail drawer. Opening the CONNECTED row triggers a // lazy StationDiagnostic read (bssid/band/rate) that fills in async. function expandRow(row) { const willOpen = expandedSsid !== row.ssid; expandedSsid = willOpen ? row.ssid : ""; if (willOpen && row.connected && networkService) refreshNetwork(networkService.diagnostics()); } function toggleBluetooth() { if (bluetoothService) refreshBluetooth(bluetoothService.toggle(!(bluetoothState.adapter && bluetoothState.adapter.state === "powered"))); } function scanBluetooth() { if (bluetoothService) refreshBluetooth(bluetoothService.scan()); } function bluetoothAction(kind, id) { if (bluetoothService) refreshBluetooth(bluetoothService.deviceAction(kind, id)); } function bluetoothDeviceIcon(type) { const t = String(type || ""); if (t.indexOf("gaming") >= 0) return "\uF11B"; if (t.indexOf("headset") >= 0 || t.indexOf("headphone") >= 0) return "\uF025"; if (t.indexOf("card") >= 0 || t.indexOf("speaker") >= 0 || t.indexOf("audio") >= 0) return "\uF028"; if (t.indexOf("keyboard") >= 0) return "\uF11C"; return "\uF293"; } function bluetoothBatteryLabel(device) { return device && device.battery >= 0 ? "\uF240 " + device.battery + "%" : "󰂑"; } function networkFeedback() { if (networkState.pending) return "network action pending…"; if (networkState.scanning) return "scanning for networks…"; const error = String(networkState.error || ""); if (error === "no-wifi-device") return "no wireless device"; if (error === "scan-failed") return "network scan failed"; if (error === "action-unavailable") return "network action unavailable"; if (error.length > 0) return "network action failed"; return ""; } function bluetoothFeedback() { if (bluetoothState.adapter && bluetoothState.adapter.discovering) return "scanning for bluetooth devices…"; return String(bluetoothState.error || "").length > 0 ? "bluetooth action failed" : ""; } function roleColor(role) { if (!theme) return "white"; if (role === "warning") return theme.warning; if (role === "error") return theme.error; if (role === "ui.minor") return theme.ui.minor; if (role === "content.accent") return theme.content.accent; return theme.ui.accent; } spacing: 10 width: parent ? parent.width : 0 StableListModel { id: wifiRows rows: root.networkState.networks || [] } // Async reads (scan retries, post-action refresh) update the service // model and emit refreshed(); re-pull stateJson so the scan spinner // clears and new/connected rows land without a user action. Connections { target: root.networkService function onRefreshed() { if (root.networkService) root.refreshNetwork(root.networkService.stateJson()); } } Connections { target: root.bluetoothService function onRefreshed() { if (root.bluetoothService) root.refreshBluetooth(root.bluetoothService.stateJson()); } } // One scroll pane for the whole panel: a long Wi-Fi scan + the // Bluetooth list scroll together, and an expanded row's detail drawer // grows inside it without clipping. Idle (no overflow) => not interactive. Flickable { id: networkScroll width: parent.width height: root.height contentWidth: width contentHeight: panelCol.height clip: true interactive: contentHeight > height boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick Column { id: panelCol width: parent.width spacing: 10 Row { objectName: "networkRateRow" width: parent.width spacing: 8 NuguText { theme: root.theme text: root.resourceService ? `↓ ${root.formatRate(root.resourceService.networkRxRate)}` : "↓ 0 B/s" color: root.theme ? root.theme.ui.accent : "white" } NuguText { theme: root.theme text: root.resourceService ? `↑ ${root.formatRate(root.resourceService.networkTxRate)}` : "↑ 0 B/s" color: root.theme ? root.theme.ui.focus : "white" } } NuguText { visible: root.networkFeedback().length > 0 width: parent.width theme: root.theme variant: "meta" text: root.networkFeedback() color: root.networkState.error ? root.roleColor("error") : root.roleColor("ui.minor") wrapMode: Text.WordWrap } Column { objectName: "wifiToggleScanList" width: parent.width spacing: 6 Row { width: parent.width spacing: 6 NuguText { theme: root.theme variant: "title" text: "wireless" width: parent.width - 132 } PopupButton { theme: root.theme width: 58 height: 26 text: root.networkState.wifi && root.networkState.wifi.enabled ? "on" : "off" checked: root.networkState.wifi && root.networkState.wifi.enabled onClicked: root.toggleWifi() } PopupButton { theme: root.theme width: 58 height: 26 text: root.networkState.scanning ? "scan…" : "scan" checked: root.networkState.scanning onClicked: root.scanWifi() } } Column { width: parent.width spacing: 6 Repeater { model: wifiRows delegate: Column { id: wifiRow required property var rowData readonly property bool active: rowData.connected === true readonly property bool revealed: root.expandedSsid === rowData.ssid onRevealedChanged: if (!revealed) wifiPp.text = "" readonly property var detail: root.networkState.connectionDetail || ({}) width: parent.width spacing: 4 PopupCard { theme: root.theme padding: 0 active: wifiRow.active revealed: wifiRow.revealed attention: wifiRow.rowData.state === "connecting" interactive: true accessibleName: wifiRow.revealed ? `collapse ${wifiRow.rowData.ssid}` : `expand ${wifiRow.rowData.ssid}` tooltip: accessibleName width: parent.width height: 44 onClicked: root.expandRow(wifiRow.rowData) Text { id: wifiGlyph anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter font.family: "Symbols Nerd Font Mono" font.pixelSize: 18 text: wifiRow.rowData.signalGlyph || "" color: root.roleColor(wifiRow.rowData.signalRole || "ui.minor") } // chevron hints the row expands; rotates when open Text { id: wifiChevron anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter font.family: "Symbols Nerd Font Mono" font.pixelSize: 16 text: "" rotation: wifiRow.revealed ? 90 : 0 color: root.roleColor(wifiRow.active ? "content.accent" : "ui.minor") } NuguText { id: wifiLabel anchors.right: wifiChevron.left anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "meta" text: wifiRow.active ? "connected" : (wifiRow.rowData.securityLabel || "") color: root.roleColor(wifiRow.active ? "content.accent" : "ui.minor") } NuguText { anchors.left: wifiGlyph.right anchors.leftMargin: 10 anchors.right: wifiLabel.left anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter theme: root.theme text: wifiRow.rowData.ssid elide: Text.ElideRight } } // --- detail + actions drawer (revealed on tap) ----------- PopupCard { id: wifiDrawer visible: wifiRow.revealed theme: root.theme padding: 0 revealed: true width: parent.width height: visible ? drawerCol.height + 16 : 0 Column { id: drawerCol x: 12 y: 8 width: parent.width - 24 spacing: 6 // signal + security — available for every row NuguText { theme: root.theme variant: "meta" text: { const dbm = wifiRow.rowData.signalDbm; const sig = (dbm === null || dbm === undefined) ? `${wifiRow.rowData.signal}/4 bars` : `${dbm} dBm`; return `${wifiRow.rowData.securityLabel || "open"} · ${sig}`; } color: root.theme ? root.theme.content.normal : "white" } // connected-only BSS detail (StationDiagnostic) Column { visible: wifiRow.active && !!wifiRow.detail.bssid width: parent.width spacing: 2 NuguText { theme: root.theme variant: "meta" text: `${wifiRow.detail.band || "?"}${wifiRow.detail.channel ? " · ch " + wifiRow.detail.channel : ""}${wifiRow.detail.mode ? " · " + wifiRow.detail.mode : ""}` opacity: 0.8 } NuguText { theme: root.theme variant: "meta" visible: !!wifiRow.detail.rxRate || !!wifiRow.detail.txRate text: `↓ ${wifiRow.detail.rxRate || "?"} ↑ ${wifiRow.detail.txRate || "?"}` opacity: 0.8 } NuguText { theme: root.theme variant: "meta" text: wifiRow.detail.bssid || "" opacity: 0.6 } } // action pills — hidden when a passphrase is needed Row { visible: !wifiRow.rowData.needsPassphrase spacing: 8 PopupButton { visible: !wifiRow.active theme: root.theme width: 96 height: 28 text: "connect" role: "primary" onClicked: { root.expandedSsid = ""; root.selectNetwork(wifiRow.rowData.id); } } PopupButton { visible: wifiRow.active theme: root.theme width: 96 height: 28 text: "disconnect" role: "primary" onClicked: { root.expandedSsid = ""; root.disconnectNetwork(); } } PopupButton { visible: wifiRow.active || wifiRow.rowData.known theme: root.theme width: 96 height: 28 text: "forget" role: "destructive" onClicked: { root.expandedSsid = ""; root.forgetNetwork(wifiRow.rowData.id); } } } // --- inline passphrase entry for a NEW secured network. // echoMode Password; text lives only here, cleared on // submit, never bound into networkState / any logged field. Item { id: wifiPpRow function submit() { root.connectSecured(wifiRow.rowData.ssid, wifiPp.text); wifiPp.text = ""; root.expandedSsid = ""; } visible: wifiRow.rowData.needsPassphrase width: parent.width height: visible ? 38 : 0 onVisibleChanged: { if (visible && wifiRow.revealed) Qt.callLater(wifiPp.input.forceActiveFocus); else wifiPp.text = ""; } PopupTextField { id: wifiPp anchors.left: parent.left anchors.right: wifiPpConfirm.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter theme: root.theme placeholderText: "passphrase" echoMode: TextInput.Password onAccepted: wifiPpRow.submit() onCancelled: { text = ""; root.expandedSsid = ""; } } PopupButton { id: wifiPpConfirm anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter theme: root.theme width: 64 height: 32 text: "join" role: "primary" onClicked: wifiPpRow.submit() } } } } } } } NuguText { visible: !!(root.networkState.wifi && root.networkState.wifi.enabled) && (!root.networkState.networks || root.networkState.networks.length === 0) theme: root.theme variant: "meta" text: root.networkState.scanning ? "scanning…" : "no networks" opacity: 0.7 } } NuguText { visible: root.bluetoothFeedback().length > 0 width: parent.width theme: root.theme variant: "meta" text: root.bluetoothFeedback() color: root.bluetoothState.error ? root.roleColor("error") : root.roleColor("ui.minor") wrapMode: Text.WordWrap } PopupDivider { theme: root.theme width: parent.width } Column { objectName: "bluetoothToggleScanList" width: parent.width spacing: 6 Row { width: parent.width spacing: 6 NuguText { theme: root.theme variant: "title" text: "bluetooth" width: parent.width - 132 } PopupButton { theme: root.theme width: 58 height: 26 text: root.bluetoothState.adapter && root.bluetoothState.adapter.state === "powered" ? "on" : "off" checked: root.bluetoothState.adapter && root.bluetoothState.adapter.state === "powered" onClicked: root.toggleBluetooth() } PopupButton { theme: root.theme width: 58 height: 26 text: root.bluetoothState.adapter && root.bluetoothState.adapter.discovering ? "scan…" : "scan" checked: root.bluetoothState.adapter && root.bluetoothState.adapter.discovering onClicked: root.scanBluetooth() } } Column { width: parent.width spacing: 6 Row { id: bluetoothHeader width: parent.width height: 22 spacing: 6 Item { width: 34 height: 1 } NuguText { theme: root.theme variant: "meta" text: "device" width: bluetoothHeader.width - 34 - 78 - 84 - 96 - 68 - bluetoothHeader.spacing * 5 opacity: 0.75 } NuguText { theme: root.theme variant: "meta" text: "trust" width: 78 horizontalAlignment: Text.AlignHCenter opacity: 0.75 } NuguText { theme: root.theme variant: "meta" text: "state" width: 84 horizontalAlignment: Text.AlignHCenter opacity: 0.75 } NuguText { theme: root.theme variant: "meta" text: "battery" width: 96 horizontalAlignment: Text.AlignRight opacity: 0.75 } NuguText { theme: root.theme variant: "meta" text: "actions" width: 68 horizontalAlignment: Text.AlignHCenter opacity: 0.75 } } PopupDivider { theme: root.theme width: parent.width } Repeater { model: root.bluetoothState.devices || [] delegate: PopupCard { id: deviceRow required property var modelData theme: root.theme padding: 0 active: modelData.connected destructive: modelData.blocked interactive: true accessibleName: modelData.connected ? `disconnect ${modelData.name}` : `connect ${modelData.name}` tooltip: accessibleName width: root.width height: 46 onClicked: root.bluetoothAction(modelData.connected ? "disconnect" : "connect", modelData.id) Row { id: deviceLine anchors.fill: parent anchors.leftMargin: 8 anchors.rightMargin: 8 spacing: 6 Text { width: 26 anchors.verticalCenter: parent.verticalCenter font.family: "Symbols Nerd Font Mono" font.pixelSize: 17 color: root.roleColor(deviceRow.modelData.connected ? "ui.accent" : "ui.minor") text: root.bluetoothDeviceIcon(deviceRow.modelData.type) horizontalAlignment: Text.AlignHCenter } NuguText { anchors.verticalCenter: parent.verticalCenter theme: root.theme text: deviceRow.modelData.name width: deviceLine.width - 26 - 78 - 84 - 96 - 68 - deviceLine.spacing * 5 elide: Text.ElideRight } PopupBadge { anchors.verticalCenter: parent.verticalCenter theme: root.theme width: 78 text: deviceRow.modelData.trusted ? "trusted" : "untrusted" icon: deviceRow.modelData.trusted ? "✓" : "" role: deviceRow.modelData.trusted ? "active" : "normal" } PopupBadge { anchors.verticalCenter: parent.verticalCenter theme: root.theme width: 84 text: deviceRow.modelData.connected ? "linked" : "off" icon: deviceRow.modelData.connected ? "●" : "○" role: deviceRow.modelData.connected ? "active" : "normal" } NuguText { objectName: "batteryBadges" anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "mono" width: 96 horizontalAlignment: Text.AlignRight font.family: "Symbols Nerd Font Mono" color: root.roleColor(deviceRow.modelData.battery >= 0 ? "content.accent" : "ui.minor") text: root.bluetoothBatteryLabel(deviceRow.modelData) } Row { anchors.verticalCenter: parent.verticalCenter width: 68 spacing: 4 PopupButton { theme: root.theme width: 32 height: 28 role: "ghost" showText: false icon: deviceRow.modelData.trusted ? "󰌊" : "󰌋" accessibleName: deviceRow.modelData.trusted ? "block device" : "trust device" tooltip: accessibleName onClicked: root.bluetoothAction(deviceRow.modelData.trusted ? "block" : "trust", deviceRow.modelData.id) } PopupButton { theme: root.theme width: 32 height: 28 role: "ghost" showText: false icon: "󰆴" accessibleName: "forget device" tooltip: accessibleName onClicked: root.bluetoothAction("forget", deviceRow.modelData.id) } } } MouseArea { anchors.fill: parent acceptedButtons: Qt.MiddleButton | Qt.RightButton propagateComposedEvents: true onClicked: event => { if (event.button === Qt.MiddleButton) root.bluetoothAction("forget", deviceRow.modelData.id); else root.bluetoothAction(deviceRow.modelData.trusted ? "block" : "trust", deviceRow.modelData.id); } } } } } } } } }