Skip to content

Commit

Permalink
add scrollable node section, group mullvad nodes by country, add exit…
Browse files Browse the repository at this point in the history
…node as subtitle
  • Loading branch information
rjocoleman committed Nov 26, 2023
1 parent c7a7f5e commit 247dc9b
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 39 deletions.
186 changes: 157 additions & 29 deletions [email protected]/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const TailscaleMenuToggle = GObject.registerClass(
toggleMode: true,
menuEnabled: true,
});

this.title = "Tailscale";
tailscale.bind_property("running", this, "checked", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

Expand All @@ -133,43 +134,100 @@ const TailscaleMenuToggle = GObject.registerClass(
// consistency with other menus.
this.menu.setHeader(icon, this.title);

tailscale.connect("notify::exit-node", () => {
this.subtitle = tailscale.exit_node ? tailscale.exit_node_name : "";
this.menu.setHeader(icon, this.title, this.subtitle);
});

// NODES
const nodes = new PopupMenu.PopupMenuSection();
const nodes = new ScrollablePopupMenu();
const update_nodes = (obj) => {
nodes.removeAll();
const mullvad = new PopupMenu.PopupSubMenuMenuItem("Mullvad", false, {});
for (const node of obj.nodes) {
const menu = (node.mullvad && !node.exit_node) ? mullvad.menu : nodes;
const device_icon = !node.online
? "network-offline-symbolic"
: ((node.os == "android" || node.os == "iOS")
? "phone-symbolic"
: (node.mullvad
? "network-vpn-symbolic"
: "computer-symbolic"));

// Prepare menu sections for non-Mullvad and Mullvad nodes
const nonMullvadSection = new PopupMenu.PopupMenuSection();
const mullvadSection = new PopupMenu.PopupMenuSection();

const nonMullvadNodes = obj.nodes.filter(node => !node.mullvad);
const mullvadNodes = obj.nodes.filter(node => node.mullvad);
const mullvadExitNode = mullvadNodes.find(node => node.exit_node);

// Treat Mullvad Exit Node differently
if (mullvadExitNode) {
const exitMenuItem = createTailscaleDeviceItem(
mullvadExitNode,
`${mullvadExitNode.location.Country} (${mullvadExitNode.name})`,
_("disable exit node"),
'network-vpn-symbolic',
tailscale
);
mullvadSection.addMenuItem(exitMenuItem);
}

// If there are mullvad nodes, group them by country
if (mullvadNodes.length > 0) {
const mullvadByCountry = mullvadNodes.reduce((acc, node) => {
const country = node.location.Country;
if (!acc[country]) acc[country] = [];
acc[country].push(node);
return acc;
}, {});

// Sort countries and create menu items
Object.keys(mullvadByCountry).sort().forEach(country => {
const countryNodes = mullvadByCountry[country];
if (countryNodes.length === 1) {
const node = countryNodes[0];
if (!node.exit_node) {
const menuItem = createTailscaleDeviceItem(
node,
`${node.location.Country} (${node.name})`,
node.exit_node ? _("disable exit node") : node.online ? _("use as exit node") : _("offline"),
'network-vpn-symbolic',
tailscale
);
mullvadSection.addMenuItem(menuItem);
}
} else {
const countrySubMenu = new PopupMenu.PopupSubMenuMenuItem(country, true);
countryNodes.forEach(node => {
const menuItem = createTailscaleDeviceItem(
node,
`${node.location.City} (${node.name})`,
node.exit_node ? _("disable exit node") : node.online ? _("use as exit node") : _("offline"),
'network-vpn-symbolic',
tailscale
);
countrySubMenu.menu.addMenuItem(menuItem);
});
mullvadSection.addMenuItem(countrySubMenu);
}
});
}

// Add non-Mullvad nodes to the nonMullvadSection
nonMullvadNodes.forEach(node => {
const deviceIcon = node.online
? (node.os === "android" || node.os === "iOS" ? "phone-symbolic" : "computer-symbolic")
: "network-offline-symbolic";
const subtitle = node.exit_node ? _("disable exit node") : (node.exit_node_option ? _("use as exit node") : "");
const onClick = node.exit_node_option ? () => { tailscale.exit_node = node.exit_node ? "" : node.id; } : null;
const onLongClick = () => {
if (!node.ips)
return false;

St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, node.ips[0]);
St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, node.ips[0]);
Main.osdWindowManager.show(-1, icon, _("IP address has been copied to the clipboard"));
return true;
};

menu.addMenuItem(new TailscaleDeviceItem(device_icon, node.name, subtitle, onClick, onLongClick));
const item = createTailscaleDeviceItem(node, node.name, subtitle, deviceIcon, tailscale);
nonMullvadSection.addMenuItem(item);
});

// Add sections to ScrollablePopupMenu with labels
if (nonMullvadNodes.length > 0) {
nodes.addSectionWithLabel(nonMullvadSection, _("Nodes"));
}
if (mullvad.menu.isEmpty()) {
mullvad.destroy();
} else {
nodes.addMenuItem(mullvad);
if (mullvadNodes.length > 0) {
nodes.addSectionWithLabel(mullvadSection, _("Mullvad Nodes"));
}
}

// Add the entire nodes section to the main menu
this.menu.addMenuItem(nodes);
};
tailscale.connect("notify::nodes", (obj) => update_nodes(obj));
update_nodes(tailscale);
this.menu.addMenuItem(nodes);

// SEPARATOR
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
Expand Down Expand Up @@ -252,3 +310,73 @@ function init(meta) {
ExtensionUtils.initTranslations(Me.metadata.uuid);
return new TailscaleExtension(meta.uuid, Me.path);
}

const ScrollablePopupMenu = GObject.registerClass(
class ScrollablePopupMenu extends PopupMenu.PopupBaseMenuItem {
_init() {
super._init({
hover: false,
activate: false,
});

this.scrollView = new St.ScrollView({
style_class: 'vfade',
hscrollbar_policy: St.PolicyType.NEVER,
vscrollbar_policy: St.PolicyType.AUTOMATIC,
enable_mouse_scrolling: true,
});

this.box = new St.BoxLayout({ width: 300, vertical: true });
this.scrollView.add_actor(this.box);
this.actor.add_actor(this.scrollView);

this.scrollView.connect('scroll-event', this._onScrollEvent.bind(this));

this.scrollView.set_height(300);

this.addSection = (section) => {
this.box.add_actor(section.actor);
};

this.removeAll = () => {
this.box.destroy_all_children();
};

this.addSectionWithLabel = (section, labelText) => {
if (labelText) {
const label = new St.Label({
text: labelText,
x_expand: true,
y_expand: true,
style_class: 'popup-menu-section-title'
});
this.box.add_actor(label);
}
this.box.add_actor(section.actor);
};
}

_onScrollEvent(actor, event) {
if (!actor.has_key_focus()) {
actor.grab_key_focus();
}
}
}
);

function createTailscaleDeviceItem(node, primaryText, secondaryText, iconName, tailscale) {
const onClick = node.exit_node_option ? () => { tailscale.exit_node = node.exit_node ? "" : node.id; } : null;
const onLongClick = () => {
if (!node.ips)
return false;

const gicon = Gio.Icon.new_for_string(iconName);
St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, node.ips[0]);
St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, node.ips[0]);
Main.osdWindowManager.show(-1, gicon, _("IP address has been copied to the clipboard"));
return true;
};

const item = new TailscaleDeviceItem(iconName, primaryText, secondaryText, onClick, onLongClick);
return item;
}
32 changes: 22 additions & 10 deletions [email protected]/tailscale.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const Tailscale = GObject.registerClass(
this._shields_up = false;
this._ssh = false;
this._exit_node = "";
this._exit_node_name = null;
this._nodes = [];
this._cancelable = new Gio.Cancellable();
this._listen();
Expand All @@ -130,16 +131,20 @@ export const Tailscale = GObject.registerClass(

_process_nodes(prefs, peers) {
const nodes = peers
.map(peer => ({
id: peer.ID,
name: peer.DNSName.split(".")[0],
os: peer.OS,
exit_node: peer.ID == prefs.ExitNodeID,
exit_node_option: peer.ExitNodeOption,
online: peer.Online,
ips: peer.TailscaleIPs,
mullvad: peer.Tags?.includes("tag:mullvad-exit-node") || false,
}))
.map(peer => {
const node = {
id: peer.ID,
name: peer.DNSName.split(".")[0],
os: peer.OS,
exit_node: peer.ID == prefs.ExitNodeID,
exit_node_option: peer.ExitNodeOption,
online: peer.Online,
ips: peer.TailscaleIPs,
mullvad: peer.Tags?.includes("tag:mullvad-exit-node") || false,
location: peer.Location,
};
return node;
})
.sort((a, b) =>
(b.exit_node - a.exit_node)
|| (b.online - a.online)
Expand All @@ -157,6 +162,8 @@ export const Tailscale = GObject.registerClass(
const exit_node_id = prefs.ExitNodeID;
if (exit_node_id != this._exit_node) {
this._exit_node = exit_node_id;
const exitNodePeer = this._peers.find(peer => peer.ID === exit_node_id);
this._exit_node_name = exitNodePeer ? exitNodePeer.DNSName.split(".")[0] : null;
this.notify("exit-node");
}
}
Expand Down Expand Up @@ -278,6 +285,10 @@ export const Tailscale = GObject.registerClass(
this._update_prefs({ ExitNodeID: value });
}

get exit_node_name() {
return this._exit_node_name;
}

get nodes() {
return this._nodes;
}
Expand Down Expand Up @@ -307,6 +318,7 @@ export const Tailscale = GObject.registerClass(
Online: peer.Online,
TailscaleIPs: peer.Addresses.map(address => address.split("/")[0]),
Tags: peer.Tags,
Location: peer.Hostinfo.Location
}));
should_update = true;
}
Expand Down

0 comments on commit 247dc9b

Please sign in to comment.