updates to networking

This commit is contained in:
Hannes
2026-08-21 23:30:28 +02:00
parent 2b4d3cd911
commit d734110cc2
3 changed files with 392 additions and 267 deletions
+213 -129
View File
@@ -2,77 +2,211 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "../../../../config" import Quickshell.Networking
QtObject { QtObject {
id: root id: root
property string status: "..." property string status: "..."
property string interfaceName: "" property ListModel activeConnections: ListModel {}
property string interfaceType: ""
property string connectionName: ""
property string ipv4: ""
property string wifiName: ""
property string signalStrength: ""
property string wifiChannel: ""
property string wifiFrequency: ""
property string linkSpeed: ""
property string downloadSpeed: "" property string downloadSpeed: ""
property string uploadSpeed: "" property string uploadSpeed: ""
property string ping: "" property string ping: ""
property bool internetAvailable: false
property var downloadHistory: []
property var uploadHistory: []
property var pingHistory: []
property int _maxHistory: 60
property bool internetAvailable: Networking.connectivity === NetworkConnectivity.Full
property bool vpnConnected: false property bool vpnConnected: false
property bool vpnExists: false
property string vpnName: "" property string vpnName: ""
property string vpnIp: "" property string vpnIp: ""
property bool tailscaleConnected: false property bool tailscaleConnected: false
property bool tailscaleExists: false
property string tailscaleIp: "" property string tailscaleIp: ""
property bool running: false property bool running: false
property string _activeDevice: ""
property real _prevRxBytes: 0 property real _prevRxBytes: 0
property real _prevTxBytes: 0 property real _prevTxBytes: 0
property var _prevUpdateTime: 0 property var _prevUpdateTime: 0
property string _trafficActiveDevice: ""
readonly property var networking: Networking
function findAllConnectedDevices() {
var devs = networking.devices.values
var result = []
for (var i = 0; i < devs.length; i++) {
if (devs[i].connected) result.push(devs[i])
}
return result
}
function getNmConnectionName(net) {
if (!net) return ""
var settings = net.nmSettings
if (settings.length > 0) return settings[0].id
return ""
}
property var _pendingDevices: []
function updateAll() {
var connected = findAllConnectedDevices()
_pendingDevices = connected
activeConnections.clear()
var hasWifi = false
var hasWired = false
var bestWifiSignal = 0
var trafficDev = connected.length > 0 ? connected[0] : null
for (var i = 0; i < connected.length; i++) {
var dev = connected[i]
var entry = {
interfaceName: dev.name,
interfaceType: "",
connectionName: "",
wifiName: "",
signalStrength: 0,
linkSpeed: "",
ipv4: ""
}
if (dev.type === DeviceType.Wifi) {
entry.interfaceType = "wifi"
hasWifi = true
var networks = dev.networks.values
for (var j = 0; j < networks.length; j++) {
var net = networks[j]
if (net.connected) {
entry.wifiName = net.name
entry.signalStrength = Math.round(net.signalStrength * 100)
entry.connectionName = root.getNmConnectionName(net)
if (net.signalStrength > bestWifiSignal) bestWifiSignal = net.signalStrength
break
}
}
} else if (dev.type === DeviceType.Wired) {
entry.interfaceType = "ethernet"
hasWired = true
entry.linkSpeed = dev.linkSpeed + " Mbps"
if (dev.network) {
entry.connectionName = root.getNmConnectionName(dev.network)
}
}
activeConnections.append(entry)
if (dev.type === DeviceType.Wired && trafficDev !== dev) {
trafficDev = dev
}
}
if (trafficDev) {
_trafficActiveDevice = trafficDev.name
trafficScript.running = true
}
root.computeStatus(hasWired, hasWifi, bestWifiSignal)
if (connected.length > 0) {
ipv4Script.running = true
}
}
property Timer networkTimer: Timer { property Timer networkTimer: Timer {
interval: 10000 interval: 10000
repeat: true repeat: true
running: true running: true
onTriggered: networkScript.running = true onTriggered: root.updateAll()
} }
property Process networkScript: Process { property Process ipv4Script: Process {
command: ["sh", "-c",
"ip -4 addr show 2>/dev/null | awk '/^[0-9]+:/{iface=$2; gsub(/:.*/, \"\", iface)} /inet /{print iface\" \"$2}' | " +
"grep -v '\\blo\\b' | grep -v '\\bvirbr' | grep -v '\\btailscale' | grep -v '\\bdocker' | grep -v '\\bveth' | " +
"sed 's|/[0-9]*||'"
]
stdout: StdioCollector {
onStreamFinished: root.parseIpv4Output(text.trim())
}
}
function parseIpv4Output(output) {
var ipMap = {}
if (!output) {
root.applyIpv4Map(ipMap)
return
}
var lines = output.split("\n")
for (var i = 0; i < lines.length; i++) {
var parts = lines[i].trim().split(" ")
if (parts.length >= 2) {
ipMap[parts[0]] = parts[1]
}
}
root.applyIpv4Map(ipMap)
}
function applyIpv4Map(ipMap) {
for (var i = 0; i < activeConnections.count; i++) {
var iface = activeConnections.get(i).interfaceName
var ip = ipMap[iface] || ""
activeConnections.setProperty(i, "ipv4", ip)
}
}
property Process trafficScript: Process {
command: ["sh", "-c", command: ["sh", "-c",
"echo '===SHOW==='; " +
"ACTIVE=$(nmcli -t device status 2>/dev/null | awk -F: '$3==\"connected\"{print $1; exit}'); " +
"echo \"ACTIVE_DEVICE=$ACTIVE\"; " +
"if [ -n \"$ACTIVE\" ]; then " +
" nmcli -t -f ALL device show \"$ACTIVE\" 2>/dev/null; " +
"fi; " +
"echo '===TRAFFIC==='; " + "echo '===TRAFFIC==='; " +
"if [ -n \"$ACTIVE\" ]; then " + "DEV=\"" + root._trafficActiveDevice + "\"; " +
" echo \"RX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/rx_bytes 2>/dev/null || echo 0)\"; " + "if [ -n \"$DEV\" ]; then " +
" echo \"TX_BYTES=$(cat /sys/class/net/$ACTIVE/statistics/tx_bytes 2>/dev/null || echo 0)\"; " + " echo \"RX_BYTES=$(cat /sys/class/net/$DEV/statistics/rx_bytes 2>/dev/null || echo 0)\"; " +
" echo \"TX_BYTES=$(cat /sys/class/net/$DEV/statistics/tx_bytes 2>/dev/null || echo 0)\"; " +
"fi; " + "fi; " +
"echo '===WIFI==='; " +
"nmcli -t -f active,ssid,signal,chan,freq,rate dev wifi | awk -F: '$1==\"yes\" { $1=\"\"; sub(/^:/,\"\"); print }'; " +
"echo '===PING==='; " + "echo '===PING==='; " +
"ping -c 1 -W 2 1.1.1.1 2>/dev/null | awk -F/ '/rtt/{print $5}' || echo ''; " + "ping -c 1 -W 2 1.1.1.1 2>/dev/null | awk -F/ '/rtt/{print $5}' || echo ''; " +
"echo '===INTERNET==='; " + "echo '===VPN_ALL==='; " +
"ping -c 1 -W 1 1.1.1.1 2>/dev/null >/dev/null && echo 1 || echo 0; " + "nmcli -t -f NAME,TYPE connection show 2>/dev/null | grep -i vpn || true; " +
"echo '===VPN==='; " + "echo '===VPN_ACTIVE==='; " +
"nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep -i vpn || true; " + "nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep -i vpn || true; " +
"echo '===TAILSCALE==='; " + "echo '===TAILSCALE_EXISTS==='; " +
"TAIL_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP 'inet \\K[\\d.]+'); " + "ip link show tailscale0 >/dev/null 2>&1 && echo EXISTS || echo ''; " +
"if [ -n \"$TAIL_IP\" ]; then echo \"TAILSCALE_IP=$TAIL_IP\"; else echo \"\"; fi" "echo '===TAILSCALE_IP==='; " +
"ip -4 addr show tailscale0 2>/dev/null | grep -oP 'inet \\K[\\d.]+' || echo ''"
] ]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: root.parseNetworkOutput(text.trim()) onStreamFinished: root.parseOutput(text.trim())
} }
} }
function computeStatus(hasWired, hasWifi, bestSignal) {
if (!hasWired && !hasWifi) {
root.status = "󰤭/󱐤"
return
}
var parts = []
if (hasWired) parts.push("󰈀")
if (hasWifi) {
var sig = Math.round(bestSignal * 100)
if (sig > 80) parts.push("󰤨")
else if (sig > 60) parts.push("󰤥")
else if (sig > 40) parts.push("󰤢")
else if (sig > 20) parts.push("󰤟")
else if (sig > 0) parts.push("󰤯")
else parts.push("󰤭")
}
root.status = parts.join(" ")
}
function formatSpeed(bytesPerSec) { function formatSpeed(bytesPerSec) {
if (bytesPerSec >= 1073741824) return (bytesPerSec / 1073741824).toFixed(1) + " GB/s" if (bytesPerSec >= 1073741824) return (bytesPerSec / 1073741824).toFixed(1) + " GB/s"
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + " MB/s" if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + " MB/s"
@@ -80,54 +214,38 @@ QtObject {
return bytesPerSec.toFixed(0) + " B/s" return bytesPerSec.toFixed(0) + " B/s"
} }
function parseNetworkOutput(output) { function parseOutput(output) {
var sections = output.split("===TAILSCALE===") var sections = output.split("===TAILSCALE_IP===")
var tailscaleSection = sections.length > 1 ? sections[1].trim() : "" var tailscaleIpSection = sections.length > 1 ? sections[1].trim() : ""
var beforeTailscale = sections[0] var beforeTsIp = sections[0]
sections = beforeTailscale.split("===VPN===") sections = beforeTsIp.split("===TAILSCALE_EXISTS===")
var vpnSection = sections.length > 1 ? sections[1].trim() : "" var tailscaleExistsSection = sections.length > 1 ? sections[1].trim() : ""
var beforeVpn = sections[0] var beforeTsExist = sections[0]
sections = beforeVpn.split("===INTERNET===") sections = beforeTsExist.split("===VPN_ACTIVE===")
root.internetAvailable = sections.length > 1 ? sections[1].trim() === "1" : false var vpnActiveSection = sections.length > 1 ? sections[1].trim() : ""
var beforeInternet = sections[0] var beforeVpnActive = sections[0]
sections = beforeInternet.split("===PING===") sections = beforeVpnActive.split("===VPN_ALL===")
var vpnAllSection = sections.length > 1 ? sections[1].trim() : ""
var beforeVpnAll = sections[0]
sections = beforeVpnAll.split("===PING===")
root.ping = sections.length > 1 ? sections[1].trim() : "" root.ping = sections.length > 1 ? sections[1].trim() : ""
var beforePing = sections[0] var pingVal = parseFloat(root.ping)
sections = beforePing.split("===WIFI===") if (!isNaN(pingVal) && pingVal > 0) {
var wifiSection = sections.length > 1 ? sections[1].trim() : "" var pingHist = root.pingHistory.slice()
pingHist.push(pingVal)
if (pingHist.length > root._maxHistory) pingHist.shift()
root.pingHistory = pingHist
}
var beforeWifi = sections[0] var beforePing = sections[0]
sections = beforeWifi.split("===TRAFFIC===") sections = beforePing.split("===TRAFFIC===")
var trafficSection = sections.length > 1 ? sections[1].trim() : "" var trafficSection = sections.length > 1 ? sections[1].trim() : ""
var beforeTraffic = sections[0]
sections = beforeTraffic.split("===SHOW===")
var showSection = sections.length > 1 ? sections[1].trim() : ""
// Parse SHOW section
var activeDevice = ""
var lines = showSection.split("\n")
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (line.indexOf("ACTIVE_DEVICE=") === 0) {
activeDevice = line.substring("ACTIVE_DEVICE=".length)
} else if (line.indexOf("GENERAL.DEVICE:") === 0) {
root.interfaceName = line.substring("GENERAL.DEVICE:".length)
} else if (line.indexOf("GENERAL.TYPE:") === 0) {
root.interfaceType = line.substring("GENERAL.TYPE:".length)
} else if (line.indexOf("GENERAL.CONNECTION:") === 0) {
root.connectionName = line.substring("GENERAL.CONNECTION:".length)
} else if (line.indexOf("IP4.ADDRESS") >= 0) {
var ipPart = line.substring(line.indexOf(":") + 1)
root.ipv4 = ipPart.split("/")[0]
}
}
// Parse TRAFFIC section
if (trafficSection) { if (trafficSection) {
var trafficLines = trafficSection.split("\n") var trafficLines = trafficSection.split("\n")
var rxBytes = 0 var rxBytes = 0
@@ -138,75 +256,41 @@ QtObject {
else if (tline.indexOf("TX_BYTES=") === 0) txBytes = parseFloat(tline.substring("TX_BYTES=".length)) else if (tline.indexOf("TX_BYTES=") === 0) txBytes = parseFloat(tline.substring("TX_BYTES=".length))
} }
// console.log("[Network] rxBytes:", rxBytes, "txBytes:", txBytes) if (root._prevRxBytes > 0 && root._prevUpdateTime > 0) {
// console.log("[Network] activeDevice:", activeDevice, "| prevActiveDevice:", root._activeDevice)
// console.log("[Network] prevRxBytes:", root._prevRxBytes, "prevTxBytes:", root._prevTxBytes, "prevTime:", root._prevUpdateTime)
if (activeDevice === root._activeDevice && root._prevRxBytes > 0 && root._prevUpdateTime > 0) {
var now = Date.now() var now = Date.now()
var interval = (now - root._prevUpdateTime) / 1000 var interval = (now - root._prevUpdateTime) / 1000
// console.log("[Network] Same device, interval:", interval, "s")
// console.log("[Network] rxDelta:", (rxBytes - root._prevRxBytes), "txDelta:", (txBytes - root._prevTxBytes))
if (interval > 0) { if (interval > 0) {
var downBps = (rxBytes - root._prevRxBytes) / interval var dlBytes = (rxBytes - root._prevRxBytes) / interval
var upBps = (txBytes - root._prevTxBytes) / interval var ulBytes = (txBytes - root._prevTxBytes) / interval
// console.log("[Network] downBps:", downBps, "upBps:", upBps) root.downloadSpeed = root.formatSpeed(dlBytes)
root.downloadSpeed = root.formatSpeed(downBps) root.uploadSpeed = root.formatSpeed(ulBytes)
root.uploadSpeed = root.formatSpeed(upBps)
// console.log("[Network] downloadSpeed:", root.downloadSpeed, "uploadSpeed:", root.uploadSpeed) var dlHist = root.downloadHistory.slice()
} else { dlHist.push(dlBytes)
console.log("[Network] Interval zero or negative, skipping") if (dlHist.length > root._maxHistory) dlHist.shift()
root.downloadHistory = dlHist
var ulHist = root.uploadHistory.slice()
ulHist.push(ulBytes)
if (ulHist.length > root._maxHistory) ulHist.shift()
root.uploadHistory = ulHist
} }
} else { } else {
// console.log("[Network] First run or device changed, storing baseline")
root.downloadSpeed = "" root.downloadSpeed = ""
root.uploadSpeed = "" root.uploadSpeed = ""
} }
root._activeDevice = activeDevice
root._prevRxBytes = rxBytes root._prevRxBytes = rxBytes
root._prevTxBytes = txBytes root._prevTxBytes = txBytes
root._prevUpdateTime = Date.now() root._prevUpdateTime = Date.now()
// console.log("[Network] Stored baseline: rxBytes:", root._prevRxBytes, "txBytes:", root._prevTxBytes)
} }
// Parse WIFI section root.vpnExists = vpnAllSection.length > 0
if (wifiSection) { if (vpnActiveSection) {
var wifiParts = wifiSection.split(" ")
if (wifiParts.length >= 2) {
root.wifiName = wifiParts[0] || ""
root.signalStrength = wifiParts[1] || ""
if (wifiParts.length >= 3) root.wifiChannel = wifiParts[2] || ""
if (wifiParts.length >= 4) root.wifiFrequency = wifiParts[3] + " " + wifiParts[4] || ""
if (wifiParts.length >= 5) root.linkSpeed = wifiParts[5] + " " + wifiParts[6] || ""
}
}
// Determine status icon
if (root.interfaceType === "ethernet") {
root.status = "󰈀"
} else if (root.interfaceType === "wifi") {
if (!root.connectionName) {
root.status = "󰤭"
} else {
var sig = parseInt(root.signalStrength)
if (sig > 80) root.status = "󰤨"
else if (sig > 60) root.status = "󰤥"
else if (sig > 40) root.status = "󰤢"
else if (sig > 20) root.status = "󰤟"
else root.status = "󰤯"
}
} else if (!activeDevice) {
root.status = "󰤭/󱐤"
}
// Parse VPN section
if (vpnSection) {
root.vpnConnected = true root.vpnConnected = true
var vpnLines = vpnSection.split("\n") var vpnLines = vpnActiveSection.split("\n")
if (vpnLines.length > 0) { if (vpnLines.length > 0) {
var vpnParts = vpnLines[0].split(":") root.vpnName = vpnLines[0].split(":")[0] || ""
root.vpnName = vpnParts[0] || ""
} }
} else { } else {
root.vpnConnected = false root.vpnConnected = false
@@ -214,10 +298,10 @@ QtObject {
root.vpnIp = "" root.vpnIp = ""
} }
// Parse TAILSCALE section root.tailscaleExists = tailscaleExistsSection.indexOf("EXISTS") >= 0
if (tailscaleSection && tailscaleSection.indexOf("TAILSCALE_IP=") === 0) { if (tailscaleIpSection) {
root.tailscaleConnected = true root.tailscaleConnected = true
root.tailscaleIp = tailscaleSection.substring("TAILSCALE_IP=".length) root.tailscaleIp = tailscaleIpSection
} else { } else {
root.tailscaleConnected = false root.tailscaleConnected = false
root.tailscaleIp = "" root.tailscaleIp = ""
@@ -225,6 +309,6 @@ QtObject {
} }
Component.onCompleted: { Component.onCompleted: {
networkScript.running = true root.updateAll()
} }
} }
+106 -132
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell
import "../../../config" import "../../../config"
import "../states/Network" import "../states/Network"
@@ -12,9 +11,8 @@ MouseArea {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
console.log("[Network] Refreshing... current status: " + NetworkState.status) console.log("[Network] Manual refresh")
NetworkState.networkScript.running = false NetworkState.updateAll()
NetworkState.networkScript.running = true
} }
Rectangle { Rectangle {
@@ -44,166 +42,142 @@ MouseArea {
Column { Column {
spacing: parent.spacing spacing: parent.spacing
Repeater {
model: NetworkState.activeConnections
delegate: Column {
spacing: 2
Text { Text {
text: "interfaceName: " + NetworkState.interfaceName text: model.interfaceType === "wifi" ? "Wifi" : "Ethernet"
wrapMode: Text.WordWrap color: Colors.secondary
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
font.bold: true
}
Text {
text: " interface: " + model.interfaceName
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: " connection: " + model.connectionName
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: " ipv4: " + model.ipv4
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
visible: model.ipv4 !== ""
}
Text {
text: " wifi: " + model.wifiName + " signal: " + model.signalStrength + "%"
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
visible: model.interfaceType === "wifi"
}
Text {
text: " linkSpeed: " + model.linkSpeed
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
visible: model.interfaceType === "ethernet"
}
}
}
Item { width: 1; height: parent.spacing }
Column {
width: parent.width
spacing: 2
Text {
text: "download: " + NetworkState.downloadSpeed
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Sparkline {
values: NetworkState.downloadHistory
lineColor: Colors.primary
width: parent.width
}
}
Column {
width: parent.width
spacing: 2
Text {
text: "upload: " + NetworkState.uploadSpeed
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Sparkline {
values: NetworkState.uploadHistory
lineColor: Colors.tertiary
width: parent.width
}
}
Column {
width: parent.width
spacing: 2
Text {
text: "ping: " + NetworkState.ping + " ms"
color: Colors.primary
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Sparkline {
values: NetworkState.pingHistory
lineColor: Colors.error
width: parent.width
}
} }
Text { Text {
text: "interfaceType: " + NetworkState.interfaceType text: "no connection"
wrapMode: Text.WordWrap color: Colors.error
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
visible: !NetworkState.internetAvailable
} }
Text { Text {
text: "connectionName: " + NetworkState.connectionName text: "VPN: disconnected"
wrapMode: Text.WordWrap
color: Colors.primary color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
visible: NetworkState.vpnExists && !NetworkState.vpnConnected
} }
Text { Text {
text: "ipv4: " + NetworkState.ipv4 text: "VPN: " + NetworkState.vpnName
wrapMode: Text.WordWrap
color: Colors.primary color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
visible: NetworkState.vpnConnected
} }
Text { Text {
text: "wifiName: " + NetworkState.wifiName text: "Tailscale: disconnected"
wrapMode: Text.WordWrap
color: Colors.primary color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
visible: NetworkState.tailscaleExists && !NetworkState.tailscaleConnected
} }
Text { Text {
text: "signalStrength: " + NetworkState.signalStrength text: "Tailscale: " + NetworkState.tailscaleIp
wrapMode: Text.WordWrap
color: Colors.primary color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "wifiChannel: " + NetworkState.wifiChannel
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "wifiFrequency: " + NetworkState.wifiFrequency
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "linkSpeed: " + NetworkState.linkSpeed
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "downloadSpeed: " + NetworkState.downloadSpeed
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "uploadSpeed: " + NetworkState.uploadSpeed
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "ping: " + NetworkState.ping
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "internetAvailable: " + NetworkState.internetAvailable
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "vpnConnected: " + NetworkState.vpnConnected
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "vpnName: " + NetworkState.vpnName
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "vpnIp: " + NetworkState.vpnIp
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "tailscaleConnected: " + NetworkState.tailscaleConnected
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font
}
Text {
text: "tailscaleIp: " + NetworkState.tailscaleIp
wrapMode: Text.WordWrap
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Config.font_size_tiny(bar.height) font.pixelSize: Config.font_size_tiny(bar.height)
font.family: Config.font font.family: Config.font
visible: NetworkState.tailscaleConnected
} }
} }
} }
+67
View File
@@ -0,0 +1,67 @@
import QtQuick
import "../../../config"
Item {
id: root
property var values: []
property color lineColor: Colors.primary
property color fillColor: Qt.alpha(Colors.primary, 0.15)
property int pointCount: 60
property real minWidth: 120
property real graphHeight: 30
width: Math.max(minWidth, parent ? parent.width : minWidth)
height: graphHeight
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var data = root.values
if (!data || data.length < 2) return
var displayCount = Math.min(data.length, root.pointCount)
var startIdx = data.length - displayCount
var slice = data.slice(startIdx)
var maxVal = 0
for (var i = 0; i < slice.length; i++) {
if (slice[i] > maxVal) maxVal = slice[i]
}
if (maxVal === 0) maxVal = 1
var stepX = width / (slice.length - 1)
var padY = 2
var drawH = height - padY * 2
ctx.beginPath()
ctx.moveTo(0, height - padY - (slice[0] / maxVal) * drawH)
for (var j = 1; j < slice.length; j++) {
var x = j * stepX
var y = height - padY - (slice[j] / maxVal) * drawH
ctx.lineTo(x, y)
}
ctx.strokeStyle = root.lineColor
ctx.lineWidth = 1.5
ctx.stroke()
ctx.lineTo(width, height)
ctx.lineTo(0, height)
ctx.closePath()
ctx.fillStyle = root.fillColor
ctx.fill()
}
Connections {
target: root
function onValuesChanged() { canvas.requestPaint() }
function onWidthChanged() { canvas.requestPaint() }
function onHeightChanged() { canvas.requestPaint() }
}
}
}