661 lines
30 KiB
QML
661 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.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 }
|
|
}
|
|
}
|
|
}
|
|
}
|