Files
quickdots/bar/modules/Network.qml

650 lines
30 KiB
QML

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.netWifiIconOk : t.netWifiIconErr
if (hasWired) return wiredUp ? t.netEthIconOk : t.netEthIconErr
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
chipColor: t.netBg
chipColorHovered: t.netBgHover
chipBorderColor: t.netBorder
chipBorderColorHovered: t.netBorderHover
chipHoverEnabled: true
chipTapAction: () => netPopup.toggle()
chipAnimImplicitWidth: true
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
}
}
StandardPopup {
id: netPopup
ns: "network"
chipRoot: root
cardWidth: 310
cardColor: t.netPopupBg
cardBorderColor: t.netPopupBorder
cardHeight: cardBody.implicitHeight + 28
layerEnabled: true
function open() {
root.cancelConnect()
_doOpen()
if (root.hasWifi && root.wifiDevice)
root.wifiDevice.scannerEnabled = 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.netEthIconOk : root.t.netEthIconErr
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 }
}
}
}