11 Commits
6 changed files with 71 additions and 43 deletions
+3 -2
View File
@@ -1,13 +1,14 @@
import { NetworkStatus } from "./types";
export const MANAGEMENT_URL = 'https://vpn.vyntehome.com';
export const GATEWAY_HOST = new URL(MANAGEMENT_URL).host;
export const APP_NAME = "Vynte Connect";
export const POLL_INTERVAL_MS = 5000;
export const WINDOW_WIDTH = 384;
export const WINDOW_HEIGHT = 420;
export const DEFAULT_STATUS = {
export const DEFAULT_STATUS: NetworkStatus = {
state: "checking",
title: `Checking ${GATEWAY_HOST}`,
message: "Preparing the Vynte access client.",
connectedSince: null,
gatewayHost: GATEWAY_HOST
};
+51 -21
View File
@@ -1,19 +1,39 @@
import { app, BrowserWindow, ipcMain, nativeImage, shell, Tray } from 'electron';
import { app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, nativeImage, shell, Tray } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
import { APP_NAME, DEFAULT_STATUS, GATEWAY_HOST, MANAGEMENT_URL, POLL_INTERVAL_MS, WINDOW_HEIGHT, WINDOW_WIDTH } from './config';
import { NetBirdClient } from './netbird';
import { baseStatus, normalizeStatus } from './status';
import { NetworkStatus, UIStatus } from './types';
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
const singleInstanceLock = app.requestSingleInstanceLock()
if (!singleInstanceLock) app.quit();
else {
app.on('second-instance', () => {
if (window) {
if (window.isMinimized()) window.restore();
window.focus();
}
});
app.on('before-quit', disconnect);
}
app.whenReady().then(() => {
createWindow();
createTray();
refreshStatus();
pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS);
});
app.on('before-quit', () => {
if (pollTimer) clearInterval(pollTimer);
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
@@ -38,7 +58,7 @@ let tray: Tray;
let window: BrowserWindow;
let pollTimer: NodeJS.Timeout;
let autoConnect = false;
let cachedStatus = { ...DEFAULT_STATUS };
let cachedStatus = DEFAULT_STATUS;
function resourcePath(name: string) {
if (app.isPackaged) {
@@ -51,7 +71,7 @@ function netbirdClient() {
return new NetBirdClient(resourcePath(process.platform === "win32" ? 'netbird.exe' : 'netbird'));
}
function withAutoConnect(status: Record<string, any>) {
function withAutoConnect(status: NetworkStatus): UIStatus {
return { ...status, autoConnect };
}
@@ -65,9 +85,28 @@ function updateTray() {
if (!tray) return;
const label = cachedStatus.state === 'connected' ? `${APP_NAME}: Connected` : APP_NAME;
tray.setToolTip(label);
const template: MenuItemConstructorOptions[] = [];
const connected = cachedStatus.state === "connected";
const busy = ['connecting', 'disconnecting'].includes(cachedStatus.state);
template.push({
label: connected ? 'Disconnect' : 'Connect',
click: connected ? disconnect : connect,
enabled: !busy
});
template.push(
{
label: 'Logout',
click: logout
},
{ role: 'quit' }
);
tray.setContextMenu(Menu.buildFromTemplate(template));
}
function setStatus(status: Record<string, any>) {
function setStatus(status: NetworkStatus) {
cachedStatus = {
...cachedStatus,
...status,
@@ -88,7 +127,9 @@ async function refreshStatus() {
}));
}
return setStatus(normalizeStatus(result.stdout, cachedStatus));
const status = normalizeStatus(result.stdout, cachedStatus);
if (status.state === "disconnected" && cachedStatus.state === "connecting") return;
setStatus(status);
}
async function connect() {
@@ -187,21 +228,10 @@ function showWindow() {
function createTray() {
const icon = nativeImage.createFromPath(resourcePath('VynteIcon.png')).resize({ width: 18, height: 18 });
tray = new Tray(icon);
tray.setToolTip(APP_NAME);
updateTray();
tray.on('click', showWindow);
}
app.whenReady().then(() => {
createWindow();
createTray();
refreshStatus();
pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS);
});
app.on('before-quit', () => {
if (pollTimer) clearInterval(pollTimer);
});
ipcMain.handle('status:get', () => withAutoConnect(cachedStatus));
ipcMain.handle('status:refresh', refreshStatus);
ipcMain.handle('vpn:connect', connect);
+2 -1
View File
@@ -2,6 +2,7 @@
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import { contextBridge, ipcRenderer } from "electron";
import { UIStatus } from "./types";
contextBridge.exposeInMainWorld('vynte', {
getStatus: () => ipcRenderer.invoke('status:get'),
@@ -12,5 +13,5 @@ contextBridge.exposeInMainWorld('vynte', {
toggleAuto: () => ipcRenderer.invoke('auto:toggle'),
quit: () => ipcRenderer.invoke('app:quit'),
openGateway: () => ipcRenderer.invoke('external:openGateway'),
onStatus: (callback: Function) => ipcRenderer.on('status', (_, status) => callback(status))
onStatus: (callback: (status: UIStatus) => void) => ipcRenderer.on('status', (_, status) => callback(status))
})
+2 -7
View File
@@ -27,7 +27,7 @@
*/
import './index.css'
import type { NetworkStatus } from './status';
import { UIStatus } from './types';
const body = document.body;
@@ -40,10 +40,6 @@ const auto = document.getElementById('auto') as HTMLElement;
const logout = document.getElementById('logout') as HTMLElement;
const quit = document.getElementById('quit') as HTMLElement;
interface UIStatus extends NetworkStatus {
autoConnect?: boolean;
}
let currentStatus: Partial<UIStatus> = {
state: 'checking',
};
@@ -72,11 +68,10 @@ function setStatus(status: UIStatus): void {
}
orb.addEventListener('click', async () => {
if (currentStatus.state === 'connected') {
if (['connected', 'connecting'].includes(currentStatus.state ?? '')) {
await window.vynte.disconnect();
} else if (
![
'connecting',
'disconnecting',
'loggingOut',
'checking',
+2 -12
View File
@@ -1,12 +1,5 @@
import { GATEWAY_HOST } from './config';
export interface NetworkStatus {
state?: string;
title?: string;
message?: string;
connectedSince: number | null;
gatewayHost: string;
}
import { NetworkStatus } from './types';
interface NetBirdStatusResponse {
daemonStatus?: string;
@@ -22,11 +15,8 @@ interface NetBirdStatusResponse {
};
}
export function baseStatus(
overrides: Partial<NetworkStatus> = {}
): NetworkStatus {
export function baseStatus(overrides: NetworkStatus): NetworkStatus {
return {
connectedSince: null,
gatewayHost: GATEWAY_HOST,
...overrides,
};
+11
View File
@@ -0,0 +1,11 @@
export interface NetworkStatus {
state: string;
title: string;
message: string;
connectedSince?: number;
gatewayHost?: string;
}
export interface UIStatus extends NetworkStatus {
autoConnect?: boolean;
}