Refine Mullvad exit node picker
This commit is contained in:
@@ -143,34 +143,44 @@ function parseExitNodeList(raw) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function mullvadCountryOptions(nodes) {
|
function mullvadRegionOptions(nodes) {
|
||||||
var byCountry = {}
|
var byRegion = {}
|
||||||
var values = Array.isArray(nodes) ? nodes : []
|
var values = Array.isArray(nodes) ? nodes : []
|
||||||
for (var i = 0; i < values.length; i++) {
|
for (var i = 0; i < values.length; i++) {
|
||||||
var node = values[i] || {}
|
var node = values[i] || {}
|
||||||
if (node.Mullvad !== true) continue
|
if (node.Mullvad !== true) continue
|
||||||
var country = String(node.Country || "").trim()
|
var country = String(node.Country || "").trim()
|
||||||
|
var city = String(node.City || "").trim()
|
||||||
if (country === "") continue
|
if (country === "") continue
|
||||||
var current = byCountry[country]
|
if (city === "" || city === "Any") continue
|
||||||
if (!current || node.City === "Any") {
|
|
||||||
var option = {}
|
var key = country + "\n" + city
|
||||||
for (var key in node) option[key] = node[key]
|
if (byRegion[key]) continue
|
||||||
option.id = "mullvad-country:" + country
|
|
||||||
option.DisplayName = country
|
var option = {}
|
||||||
option.Country = country
|
for (var propertyName in node) option[propertyName] = node[propertyName]
|
||||||
option.MullvadCountry = true
|
option.id = "mullvad-region:" + key
|
||||||
byCountry[country] = option
|
option.DisplayName = city + ", " + country
|
||||||
}
|
option.Country = country
|
||||||
|
option.City = city
|
||||||
|
option.MullvadRegion = true
|
||||||
|
byRegion[key] = option
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = []
|
var result = []
|
||||||
for (var name in byCountry) result.push(byCountry[name])
|
for (var name in byRegion) result.push(byRegion[name])
|
||||||
result.sort(function(a, b) {
|
result.sort(function(a, b) {
|
||||||
return String(a.Country).localeCompare(String(b.Country))
|
var countryCompare = String(a.Country).localeCompare(String(b.Country))
|
||||||
|
if (countryCompare !== 0) return countryCompare
|
||||||
|
return String(a.City).localeCompare(String(b.City))
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mullvadCountryOptions(nodes) {
|
||||||
|
return mullvadRegionOptions(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
function parseStatus(raw) {
|
function parseStatus(raw) {
|
||||||
var text = String(raw || "").trim()
|
var text = String(raw || "").trim()
|
||||||
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
|
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
|
||||||
@@ -263,6 +273,7 @@ if (typeof module !== "undefined") {
|
|||||||
isMullvadPeer: isMullvadPeer,
|
isMullvadPeer: isMullvadPeer,
|
||||||
peerFromStatus: peerFromStatus,
|
peerFromStatus: peerFromStatus,
|
||||||
parseExitNodeList: parseExitNodeList,
|
parseExitNodeList: parseExitNodeList,
|
||||||
|
mullvadRegionOptions: mullvadRegionOptions,
|
||||||
mullvadCountryOptions: mullvadCountryOptions,
|
mullvadCountryOptions: mullvadCountryOptions,
|
||||||
parseStatus: parseStatus,
|
parseStatus: parseStatus,
|
||||||
parseAccounts: parseAccounts
|
parseAccounts: parseAccounts
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Panel {
|
|||||||
property int accountIndex: 0
|
property int accountIndex: 0
|
||||||
property int peerIndex: 0
|
property int peerIndex: 0
|
||||||
property int exitNodeIndex: 0
|
property int exitNodeIndex: 0
|
||||||
|
property int mullvadRegionIndex: 0
|
||||||
property bool cursorActive: false
|
property bool cursorActive: false
|
||||||
property bool copyMenuOpen: false
|
property bool copyMenuOpen: false
|
||||||
property bool mullvadPickerOpen: false
|
property bool mullvadPickerOpen: false
|
||||||
@@ -42,11 +43,11 @@ Panel {
|
|||||||
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.running && tailscale.peers.length > 0
|
||||||
readonly property var recentMullvadCountries: Array.isArray(settings.recentMullvadCountries) ? settings.recentMullvadCountries : []
|
readonly property var recentMullvadRegions: Array.isArray(settings.recentMullvadRegions) ? settings.recentMullvadRegions : (Array.isArray(settings.recentMullvadCountries) ? 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.mullvadCountries.length > 0)
|
readonly property bool showExitNodes: tailscale.running && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
||||||
readonly property var filteredMullvadCountries: filteredMullvadCountryNodes()
|
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
||||||
readonly property color iconColor: tailscale.running ? foreground : dim
|
readonly property color iconColor: tailscale.running ? foreground : dim
|
||||||
readonly property color barIconColor: tailscale.running ? barForeground : Qt.darker(barForeground, 1.55)
|
readonly property color barIconColor: tailscale.running ? 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"
|
||||||
@@ -62,68 +63,95 @@ Panel {
|
|||||||
return exitNodes[Math.max(0, Math.min(exitNodeIndex, exitNodes.length - 1))]
|
return exitNodes[Math.max(0, Math.min(exitNodeIndex, exitNodes.length - 1))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectedMullvadRegion() {
|
||||||
|
if (filteredMullvadRegions.length === 0) return null
|
||||||
|
return filteredMullvadRegions[Math.max(0, Math.min(mullvadRegionIndex, filteredMullvadRegions.length - 1))]
|
||||||
|
}
|
||||||
|
|
||||||
function displayExitNodes() {
|
function displayExitNodes() {
|
||||||
var nodes = []
|
var nodes = []
|
||||||
for (var i = 0; i < tailscale.tailnetExitNodes.length; i++) nodes.push(tailscale.tailnetExitNodes[i])
|
for (var i = 0; i < tailscale.tailnetExitNodes.length; i++) nodes.push(tailscale.tailnetExitNodes[i])
|
||||||
for (var j = 0; j < recentMullvadExitNodes.length; j++) nodes.push(recentMullvadExitNodes[j])
|
for (var j = 0; j < recentMullvadExitNodes.length; j++) nodes.push(recentMullvadExitNodes[j])
|
||||||
if (tailscale.mullvadCountries.length > 0) nodes.push({ id: "mullvad:add", AddMullvad: true, DisplayName: "Choose Mullvad region" })
|
if (tailscale.mullvadRegions.length > 0) nodes.push({ id: "mullvad:add", AddMullvad: true, DisplayName: "Choose Mullvad region" })
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
function recentMullvadNodes() {
|
function recentMullvadNodes() {
|
||||||
var nodes = []
|
var nodes = []
|
||||||
var seen = {}
|
var seen = {}
|
||||||
for (var a = 0; a < tailscale.mullvadCountries.length && nodes.length < 5; a++) {
|
for (var a = 0; a < tailscale.mullvadRegions.length && nodes.length < 5; a++) {
|
||||||
var active = tailscale.mullvadCountries[a]
|
var active = tailscale.mullvadRegions[a]
|
||||||
var activeCountry = String(active.Country || "")
|
var activeKey = mullvadRegionKey(active)
|
||||||
if (active.ExitNode === true && activeCountry !== "" && !seen[activeCountry]) {
|
if (active.ExitNode === true && activeKey !== "" && !seen[activeKey]) {
|
||||||
nodes.push(active)
|
nodes.push(active)
|
||||||
seen[activeCountry] = true
|
seen[activeKey] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var i = 0; i < recentMullvadCountries.length && nodes.length < 5; i++) {
|
for (var i = 0; i < recentMullvadRegions.length && nodes.length < 5; i++) {
|
||||||
var country = String(recentMullvadCountries[i] || "")
|
var region = String(recentMullvadRegions[i] || "")
|
||||||
if (country === "" || seen[country]) continue
|
if (region === "" || seen[region]) continue
|
||||||
var node = mullvadCountryNode(country)
|
var node = mullvadRegionNode(region)
|
||||||
if (node) {
|
if (node) {
|
||||||
nodes.push(node)
|
nodes.push(node)
|
||||||
seen[country] = true
|
seen[region] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
function mullvadCountryNode(country) {
|
function mullvadRegionKey(node) {
|
||||||
for (var i = 0; i < tailscale.mullvadCountries.length; i++) {
|
if (!node) return ""
|
||||||
var node = tailscale.mullvadCountries[i]
|
var country = String(node.Country || "")
|
||||||
if (String(node.Country || "") === String(country || "")) return node
|
var city = String(node.City || "")
|
||||||
|
if (country === "" || city === "") return ""
|
||||||
|
return country + "\n" + city
|
||||||
|
}
|
||||||
|
|
||||||
|
function mullvadRegionNode(region) {
|
||||||
|
for (var i = 0; i < tailscale.mullvadRegions.length; i++) {
|
||||||
|
var node = tailscale.mullvadRegions[i]
|
||||||
|
if (mullvadRegionKey(node) === String(region || "")) return node
|
||||||
|
if (String(node.Country || "") === String(region || "")) return node
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function filteredMullvadCountryNodes() {
|
function filteredMullvadRegionNodes() {
|
||||||
var query = String(mullvadQuery || "").trim().toLowerCase()
|
var query = String(mullvadQuery || "").trim().toLowerCase()
|
||||||
var result = []
|
var result = []
|
||||||
for (var i = 0; i < tailscale.mullvadCountries.length; i++) {
|
for (var i = 0; i < tailscale.mullvadRegions.length; i++) {
|
||||||
var node = tailscale.mullvadCountries[i]
|
var node = tailscale.mullvadRegions[i]
|
||||||
var label = String(node.DisplayName || node.Country || "").toLowerCase()
|
var label = (String(node.City || "") + " " + String(node.Country || "")).toLowerCase()
|
||||||
if (query === "" || label.indexOf(query) !== -1) result.push(node)
|
if (query === "" || label.indexOf(query) !== -1) result.push(node)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistRecentMullvad(country) {
|
function mullvadRegionTitle(peer) {
|
||||||
var name = String(country || "")
|
if (!peer) return "Unknown"
|
||||||
|
var city = String(peer.City || "").trim()
|
||||||
|
var country = String(peer.Country || "").trim()
|
||||||
|
if (city === "" || city === "Any") return country || String(peer.DisplayName || "Unknown")
|
||||||
|
return city
|
||||||
|
}
|
||||||
|
|
||||||
|
function mullvadRegionSubtitle(peer) {
|
||||||
|
if (!peer) return ""
|
||||||
|
return String(peer.Country || "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistRecentMullvad(region) {
|
||||||
|
var name = String(region || "")
|
||||||
if (name === "") return
|
if (name === "") return
|
||||||
var next = [name]
|
var next = [name]
|
||||||
for (var i = 0; i < recentMullvadCountries.length && next.length < 5; i++) {
|
for (var i = 0; i < recentMullvadRegions.length && next.length < 5; i++) {
|
||||||
var existing = String(recentMullvadCountries[i] || "")
|
var existing = String(recentMullvadRegions[i] || "")
|
||||||
if (existing !== "" && existing !== name && next.indexOf(existing) === -1) next.push(existing)
|
if (existing !== "" && existing !== name && next.indexOf(existing) === -1) next.push(existing)
|
||||||
}
|
}
|
||||||
if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return
|
if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return
|
||||||
var entry = { id: root.moduleName }
|
var entry = { id: root.moduleName }
|
||||||
for (var key in settings) if (key !== "id") entry[key] = settings[key]
|
for (var key in settings) if (key !== "id") entry[key] = settings[key]
|
||||||
entry.recentMullvadCountries = next
|
entry.recentMullvadRegions = next
|
||||||
root.bar.shell.updateEntryInline(root.moduleName, entry)
|
root.bar.shell.updateEntryInline(root.moduleName, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +159,11 @@ Panel {
|
|||||||
if (!peer) return
|
if (!peer) return
|
||||||
if (peer.AddMullvad === true) {
|
if (peer.AddMullvad === true) {
|
||||||
mullvadPickerOpen = !mullvadPickerOpen
|
mullvadPickerOpen = !mullvadPickerOpen
|
||||||
|
mullvadRegionIndex = 0
|
||||||
if (mullvadPickerOpen) Qt.callLater(function() { if (mullvadSearch) mullvadSearch.forceActiveFocus() })
|
if (mullvadPickerOpen) Qt.callLater(function() { if (mullvadSearch) mullvadSearch.forceActiveFocus() })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (peer.Mullvad === true) persistRecentMullvad(peer.Country)
|
if (peer.Mullvad === true) persistRecentMullvad(mullvadRegionKey(peer))
|
||||||
tailscale.setExitNode(peer)
|
tailscale.setExitNode(peer)
|
||||||
mullvadPickerOpen = false
|
mullvadPickerOpen = false
|
||||||
}
|
}
|
||||||
@@ -150,6 +179,7 @@ Panel {
|
|||||||
if (accountIndex >= tailscale.accounts.length) accountIndex = Math.max(0, tailscale.accounts.length - 1)
|
if (accountIndex >= tailscale.accounts.length) accountIndex = Math.max(0, tailscale.accounts.length - 1)
|
||||||
if (peerIndex >= tailscale.peers.length) peerIndex = Math.max(0, tailscale.peers.length - 1)
|
if (peerIndex >= tailscale.peers.length) peerIndex = Math.max(0, tailscale.peers.length - 1)
|
||||||
if (exitNodeIndex >= exitNodes.length) exitNodeIndex = Math.max(0, exitNodes.length - 1)
|
if (exitNodeIndex >= exitNodes.length) exitNodeIndex = Math.max(0, exitNodes.length - 1)
|
||||||
|
if (mullvadRegionIndex >= filteredMullvadRegions.length) mullvadRegionIndex = Math.max(0, filteredMullvadRegions.length - 1)
|
||||||
if (focusSection === "auth" && !tailscale.accountsAccessDenied) focusSection = tailscale.accounts.length > 1 ? "accounts" : (showExitNodes ? "exitNodes" : (showPeers ? "peers" : "header"))
|
if (focusSection === "auth" && !tailscale.accountsAccessDenied) focusSection = tailscale.accounts.length > 1 ? "accounts" : (showExitNodes ? "exitNodes" : (showPeers ? "peers" : "header"))
|
||||||
if (focusSection === "accounts" && tailscale.accounts.length <= 1) focusSection = tailscale.accountsAccessDenied ? "auth" : (showExitNodes ? "exitNodes" : (showPeers ? "peers" : "header"))
|
if (focusSection === "accounts" && tailscale.accounts.length <= 1) focusSection = tailscale.accountsAccessDenied ? "auth" : (showExitNodes ? "exitNodes" : (showPeers ? "peers" : "header"))
|
||||||
if (focusSection === "peers" && !showPeers) focusSection = showExitNodes ? "exitNodes" : (tailscale.accountsAccessDenied ? "auth" : (tailscale.accounts.length > 1 ? "accounts" : "header"))
|
if (focusSection === "peers" && !showPeers) focusSection = showExitNodes ? "exitNodes" : (tailscale.accountsAccessDenied ? "auth" : (tailscale.accounts.length > 1 ? "accounts" : "header"))
|
||||||
@@ -219,6 +249,18 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveMullvadRegionCursor(delta) {
|
||||||
|
if (filteredMullvadRegions.length === 0) return
|
||||||
|
cursorActive = true
|
||||||
|
mullvadRegionIndex = Math.max(0, Math.min(filteredMullvadRegions.length - 1, mullvadRegionIndex + delta))
|
||||||
|
scrollMullvadRegionCursorIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateMullvadRegionCursor() {
|
||||||
|
var region = selectedMullvadRegion()
|
||||||
|
if (region) chooseExitNode(region)
|
||||||
|
}
|
||||||
|
|
||||||
function scrollItemIntoView(item) {
|
function scrollItemIntoView(item) {
|
||||||
if (!panelFlick || !item) return
|
if (!panelFlick || !item) return
|
||||||
Qt.callLater(function() {
|
Qt.callLater(function() {
|
||||||
@@ -240,6 +282,10 @@ Panel {
|
|||||||
else if (focusSection === "exitNodes" && exitNodeColumn && exitNodeIndex >= 0 && exitNodeIndex < exitNodeColumn.children.length) scrollItemIntoView(exitNodeColumn.children[exitNodeIndex])
|
else if (focusSection === "exitNodes" && exitNodeColumn && exitNodeIndex >= 0 && exitNodeIndex < exitNodeColumn.children.length) scrollItemIntoView(exitNodeColumn.children[exitNodeIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollMullvadRegionCursorIntoView() {
|
||||||
|
if (mullvadRegionColumn && mullvadRegionIndex >= 0 && mullvadRegionIndex < mullvadRegionColumn.children.length) scrollItemIntoView(mullvadRegionColumn.children[mullvadRegionIndex])
|
||||||
|
}
|
||||||
|
|
||||||
function setPeerCursor(index) {
|
function setPeerCursor(index) {
|
||||||
cursorActive = true
|
cursorActive = true
|
||||||
focusSection = "peers"
|
focusSection = "peers"
|
||||||
@@ -282,9 +328,11 @@ Panel {
|
|||||||
}
|
}
|
||||||
onPeerIndexChanged: scrollCursorIntoView()
|
onPeerIndexChanged: scrollCursorIntoView()
|
||||||
onExitNodeIndexChanged: scrollCursorIntoView()
|
onExitNodeIndexChanged: scrollCursorIntoView()
|
||||||
|
onMullvadRegionIndexChanged: if (mullvadPickerOpen) scrollMullvadRegionCursorIntoView()
|
||||||
onShowConnectionsChanged: ensureCursor()
|
onShowConnectionsChanged: ensureCursor()
|
||||||
onShowPeersChanged: ensureCursor()
|
onShowPeersChanged: ensureCursor()
|
||||||
onShowExitNodesChanged: ensureCursor()
|
onShowExitNodesChanged: ensureCursor()
|
||||||
|
onFilteredMullvadRegionsChanged: ensureCursor()
|
||||||
|
|
||||||
Service {
|
Service {
|
||||||
id: tailscale
|
id: tailscale
|
||||||
@@ -551,16 +599,41 @@ Panel {
|
|||||||
id: mullvadSearch
|
id: mullvadSearch
|
||||||
width: parent.width
|
width: parent.width
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
placeholderText: "Search countries"
|
placeholderText: "Search regions"
|
||||||
text: root.mullvadQuery
|
text: root.mullvadQuery
|
||||||
onTextChanged: root.mullvadQuery = text
|
onTextChanged: {
|
||||||
|
root.mullvadQuery = text
|
||||||
|
root.mullvadRegionIndex = 0
|
||||||
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (root.filteredMullvadCountries.length > 0) root.chooseExitNode(root.filteredMullvadCountries[0])
|
root.activateMullvadRegionCursor()
|
||||||
|
}
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (event.key === Qt.Key_Down || event.text === "j") {
|
||||||
|
root.moveMullvadRegionCursor(1)
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_Up || event.text === "k") {
|
||||||
|
root.moveMullvadRegionCursor(-1)
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.activateMullvadRegionCursor()
|
||||||
|
event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
root.mullvadPickerOpen = false
|
||||||
|
keyCatcher.forceActiveFocus()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: root.filteredMullvadCountries.length === 0
|
visible: root.filteredMullvadRegions.length === 0
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "No Mullvad regions found."
|
text: "No Mullvad regions found."
|
||||||
color: root.dim
|
color: root.dim
|
||||||
@@ -569,12 +642,20 @@ Panel {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Column {
|
||||||
model: root.filteredMullvadCountries
|
id: mullvadRegionColumn
|
||||||
MullvadCountryRow {
|
width: parent.width
|
||||||
required property var modelData
|
spacing: Style.space(6)
|
||||||
width: parent.width
|
|
||||||
peer: modelData
|
Repeater {
|
||||||
|
model: root.filteredMullvadRegions
|
||||||
|
MullvadRegionRow {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
width: parent.width
|
||||||
|
peer: modelData
|
||||||
|
rowIndex: index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1082,18 +1163,21 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component MullvadCountryRow: CursorSurface {
|
component MullvadRegionRow: CursorSurface {
|
||||||
id: countryRow
|
id: regionRow
|
||||||
|
|
||||||
property var peer: null
|
property var peer: null
|
||||||
readonly property string countryName: peer ? String(peer.DisplayName || peer.Country || "Unknown") : "Unknown"
|
property int rowIndex: 0
|
||||||
|
readonly property string regionName: root.mullvadRegionTitle(peer)
|
||||||
|
readonly property string regionDetail: root.mullvadRegionSubtitle(peer)
|
||||||
readonly property bool activeExitNode: peer && peer.ExitNode === true
|
readonly property bool activeExitNode: peer && peer.ExitNode === true
|
||||||
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
|
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
|
||||||
|
readonly property bool selectedRegion: root.mullvadPickerOpen && root.mullvadRegionIndex === rowIndex
|
||||||
|
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
fill: root.hoverFill
|
fill: root.hoverFill
|
||||||
currentFill: root.selectedFill
|
currentFill: root.selectedFill
|
||||||
current: activeExitNode || settingExitNode
|
current: activeExitNode || settingExitNode || selectedRegion
|
||||||
implicitHeight: row.implicitHeight + Style.spacing.lg
|
implicitHeight: row.implicitHeight + Style.spacing.lg
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -1107,7 +1191,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: ""
|
text: ""
|
||||||
color: countryRow.current ? root.foreground : root.dim
|
color: regionRow.current ? root.foreground : root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.body
|
font.pixelSize: Style.font.body
|
||||||
width: Style.space(22)
|
width: Style.space(22)
|
||||||
@@ -1122,17 +1206,18 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: countryRow.countryName
|
text: regionRow.regionName
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.body
|
font.pixelSize: Style.font.body
|
||||||
font.bold: countryRow.activeExitNode
|
font.bold: regionRow.activeExitNode
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: peer && peer.City === "Any" ? "Best available city" : String(peer && peer.City ? peer.City : "")
|
text: regionRow.regionDetail
|
||||||
|
visible: text !== ""
|
||||||
color: root.dim
|
color: root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
@@ -1145,7 +1230,8 @@ Panel {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: root.chooseExitNode(countryRow.peer)
|
onEntered: root.mullvadRegionIndex = regionRow.rowIndex
|
||||||
|
onClicked: root.chooseExitNode(regionRow.peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Item {
|
|||||||
property var exitNodes: []
|
property var exitNodes: []
|
||||||
property var tailnetExitNodes: []
|
property var tailnetExitNodes: []
|
||||||
property var mullvadExitNodes: []
|
property var mullvadExitNodes: []
|
||||||
property var mullvadCountries: []
|
property var mullvadRegions: []
|
||||||
property var accounts: []
|
property var accounts: []
|
||||||
property string selectedAccountId: ""
|
property string selectedAccountId: ""
|
||||||
property string selectedAccountLabel: ""
|
property string selectedAccountLabel: ""
|
||||||
@@ -173,7 +173,7 @@ Item {
|
|||||||
exitNodes = []
|
exitNodes = []
|
||||||
tailnetExitNodes = []
|
tailnetExitNodes = []
|
||||||
mullvadExitNodes = []
|
mullvadExitNodes = []
|
||||||
mullvadCountries = []
|
mullvadRegions = []
|
||||||
accounts = []
|
accounts = []
|
||||||
selectedAccountId = ""
|
selectedAccountId = ""
|
||||||
selectedAccountLabel = ""
|
selectedAccountLabel = ""
|
||||||
@@ -205,7 +205,7 @@ Item {
|
|||||||
selfIp = parsed.selfIp
|
selfIp = parsed.selfIp
|
||||||
peers = parsed.running ? parsed.peers : []
|
peers = parsed.running ? parsed.peers : []
|
||||||
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
||||||
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadCountries) : []
|
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||||
|
|
||||||
if (needsLogin) statusText = "Needs login"
|
if (needsLogin) statusText = "Needs login"
|
||||||
else if (running) {
|
else if (running) {
|
||||||
@@ -232,8 +232,8 @@ Item {
|
|||||||
|
|
||||||
function parseMullvadExitNodes(raw) {
|
function parseMullvadExitNodes(raw) {
|
||||||
mullvadExitNodes = Model.parseExitNodeList(raw)
|
mullvadExitNodes = Model.parseExitNodeList(raw)
|
||||||
mullvadCountries = Model.mullvadCountryOptions(mullvadExitNodes)
|
mullvadRegions = Model.mullvadRegionOptions(mullvadExitNodes)
|
||||||
exitNodes = running ? tailnetExitNodes.concat(mullvadCountries) : []
|
exitNodes = running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTailscale() {
|
function toggleTailscale() {
|
||||||
@@ -269,6 +269,10 @@ Item {
|
|||||||
|
|
||||||
function exitNodeTarget(peer) {
|
function exitNodeTarget(peer) {
|
||||||
if (!peer) return ""
|
if (!peer) return ""
|
||||||
|
if (peer.Mullvad === true) {
|
||||||
|
var mullvadIps = filterIPv4(peer.TailscaleIPs || [])
|
||||||
|
if (mullvadIps.length > 0) return mullvadIps[0]
|
||||||
|
}
|
||||||
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
||||||
if (peer.HostName) return String(peer.HostName)
|
if (peer.HostName) return String(peer.HostName)
|
||||||
var ips = filterIPv4(peer.TailscaleIPs || [])
|
var ips = filterIPv4(peer.TailscaleIPs || [])
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ const mullvadNodes = tailscale.parseExitNodeList(`
|
|||||||
IP HOSTNAME COUNTRY CITY STATUS
|
IP HOSTNAME COUNTRY CITY STATUS
|
||||||
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Any -
|
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Any -
|
||||||
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Adelaide -
|
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Adelaide -
|
||||||
|
100.70.240.117 au-bne-wg-301.mullvad.ts.net Australia Brisbane -
|
||||||
100.66.11.119 dk-cph-wg-001.mullvad.ts.net Denmark Copenhagen -
|
100.66.11.119 dk-cph-wg-001.mullvad.ts.net Denmark Copenhagen -
|
||||||
|
100.101.10.10 us-chi-wg-001.mullvad.ts.net United States Chicago -
|
||||||
|
100.102.10.10 us-nyc-wg-001.mullvad.ts.net United States New York -
|
||||||
100.1.2.3 office.tailnet.ts.net Denmark Office -
|
100.1.2.3 office.tailnet.ts.net Denmark Office -
|
||||||
|
|
||||||
# To use an exit node, use tailscale set --exit-node=
|
# To use an exit node, use tailscale set --exit-node=
|
||||||
@@ -95,21 +98,26 @@ const mullvadNodes = tailscale.parseExitNodeList(`
|
|||||||
|
|
||||||
assertDeepEqual(
|
assertDeepEqual(
|
||||||
mullvadNodes.map(node => node.DisplayName),
|
mullvadNodes.map(node => node.DisplayName),
|
||||||
['Adelaide, Australia', 'Copenhagen, Denmark'],
|
['Adelaide, Australia', 'Brisbane, Australia', 'Copenhagen, Denmark', 'Chicago, United States', 'New York, United States'],
|
||||||
'tailscale parses Mullvad exit nodes and skips duplicate country rows'
|
'tailscale parses Mullvad exit nodes and skips duplicate country rows'
|
||||||
)
|
)
|
||||||
assertEqual(mullvadNodes[1].DNSName, 'dk-cph-wg-001.mullvad.ts.net', 'tailscale preserves Mullvad hostname as exit node target')
|
assertEqual(mullvadNodes[2].DNSName, 'dk-cph-wg-001.mullvad.ts.net', 'tailscale preserves Mullvad hostname as exit node target')
|
||||||
assertDeepEqual(mullvadNodes[1].TailscaleIPs, ['100.66.11.119'], 'tailscale preserves Mullvad exit node IP')
|
assertDeepEqual(mullvadNodes[2].TailscaleIPs, ['100.66.11.119'], 'tailscale preserves Mullvad exit node IP')
|
||||||
assert(mullvadNodes.every(node => node.Mullvad === true && node.ExitNodeOption === true), 'tailscale marks Mullvad rows as exit nodes')
|
assert(mullvadNodes.every(node => node.Mullvad === true && node.ExitNodeOption === true), 'tailscale marks Mullvad rows as exit nodes')
|
||||||
|
|
||||||
const mullvadCountries = tailscale.mullvadCountryOptions(mullvadNodes)
|
const mullvadRegions = tailscale.mullvadRegionOptions(mullvadNodes)
|
||||||
assertDeepEqual(
|
assertDeepEqual(
|
||||||
mullvadCountries.map(node => node.DisplayName),
|
mullvadRegions.map(node => node.DisplayName),
|
||||||
['Australia', 'Denmark'],
|
['Adelaide, Australia', 'Brisbane, Australia', 'Copenhagen, Denmark', 'Chicago, United States', 'New York, United States'],
|
||||||
'tailscale groups Mullvad exit nodes by country'
|
'tailscale groups Mullvad exit nodes by unique city region'
|
||||||
)
|
)
|
||||||
assertEqual(mullvadCountries[0].DNSName, 'au-adl-wg-301.mullvad.ts.net', 'tailscale uses a country endpoint for grouped countries')
|
assertDeepEqual(
|
||||||
assertEqual(mullvadCountries[1].DNSName, 'dk-cph-wg-001.mullvad.ts.net', 'tailscale falls back to first city endpoint without Any')
|
mullvadRegions.filter(node => node.Country === 'United States').map(node => node.City),
|
||||||
|
['Chicago', 'New York'],
|
||||||
|
'tailscale keeps multiple Mullvad cities within a country'
|
||||||
|
)
|
||||||
|
assertEqual(mullvadRegions[0].DNSName, 'au-adl-wg-301.mullvad.ts.net', 'tailscale uses a concrete city endpoint for grouped regions')
|
||||||
|
assertEqual(mullvadRegions[2].DNSName, 'dk-cph-wg-001.mullvad.ts.net', 'tailscale preserves first available city endpoint')
|
||||||
|
|
||||||
const stopped = tailscale.parseStatus(JSON.stringify({
|
const stopped = tailscale.parseStatus(JSON.stringify({
|
||||||
BackendState: 'Stopped',
|
BackendState: 'Stopped',
|
||||||
|
|||||||
Reference in New Issue
Block a user