applying changes

This commit is contained in:
2026-10-11 16:50:54 +01:00
parent d395a155af
commit 0415804b67
4 changed files with 226 additions and 0 deletions

View File

@@ -222,6 +222,82 @@ Item {
root.passwordSsid = ""
}
// --- Captive portal ---------------------------------------------------
// NetworkManager tells "portal" (a walled-garden sign-in page sits
// between us and the real internet) apart from "full" connectivity only
// via an active probe — nmcli's cached CONNECTIVITY field just reflects
// NM's own internal check, which runs every few minutes and is far too
// slow to catch a portal right after joining a network. So this forces a
// fresh probe itself, same shell-out approach as the stats below, and
// runs it whenever we're connected — not gated to panel.visible like
// those — since the whole point is noticing this before the popup is
// ever opened.
//
// "limited" is treated the same as "portal", not just "full" vs.
// everything-else: NM only reports "portal" when the probe request
// actually comes back *different* (redirected/rewritten) from what it
// expected. Plenty of real captive portals instead black-hole every
// request that isn't to their own login domain, so the probe just times
// out — which is also why this is slow to resolve — and NM reports
// "limited" (it got a route/gateway, but the internet check itself
// never landed) rather than "portal". From the user's side both look
// identical: connected, but no real internet.
property string connectivityState: "unknown"
readonly property bool connectivityRestricted: root.connectivityState === "portal" || root.connectivityState === "limited"
// A plain, unencrypted HTTP page whose only job is to be interceptable
// by a captive portal's redirect. An https:// URL mostly just gets
// silently blocked instead of redirected, which looks identical to the
// portal not existing at all.
readonly property string captivePortalCheckUrl: "http://neverssl.com/"
// Auto-opened once per network join (mirrors TailscaleIndicator's
// openAuthUrl/_authUrlOpened) so flapping connectivity on the same
// network doesn't keep popping a new browser tab — the manual "Open"
// button in the panel banner below still works regardless.
property bool _portalAutoOpened: false
property bool _wasConnected: false
onConnectedDeviceChanged: {
const isConnected = root.connectedDevice !== null
if (isConnected && !root._wasConnected) {
root._portalAutoOpened = false
root.connectivityState = "unknown"
}
root._wasConnected = isConnected
}
onConnectivityRestrictedChanged: {
if (root.connectivityRestricted && !root._portalAutoOpened)
root.openCaptivePortal()
}
function openCaptivePortal() {
root._portalAutoOpened = true
Qt.openUrlExternally(root.captivePortalCheckUrl)
}
Process {
id: connectivityProc
command: ["nmcli", "networking", "connectivity", "check"]
stdout: StdioCollector {
id: connectivityCollector
onStreamFinished: root.connectivityState = connectivityCollector.text.trim()
}
}
Timer {
id: connectivityTimer
// Poll quickly while things are still unresolved (fresh join, or an
// active portal we're waiting to clear) and back off once
// connectivity is confirmed full, rather than shelling out every
// few seconds forever for no reason.
interval: root.connectivityState === "full" ? 20000 : 3000
repeat: true
triggeredOnStart: true
running: root.connectedDevice !== null
onTriggered: if (!connectivityProc.running) connectivityProc.running = true
}
// --- Live stats --------------------------------------------------
// None of this comes from Quickshell's Networking API — it only knows
// about devices/networks, not throughput, ping, or routing. So this
@@ -595,6 +671,34 @@ Item {
}
}
// Small warning badge over the icon's corner when the connection looks
// restricted (a captive portal, confirmed or just suspected — see
// connectivityRestricted above) — same treatment as TailscaleIndicator's
// login-needed badge, so "this needs your attention" reads the same way
// across indicators.
Rectangle {
visible: root.connectivityRestricted
width: Math.max(7, label.font.pixelSize * 0.42)
height: width
radius: width / 2
color: Colors.urgent
border.color: Colors.surface
border.width: 1
anchors.right: label.right
anchors.bottom: label.bottom
anchors.rightMargin: -1
anchors.bottomMargin: -1
Text {
anchors.centerIn: parent
text: "!"
color: Colors.surface
font.family: Appearance.fontFamily
font.pixelSize: Math.max(6, parent.height * 0.75)
font.bold: true
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
@@ -890,6 +994,88 @@ Item {
}
// Captive portal banner — shown when the connectivity probe
// above finds either a confirmed sign-in page or a
// "limited" result standing between this network and the
// real internet (see connectivityRestricted). openCaptivePortal()
// already fired once automatically when this was first
// detected; this stays up as a manual retry in case that tab
// got closed.
Rectangle {
width: parent.width
visible: root.connectivityRestricted
radius: 0
color: Qt.rgba(Colors.urgent.r, Colors.urgent.g, Colors.urgent.b, 0.18)
border.color: Colors.urgent
border.width: 1
implicitHeight: portalBannerRow.implicitHeight + 12
RowLayout {
id: portalBannerRow
anchors.fill: parent
anchors.margins: 6
spacing: 8
Text {
text: "lock_open"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.urgent
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal" ? "Sign-in required" : "No internet access"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: true
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal"
? "This network needs a web sign-in first"
: "This network may need a web sign-in first"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
Rectangle {
implicitWidth: openPortalText.implicitWidth + 16
implicitHeight: 24
radius: 0
color: openPortalMouse.containsMouse ? Colors.urgent : "transparent"
border.color: Colors.urgent
border.width: 1
Text {
id: openPortalText
anchors.centerIn: parent
text: "Open"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: openPortalMouse.containsMouse ? Colors.primaryText : Colors.urgent
}
MouseArea {
id: openPortalMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openCaptivePortal()
}
}
}
}
// Live stats — ping/loss/throughput/IP/gateway for whichever
// device is currently connected.
GridLayout {