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 color dim: Qt.darker(foreground, 1.55)
|
||||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||||
readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied
|
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 recentMullvadRegions: settings.recentMullvadRegions instanceof Array ? settings.recentMullvadRegions : (settings.recentMullvadCountries instanceof Array ? settings.recentMullvadCountries : [])
|
||||||
readonly property var recentMullvadExitNodes: recentMullvadNodes()
|
readonly property var recentMullvadExitNodes: recentMullvadNodes()
|
||||||
readonly property var exitNodes: displayExitNodes()
|
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 var filteredMullvadRegions: filteredMullvadRegionNodes()
|
||||||
readonly property color iconColor: tailscale.running ? foreground : dim
|
readonly property color iconColor: tailscale.active ? foreground : dim
|
||||||
readonly property color barIconColor: tailscale.running ? barForeground : Qt.darker(barForeground, 1.55)
|
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 hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||||
readonly property color selectedFill: bar ? Style.selectedFillFor(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 toggle(): void { root.toggle() }
|
||||||
function refresh(): string { tailscale.refresh(); return "ok" }
|
function refresh(): string { tailscale.refresh(); return "ok" }
|
||||||
function up(): string { tailscale.loginOrUp(); 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 }
|
function status(): string { return tailscale.statusText }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ Panel {
|
|||||||
iconSize: Style.space(11)
|
iconSize: Style.space(11)
|
||||||
color: root.barIconColor
|
color: root.barIconColor
|
||||||
badgeColor: root.urgent
|
badgeColor: root.urgent
|
||||||
crossed: !tailscale.running && !tailscale.needsLogin
|
crossed: !tailscale.active && !tailscale.needsLogin
|
||||||
warning: tailscale.needsLogin
|
warning: tailscale.needsLogin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,10 +435,10 @@ Panel {
|
|||||||
id: hero
|
id: hero
|
||||||
width: parent.width
|
width: parent.width
|
||||||
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
||||||
meta: root.heroPhraseText
|
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
fontFamily: root.fontFamily
|
fontFamily: root.fontFamily
|
||||||
iconOpacity: tailscale.running ? 1.0 : 0.5
|
iconOpacity: tailscale.active ? 1.0 : 0.5
|
||||||
iconComponent: Component {
|
iconComponent: Component {
|
||||||
Item {
|
Item {
|
||||||
implicitWidth: icon.implicitWidth
|
implicitWidth: icon.implicitWidth
|
||||||
@@ -449,7 +449,7 @@ Panel {
|
|||||||
iconSize: Style.font.display
|
iconSize: Style.font.display
|
||||||
color: root.iconColor
|
color: root.iconColor
|
||||||
badgeColor: root.urgent
|
badgeColor: root.urgent
|
||||||
crossed: !tailscale.running && !tailscale.needsLogin
|
crossed: !tailscale.active && !tailscale.needsLogin
|
||||||
warning: tailscale.needsLogin
|
warning: tailscale.needsLogin
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -466,61 +466,17 @@ Panel {
|
|||||||
}
|
}
|
||||||
onClicked: tailscale.toggleTailscale()
|
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 {
|
Text {
|
||||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -689,12 +645,12 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PanelSeparator {
|
PanelSeparator {
|
||||||
visible: tailscale.installed && tailscale.running
|
visible: tailscale.installed && tailscale.active
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
visible: tailscale.installed && tailscale.running
|
visible: tailscale.installed && tailscale.active
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Style.space(10)
|
spacing: Style.space(10)
|
||||||
|
|
||||||
@@ -705,7 +661,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: tailscale.installed && tailscale.running && tailscale.peers.length === 0
|
visible: tailscale.installed && tailscale.active && tailscale.peers.length === 0
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "No machines found on this tailnet."
|
text: "No machines found on this tailnet."
|
||||||
color: root.dim
|
color: root.dim
|
||||||
@@ -740,7 +696,7 @@ Panel {
|
|||||||
Timer {
|
Timer {
|
||||||
id: phraseTimer
|
id: phraseTimer
|
||||||
interval: 2800
|
interval: 2800
|
||||||
running: root.opened
|
running: root.opened && tailscale.active
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: phraseSwap.restart()
|
onTriggered: phraseSwap.restart()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ Item {
|
|||||||
property bool installed: false
|
property bool installed: false
|
||||||
property bool running: false
|
property bool running: false
|
||||||
property bool needsLogin: 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 bool refreshing: false
|
||||||
property string backendState: "Unknown"
|
property string backendState: "Unknown"
|
||||||
property string statusText: "Checking…"
|
property string statusText: "Checking…"
|
||||||
@@ -163,6 +169,7 @@ Item {
|
|||||||
function resetUnavailable(message) {
|
function resetUnavailable(message) {
|
||||||
running = false
|
running = false
|
||||||
needsLogin = false
|
needsLogin = false
|
||||||
|
_desired = -1
|
||||||
backendState = "Unavailable"
|
backendState = "Unavailable"
|
||||||
statusText = message
|
statusText = message
|
||||||
selfName = ""
|
selfName = ""
|
||||||
@@ -197,6 +204,8 @@ Item {
|
|||||||
|
|
||||||
backendState = parsed.backendState
|
backendState = parsed.backendState
|
||||||
running = parsed.running
|
running = parsed.running
|
||||||
|
// Reality caught up to the pending toggle — stop overriding.
|
||||||
|
if (_desired !== -1 && running === (_desired === 1)) _desired = -1
|
||||||
needsLogin = parsed.needsLogin
|
needsLogin = parsed.needsLogin
|
||||||
authUrl = parsed.authUrl
|
authUrl = parsed.authUrl
|
||||||
if (needsLogin && _loginInProgress && !_loginUrlOpened && authUrl !== "" && authUrl !== _preLoginAuthUrl) openAuthUrlFrom(authUrl, false)
|
if (needsLogin && _loginInProgress && !_loginUrlOpened && authUrl !== "" && authUrl !== _preLoginAuthUrl) openAuthUrlFrom(authUrl, false)
|
||||||
@@ -238,12 +247,20 @@ Item {
|
|||||||
|
|
||||||
function toggleTailscale() {
|
function toggleTailscale() {
|
||||||
if (!installed) return
|
if (!installed) return
|
||||||
if (running) runAction(["tailscale", "down"], "Turning Tailscale off…")
|
if (active) down()
|
||||||
else loginOrUp()
|
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() {
|
function loginOrUp() {
|
||||||
if (!installed || loginProcess.running) return
|
if (!installed || loginProcess.running) return
|
||||||
|
_desired = -1
|
||||||
var plan = Model.loginPlan(needsLogin, authUrl)
|
var plan = Model.loginPlan(needsLogin, authUrl)
|
||||||
if (plan.authUrl !== "") {
|
if (plan.authUrl !== "") {
|
||||||
_loginUrlOpened = false
|
_loginUrlOpened = false
|
||||||
@@ -252,7 +269,8 @@ Item {
|
|||||||
}
|
}
|
||||||
_loginOutput = ""
|
_loginOutput = ""
|
||||||
_loginError = ""
|
_loginError = ""
|
||||||
actionStatus = needsLogin ? "Starting Tailscale login…" : "Turning Tailscale on…"
|
if (needsLogin) actionStatus = "Starting Tailscale login…"
|
||||||
|
else _desired = 1
|
||||||
_loginInProgress = needsLogin
|
_loginInProgress = needsLogin
|
||||||
_loginUrlOpened = false
|
_loginUrlOpened = false
|
||||||
_preLoginAuthUrl = authUrl
|
_preLoginAuthUrl = authUrl
|
||||||
@@ -308,7 +326,7 @@ Item {
|
|||||||
if (actionProcess.running) return
|
if (actionProcess.running) return
|
||||||
_actionOutput = ""
|
_actionOutput = ""
|
||||||
_actionError = ""
|
_actionError = ""
|
||||||
actionStatus = label || "Working…"
|
actionStatus = label || ""
|
||||||
actionProcess.command = command
|
actionProcess.command = command
|
||||||
actionProcess.running = true
|
actionProcess.running = true
|
||||||
}
|
}
|
||||||
@@ -318,6 +336,8 @@ Item {
|
|||||||
var match = String(text || "").match(/https?:\/\/\S+/)
|
var match = String(text || "").match(/https?:\/\/\S+/)
|
||||||
var url = match && match[0] ? match[0] : (allowFallback === true ? authUrl : "")
|
var url = match && match[0] ? match[0] : (allowFallback === true ? authUrl : "")
|
||||||
if (url !== "") {
|
if (url !== "") {
|
||||||
|
// Turning on ended up needing browser auth — stop pretending we're up.
|
||||||
|
_desired = -1
|
||||||
_loginUrlOpened = true
|
_loginUrlOpened = true
|
||||||
_loginInProgress = false
|
_loginInProgress = false
|
||||||
loginTimeoutTimer.stop()
|
loginTimeoutTimer.stop()
|
||||||
@@ -447,8 +467,10 @@ Item {
|
|||||||
var stdout = String(actionStdout.text || root._actionOutput || "")
|
var stdout = String(actionStdout.text || root._actionOutput || "")
|
||||||
var stderr = String(actionStderr.text || root._actionError || "")
|
var stderr = String(actionStderr.text || root._actionError || "")
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
|
root._desired = -1
|
||||||
root.lastError = elideStatus(stderr || stdout || "Tailscale command failed")
|
root.lastError = elideStatus(stderr || stdout || "Tailscale command failed")
|
||||||
root.actionStatus = root.lastError
|
root.actionStatus = root.lastError
|
||||||
|
actionStatusTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
root.lastError = ""
|
root.lastError = ""
|
||||||
root.actionStatus = ""
|
root.actionStatus = ""
|
||||||
@@ -467,9 +489,11 @@ Item {
|
|||||||
var combined = String(root._loginOutput || "") + "\n" + String(root._loginError || "")
|
var combined = String(root._loginOutput || "") + "\n" + String(root._loginError || "")
|
||||||
var opened = root.openAuthUrlFrom(combined, true)
|
var opened = root.openAuthUrlFrom(combined, true)
|
||||||
if (exitCode !== 0 && !opened) {
|
if (exitCode !== 0 && !opened) {
|
||||||
|
root._desired = -1
|
||||||
root._loginInProgress = false
|
root._loginInProgress = false
|
||||||
root.lastError = elideStatus(combined || "tailscale up failed")
|
root.lastError = elideStatus(combined || "tailscale up failed")
|
||||||
root.actionStatus = root.lastError
|
root.actionStatus = root.lastError
|
||||||
|
actionStatusTimer.restart()
|
||||||
} else if (!opened) {
|
} else if (!opened) {
|
||||||
root.lastError = ""
|
root.lastError = ""
|
||||||
root.actionStatus = ""
|
root.actionStatus = ""
|
||||||
@@ -490,6 +514,7 @@ Item {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.lastError = elideStatus(stderr || stdout || "Account switch failed")
|
root.lastError = elideStatus(stderr || stdout || "Account switch failed")
|
||||||
root.actionStatus = root.lastError
|
root.actionStatus = root.lastError
|
||||||
|
actionStatusTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
root.lastError = ""
|
root.lastError = ""
|
||||||
root.actionStatus = ""
|
root.actionStatus = ""
|
||||||
@@ -512,6 +537,7 @@ Item {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.lastError = elideStatus(stderr || stdout || "Exit node selection failed")
|
root.lastError = elideStatus(stderr || stdout || "Exit node selection failed")
|
||||||
root.actionStatus = root.lastError
|
root.actionStatus = root.lastError
|
||||||
|
actionStatusTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
root.lastError = ""
|
root.lastError = ""
|
||||||
root.actionStatus = ""
|
root.actionStatus = ""
|
||||||
@@ -533,6 +559,7 @@ Item {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
root.lastError = elideStatus(stderr || stdout || "Tailscale authorization failed")
|
root.lastError = elideStatus(stderr || stdout || "Tailscale authorization failed")
|
||||||
root.actionStatus = root.lastError
|
root.actionStatus = root.lastError
|
||||||
|
actionStatusTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
root.accountsAccessDenied = false
|
root.accountsAccessDenied = false
|
||||||
root.lastError = ""
|
root.lastError = ""
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Item {
|
|||||||
width: parent.width * 1.22
|
width: parent.width * 1.22
|
||||||
height: Math.max(2, parent.height * 0.14)
|
height: Math.max(2, parent.height * 0.14)
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: root.badgeColor
|
color: root.color
|
||||||
rotation: -45
|
rotation: -45
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user