Let the Tailscale panel toggle on and off like Dropbox
The hero icon is now the optimistic on/off toggle with a tooltip, replacing the connect block and the progress lines beneath the hero. The disconnected cross is drawn grey rather than red, and action errors expire after a couple of seconds instead of sticking forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a7c0d0f6a1
commit
000fa2a972
@@ -42,14 +42,14 @@ Panel {
|
||||
readonly property color dim: Qt.darker(foreground, 1.55)
|
||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||
readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied
|
||||
readonly property bool showPeers: tailscale.running && tailscale.peers.length > 0
|
||||
readonly property bool showPeers: tailscale.active && tailscale.peers.length > 0
|
||||
readonly property var recentMullvadRegions: settings.recentMullvadRegions instanceof Array ? settings.recentMullvadRegions : (settings.recentMullvadCountries instanceof Array ? settings.recentMullvadCountries : [])
|
||||
readonly property var recentMullvadExitNodes: recentMullvadNodes()
|
||||
readonly property var exitNodes: displayExitNodes()
|
||||
readonly property bool showExitNodes: tailscale.running && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
||||
readonly property bool showExitNodes: tailscale.active && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
||||
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
||||
readonly property color iconColor: tailscale.running ? foreground : dim
|
||||
readonly property color barIconColor: tailscale.running ? barForeground : Qt.darker(barForeground, 1.55)
|
||||
readonly property color iconColor: tailscale.active ? foreground : dim
|
||||
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
||||
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||
readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent"
|
||||
|
||||
@@ -355,7 +355,7 @@ Panel {
|
||||
function toggle(): void { root.toggle() }
|
||||
function refresh(): string { tailscale.refresh(); return "ok" }
|
||||
function up(): string { tailscale.loginOrUp(); return "ok" }
|
||||
function down(): string { tailscale.runAction(["tailscale", "down"], "Turning Tailscale off…"); return "ok" }
|
||||
function down(): string { tailscale.down(); return "ok" }
|
||||
function status(): string { return tailscale.statusText }
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ Panel {
|
||||
iconSize: Style.space(11)
|
||||
color: root.barIconColor
|
||||
badgeColor: root.urgent
|
||||
crossed: !tailscale.running && !tailscale.needsLogin
|
||||
crossed: !tailscale.active && !tailscale.needsLogin
|
||||
warning: tailscale.needsLogin
|
||||
}
|
||||
}
|
||||
@@ -435,10 +435,10 @@ Panel {
|
||||
id: hero
|
||||
width: parent.width
|
||||
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
||||
meta: root.heroPhraseText
|
||||
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
iconOpacity: tailscale.running ? 1.0 : 0.5
|
||||
iconOpacity: tailscale.active ? 1.0 : 0.5
|
||||
iconComponent: Component {
|
||||
Item {
|
||||
implicitWidth: icon.implicitWidth
|
||||
@@ -449,7 +449,7 @@ Panel {
|
||||
iconSize: Style.font.display
|
||||
color: root.iconColor
|
||||
badgeColor: root.urgent
|
||||
crossed: !tailscale.running && !tailscale.needsLogin
|
||||
crossed: !tailscale.active && !tailscale.needsLogin
|
||||
warning: tailscale.needsLogin
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
@@ -466,61 +466,17 @@ Panel {
|
||||
}
|
||||
onClicked: tailscale.toggleTailscale()
|
||||
}
|
||||
|
||||
PanelToolTip {
|
||||
visible: heroIconMouse.containsMouse
|
||||
text: tailscale.active ? "Turn Tailscale off" : (tailscale.needsLogin ? "Authorize this device" : "Turn Tailscale on")
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CursorSurface {
|
||||
id: connectionRow
|
||||
visible: tailscale.installed && !tailscale.running
|
||||
width: parent.width
|
||||
implicitHeight: connectionText.implicitHeight + Style.spacing.rowPaddingX
|
||||
hasCursor: root.cursorActive && root.focusSection === "header"
|
||||
foreground: root.foreground
|
||||
fill: root.hoverFill
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !tailscale.busy
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "header"
|
||||
}
|
||||
onClicked: tailscale.loginOrUp()
|
||||
}
|
||||
|
||||
Column {
|
||||
id: connectionText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.space(12)
|
||||
anchors.rightMargin: Style.space(12)
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: tailscale.needsLogin ? "Authorize this device" : "Connect Tailscale"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: tailscale.needsLogin ? "Open Tailscale to restore this device's access" : "Reconnect with the current Tailscale settings"
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||
width: parent.width
|
||||
@@ -689,12 +645,12 @@ Panel {
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
visible: tailscale.installed && tailscale.running
|
||||
visible: tailscale.installed && tailscale.active
|
||||
foreground: root.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: tailscale.installed && tailscale.running
|
||||
visible: tailscale.installed && tailscale.active
|
||||
width: parent.width
|
||||
spacing: Style.space(10)
|
||||
|
||||
@@ -705,7 +661,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: tailscale.installed && tailscale.running && tailscale.peers.length === 0
|
||||
visible: tailscale.installed && tailscale.active && tailscale.peers.length === 0
|
||||
width: parent.width
|
||||
text: "No machines found on this tailnet."
|
||||
color: root.dim
|
||||
@@ -740,7 +696,7 @@ Panel {
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
running: root.opened
|
||||
running: root.opened && tailscale.active
|
||||
repeat: true
|
||||
onTriggered: phraseSwap.restart()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ Item {
|
||||
property bool installed: false
|
||||
property bool running: false
|
||||
property bool needsLogin: false
|
||||
|
||||
// Optimistic off state so the UI reacts the instant you click, rather than
|
||||
// waiting for the next status refresh. _desired is -1 while we just follow
|
||||
// the real state, or 0/1 while a toggle is still catching up.
|
||||
property int _desired: -1
|
||||
readonly property bool active: _desired === -1 ? running : (_desired === 1)
|
||||
property bool refreshing: false
|
||||
property string backendState: "Unknown"
|
||||
property string statusText: "Checking…"
|
||||
@@ -163,6 +169,7 @@ Item {
|
||||
function resetUnavailable(message) {
|
||||
running = false
|
||||
needsLogin = false
|
||||
_desired = -1
|
||||
backendState = "Unavailable"
|
||||
statusText = message
|
||||
selfName = ""
|
||||
@@ -197,6 +204,8 @@ Item {
|
||||
|
||||
backendState = parsed.backendState
|
||||
running = parsed.running
|
||||
// Reality caught up to the pending toggle — stop overriding.
|
||||
if (_desired !== -1 && running === (_desired === 1)) _desired = -1
|
||||
needsLogin = parsed.needsLogin
|
||||
authUrl = parsed.authUrl
|
||||
if (needsLogin && _loginInProgress && !_loginUrlOpened && authUrl !== "" && authUrl !== _preLoginAuthUrl) openAuthUrlFrom(authUrl, false)
|
||||
@@ -238,12 +247,20 @@ Item {
|
||||
|
||||
function toggleTailscale() {
|
||||
if (!installed) return
|
||||
if (running) runAction(["tailscale", "down"], "Turning Tailscale off…")
|
||||
if (active) down()
|
||||
else loginOrUp()
|
||||
}
|
||||
|
||||
function down() {
|
||||
// No progress status here — the greyed icon and hero line already convey
|
||||
// the optimistic off; only surface a message if the command fails.
|
||||
_desired = 0
|
||||
runAction(["tailscale", "down"])
|
||||
}
|
||||
|
||||
function loginOrUp() {
|
||||
if (!installed || loginProcess.running) return
|
||||
_desired = -1
|
||||
var plan = Model.loginPlan(needsLogin, authUrl)
|
||||
if (plan.authUrl !== "") {
|
||||
_loginUrlOpened = false
|
||||
@@ -252,7 +269,8 @@ Item {
|
||||
}
|
||||
_loginOutput = ""
|
||||
_loginError = ""
|
||||
actionStatus = needsLogin ? "Starting Tailscale login…" : "Turning Tailscale on…"
|
||||
if (needsLogin) actionStatus = "Starting Tailscale login…"
|
||||
else _desired = 1
|
||||
_loginInProgress = needsLogin
|
||||
_loginUrlOpened = false
|
||||
_preLoginAuthUrl = authUrl
|
||||
@@ -308,7 +326,7 @@ Item {
|
||||
if (actionProcess.running) return
|
||||
_actionOutput = ""
|
||||
_actionError = ""
|
||||
actionStatus = label || "Working…"
|
||||
actionStatus = label || ""
|
||||
actionProcess.command = command
|
||||
actionProcess.running = true
|
||||
}
|
||||
@@ -318,6 +336,8 @@ Item {
|
||||
var match = String(text || "").match(/https?:\/\/\S+/)
|
||||
var url = match && match[0] ? match[0] : (allowFallback === true ? authUrl : "")
|
||||
if (url !== "") {
|
||||
// Turning on ended up needing browser auth — stop pretending we're up.
|
||||
_desired = -1
|
||||
_loginUrlOpened = true
|
||||
_loginInProgress = false
|
||||
loginTimeoutTimer.stop()
|
||||
@@ -447,8 +467,10 @@ Item {
|
||||
var stdout = String(actionStdout.text || root._actionOutput || "")
|
||||
var stderr = String(actionStderr.text || root._actionError || "")
|
||||
if (exitCode !== 0) {
|
||||
root._desired = -1
|
||||
root.lastError = elideStatus(stderr || stdout || "Tailscale command failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
root.lastError = ""
|
||||
root.actionStatus = ""
|
||||
@@ -467,9 +489,11 @@ Item {
|
||||
var combined = String(root._loginOutput || "") + "\n" + String(root._loginError || "")
|
||||
var opened = root.openAuthUrlFrom(combined, true)
|
||||
if (exitCode !== 0 && !opened) {
|
||||
root._desired = -1
|
||||
root._loginInProgress = false
|
||||
root.lastError = elideStatus(combined || "tailscale up failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else if (!opened) {
|
||||
root.lastError = ""
|
||||
root.actionStatus = ""
|
||||
@@ -490,6 +514,7 @@ Item {
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = elideStatus(stderr || stdout || "Account switch failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
root.lastError = ""
|
||||
root.actionStatus = ""
|
||||
@@ -512,6 +537,7 @@ Item {
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = elideStatus(stderr || stdout || "Exit node selection failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
root.lastError = ""
|
||||
root.actionStatus = ""
|
||||
@@ -533,6 +559,7 @@ Item {
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = elideStatus(stderr || stdout || "Tailscale authorization failed")
|
||||
root.actionStatus = root.lastError
|
||||
actionStatusTimer.restart()
|
||||
} else {
|
||||
root.accountsAccessDenied = false
|
||||
root.lastError = ""
|
||||
|
||||
@@ -39,7 +39,7 @@ Item {
|
||||
width: parent.width * 1.22
|
||||
height: Math.max(2, parent.height * 0.14)
|
||||
radius: height / 2
|
||||
color: root.badgeColor
|
||||
color: root.color
|
||||
rotation: -45
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user