import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Networking import "../../config" as Cfg import "../../components" ModuleChip { id: root readonly property var wifiDevice: { for (const dev of (Networking.devices?.values ?? [])) if (dev.type === DeviceType.Wifi) return dev return null } readonly property var wiredDevice: { for (const dev of (Networking.devices?.values ?? [])) if (dev.type === DeviceType.Wired) return dev return null } readonly property bool hasWifi: wifiDevice !== null readonly property bool hasWired: wiredDevice !== null readonly property bool wifiUp: hasWifi && wifiDevice.connected readonly property bool wiredUp: hasWired && wiredDevice.connected readonly property string connectivityLabel: { switch (Networking.connectivity) { case NetworkConnectivity.Portal: return "Captive Portal" case NetworkConnectivity.Limited: return "Limited" case NetworkConnectivity.None: return "No Internet" default: return "" } } readonly property string wifiModeLabel: { if (!wifiUp || !wifiDevice) return "" switch (wifiDevice.mode) { case WifiDeviceMode.Infra: return "Infra" case WifiDeviceMode.Ap: return "AP" case WifiDeviceMode.Adhoc: return "Ad-Hoc" case WifiDeviceMode.Mesh: return "Mesh" default: return "" } } readonly property var activeAP: { for (const n of (wifiDevice?.networks?.values ?? [])) if (n.connected) return n return null } readonly property string ssid: activeAP ? (activeAP.name || "") : "" readonly property int strength: activeAP ? Math.round((activeAP.signalStrength || 0) * 100) : 0 readonly property string wifiIcon: { if (!wifiUp) return "󰤭" if (strength >= 80) return "󰤨" if (strength >= 60) return "󰤥" if (strength >= 40) return "󰤢" if (strength >= 20) return "󰤟" return "󰤯" } readonly property string displayIcon: hasWifi ? wifiIcon : "" readonly property color iconColor: { if (hasWifi) return wifiUp ? t.statusOk : t.statusErr if (hasWired) return wiredUp ? t.statusOk : t.statusErr return t.textDim } readonly property string chipLabel: { if (hasWifi) return (wifiUp && ssid !== "") ? ssid : "Wi-Fi" if (hasWired) return wiredUp ? "Ethernet" : "Offline" return "No NIC" } property var pendingAP: null property string pendingSSID: "" property bool showPwdPrompt: false property string enteredPwd: "" function attemptConnect() { if (!pendingAP) return if (enteredPwd.length > 0) pendingAP.connectWithPsk(enteredPwd) else pendingAP.connect() pendingAP = null pendingSSID = "" enteredPwd = "" showPwdPrompt = false } function cancelConnect() { pendingAP = null pendingSSID = "" enteredPwd = "" showPwdPrompt = false } implicitWidth: chipRow.implicitWidth + 24 height: Cfg.Config.moduleHeight radius: panelRadius color: chipHover.hovered ? t.netBgHover : t.netBg border.color: chipHover.hovered ? t.netBorderHover : t.netBorder border.width: borderWidth Behavior on implicitWidth { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } HoverHandler { id: chipHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: netPopup.toggle() } RowLayout { id: chipRow anchors.centerIn: parent spacing: 6 Text { text: root.displayIcon color: root.iconColor font.pixelSize: 16 Layout.alignment: Qt.AlignVCenter SequentialAnimation on opacity { running: root.hasWifi && root.wifiDevice && (root.wifiDevice.scannerEnabled || false) loops: Animation.Infinite NumberAnimation { to: 0.4; duration: 600; easing.type: Easing.InOutSine } NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutSine } } opacity: 1.0 } Text { text: root.chipLabel color: root.t.netText font.pixelSize: 12 font.bold: true verticalAlignment: Text.AlignVCenter Layout.maximumWidth: 110 elide: Text.ElideRight } } PopupPanel { id: netPopup ns: "network" chipRoot: root cardWidth: 310 function open() { root.cancelConnect() _doOpen() if (root.hasWifi && root.wifiDevice) root.wifiDevice.scannerEnabled = true } PopupCard { popupOpen: netPopup.popupOpen isTop: root.isTop animEnabled: netPopup.visible height: cardBody.implicitHeight + 24 color: root.t.netPopupBg radius: Cfg.Config.popupRadius border.color: root.t.netPopupBorder border.width: Cfg.Config.popupBorderWidth layer.enabled: true ColumnLayout { id: cardBody anchors { left: parent.left right: parent.right top: parent.top margins: 16 topMargin: 18 } spacing: 12 RowLayout { Layout.fillWidth: true spacing: 12 Text { text: root.displayIcon color: root.iconColor font.pixelSize: 36 Layout.alignment: Qt.AlignVCenter } ColumnLayout { spacing: 2 Layout.fillWidth: true Text { text: { if (root.hasWifi) return root.wifiUp ? (root.ssid !== "" ? root.ssid : "Connected") : "Wi-Fi" if (root.hasWired) return root.wiredUp ? "Ethernet" : "Disconnected" return "No Network" } color: root.t.netPopupHeader font.pixelSize: 17 font.bold: true elide: Text.ElideRight Layout.fillWidth: true } Text { text: { if (root.hasWifi && root.wifiDevice) { var st = root.wifiDevice.state if (st === ConnectionState.Activating) return "Connecting..." if (st === ConnectionState.Deactivating) return "Disconnecting..." if (root.wifiUp && root.activeAP) { var s = root.strength + "% signal" if (root.wifiModeLabel !== "") s += " · " + root.wifiModeLabel return s } if (st === ConnectionState.Deactivated) return "Disconnected" return "Not connected" } if (root.hasWired && root.wiredDevice) return root.wiredDevice.name || "—" return "No hardware found" } color: root.t.netPopupDim font.pixelSize: 12 } Text { visible: root.connectivityLabel !== "" text: root.connectivityLabel color: root.t.statusErr font.pixelSize: 11 } } Rectangle { visible: root.hasWifi width: 28; height: 28 radius: 8 color: scanHover.hovered ? root.t.netApHoverBg : "transparent" border.color: root.t.netPopupBorder border.width: 1 Behavior on color { ColorAnimation { duration: 120 } } Text { anchors.centerIn: parent text: "\uf0450" font.pixelSize: 15 color: scanHover.hovered ? root.t.accent : root.t.netPopupDim RotationAnimator on rotation { id: scanSpin; running: false from: 0; to: 360; duration: 700; loops: 2 easing.type: Easing.InOutCubic } } HoverHandler { id: scanHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: { scanSpin.restart() if (root.wifiDevice) root.wifiDevice.scannerEnabled = true } } } } Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator } ColumnLayout { visible: root.hasWifi Layout.fillWidth: true spacing: 4 Text { text: "AVAILABLE NETWORKS" color: root.t.netPopupDim font.pixelSize: 10 font.letterSpacing: 1.2 Layout.fillWidth: true } Text { visible: root.hasWifi && root.wifiDevice && root.wifiDevice.networks.values.length === 0 text: (root.wifiDevice && (root.wifiDevice.scannerEnabled || false)) ? "Scanning..." : "No networks found" color: root.t.netPopupDim font.pixelSize: 12 Layout.alignment: Qt.AlignHCenter Layout.topMargin: 4 } Repeater { model: (root.hasWifi && root.wifiDevice) ? root.wifiDevice.networks.values : [] delegate: Rectangle { required property var modelData Layout.fillWidth: true height: 50 radius: 10 color: { var active = modelData.connected || false if (active) return root.t.netApActiveBg if (apHover.hovered) return root.t.netApHoverBg return "transparent" } border.color: (modelData.connected || false) ? root.t.accent : "transparent" border.width: 1 Behavior on color { ColorAnimation { duration: 120 } } Behavior on border.color { ColorAnimation { duration: 120 } } HoverHandler { id: apHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: { if (modelData.connected || false) return if (modelData.security !== WifiSecurityType.None) { root.pendingAP = modelData root.pendingSSID = modelData.name || "" root.enteredPwd = "" root.showPwdPrompt = true } else { modelData.connect() } } } RowLayout { anchors { fill: parent; leftMargin: 12; rightMargin: 12 } spacing: 10 Text { text: { var s = Math.round((modelData.signalStrength || 0) * 100) if (s >= 80) return "\uf0ec8" if (s >= 60) return "\uf0ec5" if (s >= 40) return "\uf0ec2" if (s >= 20) return "\uf0ebf" return "\uf0eef" } color: (modelData.connected || false) ? root.t.accent : root.t.netPopupDim font.pixelSize: 18 Layout.alignment: Qt.AlignVCenter } ColumnLayout { spacing: 2 Layout.fillWidth: true Text { text: modelData.name || "(hidden)" color: (modelData.connected || false) ? root.t.netPopupHeader : root.t.netPopupText font.pixelSize: 13 font.bold: (modelData.connected || false) elide: Text.ElideRight Layout.fillWidth: true } Text { text: { if (modelData.stateChanging) return modelData.state === ConnectionState.Activating ? "Connecting..." : "Disconnecting..." var parts = [] if (modelData.known) parts.push("Saved") if (modelData.security !== WifiSecurityType.None) parts.push("Secured") else parts.push("Open") parts.push(Math.round((modelData.signalStrength || 0) * 100) + "%") return parts.join(" · ") } color: root.t.netPopupDim font.pixelSize: 11 } } Text { visible: (modelData.security !== WifiSecurityType.None) && !(modelData.connected || false) text: "\uf033e" color: root.t.netPopupDim font.pixelSize: 13 Layout.alignment: Qt.AlignVCenter } Text { visible: (modelData.connected || false) text: "\uf012c" color: root.t.statusOk font.pixelSize: 15 Layout.alignment: Qt.AlignVCenter } } } } Rectangle { visible: root.wifiUp Layout.fillWidth: true Layout.topMargin: 4 height: 36; radius: 10 color: discoHover.hovered ? root.t.netDisconnectBg : "transparent" border.color: root.t.netDisconnectBorder border.width: 1 Behavior on color { ColorAnimation { duration: 120 } } HoverHandler { id: discoHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: { if (root.activeAP) root.activeAP.disconnect() } } Text { anchors.centerIn: parent text: "Disconnect" color: root.t.statusErr font.pixelSize: 12 font.bold: true } } } ColumnLayout { visible: !root.hasWifi && root.hasWired Layout.fillWidth: true spacing: 10 Text { text: "CONNECTION DETAILS" color: root.t.netPopupDim font.pixelSize: 10 font.letterSpacing: 1.2 Layout.fillWidth: true } Rectangle { Layout.fillWidth: true height: 38; radius: 10 color: root.wiredUp ? root.t.netWiredOkBg : root.t.netWiredErrBg border.color: root.wiredUp ? root.t.netWiredOkBorder : root.t.netWiredErrBorder border.width: 1 RowLayout { anchors { fill: parent; leftMargin: 14; rightMargin: 14 } spacing: 10 Text { text: root.wiredUp ? "\uf0601" : "\uf0602" color: root.wiredUp ? root.t.statusOk : root.t.statusErr font.pixelSize: 18 Layout.alignment: Qt.AlignVCenter } Text { text: root.wiredUp ? "Connected" : "Cable unplugged" color: root.wiredUp ? root.t.statusOk : root.t.statusErr font.pixelSize: 13 font.bold: true Layout.fillWidth: true } } } ColumnLayout { Layout.fillWidth: true spacing: 6 Repeater { model: [ { label: "Interface", value: (root.wiredDevice && root.wiredDevice.name) ? root.wiredDevice.name : "—" }, { label: "MAC", value: (root.wiredDevice && root.wiredDevice.address) ? root.wiredDevice.address : "—" }, { label: "Link", value: (root.wiredDevice) ? (root.wiredDevice.hasLink ? "Up" : "Down") : "—" }, { label: "Speed", value: (root.wiredDevice && root.wiredUp && root.wiredDevice.linkSpeed > 0) ? (root.wiredDevice.linkSpeed + " Mbps") : "—" } ] delegate: RowLayout { required property var modelData Layout.fillWidth: true Text { text: modelData.label color: root.t.netPopupDim font.pixelSize: 12 Layout.fillWidth: true } Text { text: modelData.value color: root.t.netPopupText font.pixelSize: 12 font.bold: true font.family: "monospace" } } } } } Text { visible: !root.hasWifi && !root.hasWired text: "No network hardware detected." color: root.t.netPopupDim font.pixelSize: 12 horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true Layout.topMargin: 4 } ColumnLayout { visible: root.showPwdPrompt Layout.fillWidth: true spacing: 8 Rectangle { Layout.fillWidth: true; height: 1; color: root.t.netPopupSeparator } Text { text: "Connect to \"" + root.pendingSSID + "\"" color: root.t.netPopupHeader font.pixelSize: 13 font.bold: true elide: Text.ElideRight Layout.fillWidth: true } Rectangle { Layout.fillWidth: true height: 36; radius: 8 color: root.t.netFieldBg border.color: pwdField.activeFocus ? root.t.accent : root.t.netPopupBorder border.width: 1 Behavior on border.color { ColorAnimation { duration: 120 } } RowLayout { anchors { fill: parent; leftMargin: 12; rightMargin: 10 } spacing: 6 TextInput { id: pwdField Layout.fillWidth: true verticalAlignment: TextInput.AlignVCenter echoMode: eyeToggle.showClear ? TextInput.Normal : TextInput.Password color: root.t.netPopupText font.pixelSize: 13 selectionColor: Qt.alpha(root.t.accent, 0.4) clip: true text: root.enteredPwd onTextChanged: root.enteredPwd = text Keys.onReturnPressed: root.attemptConnect() Keys.onEscapePressed: root.cancelConnect() onVisibleChanged: if (visible) forceActiveFocus() Text { anchors.fill: parent verticalAlignment: Text.AlignVCenter text: "Password" color: root.t.netPopupDim font.pixelSize: 13 visible: pwdField.text === "" && !pwdField.activeFocus } } Text { id: eyeToggle property bool showClear: false text: showClear ? "\uf0349" : "\uf0354" color: eyeHover.hovered ? root.t.accent : root.t.netPopupDim font.pixelSize: 15 Layout.alignment: Qt.AlignVCenter HoverHandler { id: eyeHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: eyeToggle.showClear = !eyeToggle.showClear } } } } RowLayout { Layout.fillWidth: true spacing: 8 Rectangle { Layout.fillWidth: true height: 34; radius: 8 color: cancelHover.hovered ? root.t.netCancelBgHover : "transparent" border.color: root.t.netPopupBorder border.width: 1 Behavior on color { ColorAnimation { duration: 120 } } HoverHandler { id: cancelHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: root.cancelConnect() } Text { anchors.centerIn: parent text: "Cancel" color: root.t.netPopupDim font.pixelSize: 12 } } Rectangle { Layout.fillWidth: true height: 34; radius: 8 color: connHover.hovered ? root.t.accent : Qt.alpha(root.t.accent, 0.75) Behavior on color { ColorAnimation { duration: 120 } } HoverHandler { id: connHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: root.attemptConnect() } Text { anchors.centerIn: parent text: "Connect" color: root.t.netConnectText font.pixelSize: 12 font.bold: true } } } } Item { height: 4 } } } } }