From 1bd203c5555046b7ee4fbfe2f822eb3d03571ad7 Mon Sep 17 00:00:00 2001
From: SergeiEU <39683682+SergeiEU@users.noreply.github.com>
Date: Wed, 1 Apr 2026 10:17:15 +0400
Subject: Initial import
---
cmd/client/app.go | 427 ++++++++++++++++++
cmd/client/build/appicon.png | Bin 0 -> 132625 bytes
cmd/client/build/windows/icon.ico | Bin 0 -> 21677 bytes
cmd/client/build/windows/info.json | 15 +
cmd/client/build/windows/wails.exe.manifest | 15 +
cmd/client/frontend/dist/index.html | 538 +++++++++++++++++++++++
cmd/client/frontend/wailsjs/go/main/App.d.ts | 44 ++
cmd/client/frontend/wailsjs/go/main/App.js | 83 ++++
cmd/client/frontend/wailsjs/go/models.ts | 123 ++++++
cmd/client/frontend/wailsjs/runtime/package.json | 24 +
cmd/client/frontend/wailsjs/runtime/runtime.d.ts | 330 ++++++++++++++
cmd/client/frontend/wailsjs/runtime/runtime.js | 298 +++++++++++++
cmd/client/kill_other.go | 16 +
cmd/client/kill_windows.go | 56 +++
cmd/client/main.go | 96 ++++
cmd/client/wails.json | 7 +
16 files changed, 2072 insertions(+)
create mode 100644 cmd/client/app.go
create mode 100644 cmd/client/build/appicon.png
create mode 100644 cmd/client/build/windows/icon.ico
create mode 100644 cmd/client/build/windows/info.json
create mode 100644 cmd/client/build/windows/wails.exe.manifest
create mode 100644 cmd/client/frontend/dist/index.html
create mode 100755 cmd/client/frontend/wailsjs/go/main/App.d.ts
create mode 100755 cmd/client/frontend/wailsjs/go/main/App.js
create mode 100755 cmd/client/frontend/wailsjs/go/models.ts
create mode 100644 cmd/client/frontend/wailsjs/runtime/package.json
create mode 100644 cmd/client/frontend/wailsjs/runtime/runtime.d.ts
create mode 100644 cmd/client/frontend/wailsjs/runtime/runtime.js
create mode 100644 cmd/client/kill_other.go
create mode 100644 cmd/client/kill_windows.go
create mode 100644 cmd/client/main.go
create mode 100644 cmd/client/wails.json
(limited to 'cmd/client')
diff --git a/cmd/client/app.go b/cmd/client/app.go
new file mode 100644
index 0000000..8257847
--- /dev/null
+++ b/cmd/client/app.go
@@ -0,0 +1,427 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "os"
+ "os/exec"
+ "runtime"
+ "time"
+
+ wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
+
+ "vpnem/internal/config"
+ "vpnem/internal/engine"
+ "vpnem/internal/models"
+ "vpnem/internal/state"
+ syncpkg "vpnem/internal/sync"
+)
+
+const Version = "2.0.6"
+
+// App is the Wails backend.
+type App struct {
+ ctx context.Context
+ engine *engine.Engine
+ watchdog *engine.Watchdog
+ fetcher *syncpkg.Fetcher
+ updater *syncpkg.Updater
+ state *state.Store
+ log *engine.RingLog
+
+ servers []models.Server
+ ruleSets []models.RuleSet
+}
+
+// NewApp creates a new App instance.
+func NewApp(dataDir, apiURL string) *App {
+ eng := engine.New(dataDir)
+ fetcher := syncpkg.NewFetcher(apiURL)
+ rl := engine.NewRingLog(200, dataDir)
+ return &App{
+ engine: eng,
+ watchdog: engine.NewWatchdog(eng, engine.DefaultWatchdogConfig()),
+ fetcher: fetcher,
+ updater: syncpkg.NewUpdater(fetcher, Version, dataDir),
+ state: state.NewStore(dataDir),
+ log: rl,
+ }
+}
+
+// startup is called when the app starts. Must not block — Wails UI won't render until this returns.
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+ _ = a.state.Load()
+ a.logEvent("vpnem " + Version + " started")
+
+ // Sync in background, no auto-connect
+ go func() {
+ if err := a.Sync(); err != nil {
+ a.logEvent("initial sync failed: " + err.Error())
+ } else {
+ a.logEvent("initial sync ok")
+ }
+
+ // Periodic sync
+ ticker := time.NewTicker(30 * time.Minute)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ if err := a.Sync(); err != nil {
+ a.logEvent("sync failed: " + err.Error())
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+}
+
+// shutdown is called when the app closes.
+func (a *App) shutdown(ctx context.Context) {
+ a.logEvent("shutting down")
+ a.watchdog.StopWatching()
+ if err := a.engine.Stop(); err != nil {
+ a.logEvent("engine stop error: " + err.Error())
+ }
+ // Fallback: kill any orphaned sing-box
+ killSingbox()
+ _ = a.state.Save()
+ a.logEvent("shutdown complete")
+ a.log.Close()
+}
+
+func killSingbox() {
+ if runtime.GOOS == "windows" {
+ exec.Command("taskkill", "/F", "/IM", "sing-box.exe").Run()
+ } else {
+ exec.Command("pkill", "-f", "sing-box").Run()
+ }
+}
+
+func (a *App) logEvent(msg string) {
+ line := time.Now().Format("15:04:05") + " " + msg
+ a.log.Add(line)
+ log.Println(msg)
+}
+
+// --- Wails bindings ---
+
+// Sync fetches servers and rulesets from the API.
+func (a *App) Sync() error {
+ serversResp, err := a.fetcher.FetchServers()
+ if err != nil {
+ return fmt.Errorf("sync servers: %w", err)
+ }
+ a.servers = serversResp.Servers
+
+ rsResp, err := a.fetcher.FetchRuleSets()
+ if err != nil {
+ return fmt.Errorf("sync rulesets: %w", err)
+ }
+ a.ruleSets = rsResp.RuleSets
+
+ a.state.SetLastSync(time.Now())
+ _ = a.state.Save()
+ a.logEvent(fmt.Sprintf("synced: %d servers, %d rulesets", len(a.servers), len(a.ruleSets)))
+
+ // Notify frontend to refresh
+ if a.ctx != nil {
+ wailsRuntime.EventsEmit(a.ctx, "synced")
+ }
+ return nil
+}
+
+// Connect starts the VPN with the given server and mode.
+func (a *App) Connect(serverTag, modeName string) error {
+ server := a.findServer(serverTag)
+ if server == nil {
+ return fmt.Errorf("server not found: %s", serverTag)
+ }
+ mode := config.ModeByName(modeName)
+ if mode == nil {
+ return fmt.Errorf("mode not found: %s", modeName)
+ }
+
+ serverIPs := syncpkg.ServerIPs(a.servers)
+ activeRuleSets := a.activeRuleSets(*mode)
+ customBypass := a.state.GetCustomBypass()
+
+ a.logEvent("connecting: " + serverTag + " [" + modeName + "]")
+
+ // Flush DNS cache before connecting (Windows caches poisoned responses)
+ flushDNS()
+
+ if err := a.engine.RestartFull(*server, *mode, activeRuleSets, serverIPs, customBypass); err != nil {
+ a.logEvent("connect failed: " + err.Error())
+ return err
+ }
+
+ a.watchdog.StartWatching(*server, *mode, activeRuleSets, serverIPs)
+ a.state.SetServer(serverTag)
+ a.state.SetMode(modeName)
+ _ = a.state.Save()
+ a.logEvent("connected: " + serverTag)
+
+ // Validate connection in background
+ go a.validateConnection()
+ return nil
+}
+
+func flushDNS() {
+ if runtime.GOOS == "windows" {
+ exec.Command("ipconfig", "/flushdns").Run()
+ log.Println("DNS cache flushed")
+ }
+}
+
+func (a *App) validateConnection() {
+ time.Sleep(3 * time.Second)
+ if !a.engine.IsRunning() {
+ return
+ }
+
+ // Check exit IP
+ ip := a.GetExitIP()
+ if ip != "" {
+ a.logEvent("exit IP: " + ip)
+ } else {
+ a.logEvent("WARNING: could not verify exit IP")
+ }
+
+ // Check blocked site
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Head("https://rutracker.org")
+ if err == nil {
+ resp.Body.Close()
+ a.logEvent(fmt.Sprintf("validation: rutracker.org → %d OK", resp.StatusCode))
+ } else {
+ a.logEvent("validation: rutracker.org FAILED — " + err.Error())
+ }
+}
+
+// Disconnect stops the VPN and clears system proxy.
+func (a *App) Disconnect() error {
+ a.watchdog.StopWatching()
+ clearSystemProxy()
+ a.logEvent("disconnected")
+ return a.engine.Stop()
+}
+
+// SetSystemProxy sets Windows system SOCKS5 proxy directly (no TUN needed).
+// Fallback for when TUN/sing-box doesn't work with browser.
+func (a *App) SetSystemProxy(serverTag string) error {
+ server := a.findServer(serverTag)
+ if server == nil {
+ return fmt.Errorf("server not found: %s", serverTag)
+ }
+ addr := fmt.Sprintf("%s:%d", server.Server, server.ServerPort)
+ if runtime.GOOS == "windows" {
+ // Set SOCKS proxy via registry
+ exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
+ "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "1", "/f").Run()
+ exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
+ "/v", "ProxyServer", "/t", "REG_SZ", "/d", "socks="+addr, "/f").Run()
+ a.logEvent("system proxy set: " + addr)
+ }
+ return nil
+}
+
+func clearSystemProxy() {
+ if runtime.GOOS == "windows" {
+ exec.Command("reg", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
+ "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f").Run()
+ log.Println("system proxy cleared")
+ }
+}
+
+// GetServers returns the server list grouped by region.
+func (a *App) GetServers() []models.Server {
+ if a.servers == nil {
+ return []models.Server{}
+ }
+ return a.servers
+}
+
+// GetModes returns all available mode names.
+func (a *App) GetModes() []string {
+ return config.ModeNames()
+}
+
+// GetStatus returns the current connection status.
+func (a *App) GetStatus() map[string]any {
+ st := a.state.Get()
+ return map[string]any{
+ "connected": a.engine.IsRunning(),
+ "server": st.SelectedServer,
+ "mode": st.SelectedMode,
+ "lastSync": st.LastSync,
+ }
+}
+
+// GetExitIP checks the actual exit IP through the proxy.
+func (a *App) GetExitIP() string {
+ client := &http.Client{Timeout: 5 * time.Second}
+ resp, err := client.Get("http://ifconfig.me/ip")
+ if err != nil {
+ return ""
+ }
+ defer resp.Body.Close()
+ buf := make([]byte, 64)
+ n, _ := resp.Body.Read(buf)
+ return string(buf[:n])
+}
+
+// SetAutoConnect updates the auto-connect setting.
+func (a *App) SetAutoConnect(v bool) {
+ a.state.SetAutoConnect(v)
+ _ = a.state.Save()
+}
+
+// GetRuleSets returns all rule-sets with their enabled status.
+func (a *App) GetRuleSets() []map[string]any {
+ result := make([]map[string]any, 0)
+ for _, rs := range a.ruleSets {
+ enabled := !rs.Optional || a.state.IsRuleSetEnabled(rs.Tag)
+ result = append(result, map[string]any{
+ "tag": rs.Tag,
+ "description": rs.Description,
+ "type": rs.Type,
+ "optional": rs.Optional,
+ "enabled": enabled,
+ })
+ }
+ return result
+}
+
+// SetRuleSetEnabled enables or disables an optional rule-set.
+func (a *App) SetRuleSetEnabled(tag string, enabled bool) {
+ a.state.SetRuleSetEnabled(tag, enabled)
+ _ = a.state.Save()
+}
+
+// GetBypassProcesses returns default + custom bypass processes.
+func (a *App) GetBypassProcesses() map[string]any {
+ return map[string]any{
+ "default": config.BypassProcesses,
+ "custom": a.state.GetCustomBypass(),
+ }
+}
+
+// AddBypassProcess adds a custom bypass process.
+func (a *App) AddBypassProcess(name string) {
+ current := a.state.GetCustomBypass()
+ for _, p := range current {
+ if p == name {
+ return
+ }
+ }
+ a.state.SetCustomBypass(append(current, name))
+ _ = a.state.Save()
+}
+
+// RemoveBypassProcess removes a custom bypass process.
+func (a *App) RemoveBypassProcess(name string) {
+ current := a.state.GetCustomBypass()
+ var filtered []string
+ for _, p := range current {
+ if p != name {
+ filtered = append(filtered, p)
+ }
+ }
+ a.state.SetCustomBypass(filtered)
+ _ = a.state.Save()
+}
+
+// MeasureLatency pings all servers and returns sorted results.
+func (a *App) MeasureLatency() []syncpkg.LatencyResult {
+ a.logEvent("measuring latency...")
+ results := syncpkg.MeasureLatency(a.servers, 3*time.Second)
+ for _, r := range results {
+ if r.Latency >= 0 {
+ a.logEvent(fmt.Sprintf(" %s: %dms", r.Tag, r.Latency))
+ }
+ }
+ return results
+}
+
+// GetLogs returns the last N log lines.
+func (a *App) GetLogs() []string {
+ return a.log.Lines()
+}
+
+// GetGeneratedConfig returns the current sing-box config JSON for diagnostics.
+func (a *App) GetGeneratedConfig() string {
+ path := a.engine.ConfigPath()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+// CheckUpdate checks if a new version is available.
+func (a *App) CheckUpdate() (*syncpkg.UpdateInfo, error) {
+ return a.updater.Check()
+}
+
+// DownloadUpdate downloads the new binary. Returns the path.
+func (a *App) DownloadUpdate() (string, error) {
+ return a.updater.Download()
+}
+
+// RandomNLServer picks a random non-RU server tag.
+func (a *App) RandomNLServer() string {
+ var candidates []string
+ for _, s := range a.servers {
+ if s.Region != "RU" {
+ candidates = append(candidates, s.Tag)
+ }
+ }
+ if len(candidates) == 0 {
+ return ""
+ }
+ return candidates[rand.Intn(len(candidates))]
+}
+
+func (a *App) findServer(tag string) *models.Server {
+ for _, s := range a.servers {
+ if s.Tag == tag {
+ return &s
+ }
+ }
+ return nil
+}
+
+// activeRuleSets returns rule-sets for the mode PLUS domain rule-sets
+// always needed for DNS anti-poisoning (refilter-domains etc).
+func (a *App) activeRuleSets(mode config.Mode) []models.RuleSet {
+ needed := make(map[string]bool)
+
+ // Rule-sets referenced by route rules
+ for _, r := range mode.Rules {
+ for _, tag := range r.RuleSet {
+ needed[tag] = true
+ }
+ }
+
+ // Always include domain-type rule-sets for DNS rules
+ // (prevents ISP DNS poisoning for blocked domains)
+ for _, rs := range a.ruleSets {
+ if !rs.Optional && rs.Type == "domain" {
+ needed[rs.Tag] = true
+ }
+ }
+
+ var result []models.RuleSet
+ for _, rs := range a.ruleSets {
+ if needed[rs.Tag] {
+ result = append(result, rs)
+ }
+ }
+ return result
+}
diff --git a/cmd/client/build/appicon.png b/cmd/client/build/appicon.png
new file mode 100644
index 0000000..63617fe
Binary files /dev/null and b/cmd/client/build/appicon.png differ
diff --git a/cmd/client/build/windows/icon.ico b/cmd/client/build/windows/icon.ico
new file mode 100644
index 0000000..bfa0690
Binary files /dev/null and b/cmd/client/build/windows/icon.ico differ
diff --git a/cmd/client/build/windows/info.json b/cmd/client/build/windows/info.json
new file mode 100644
index 0000000..9727946
--- /dev/null
+++ b/cmd/client/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/cmd/client/build/windows/wails.exe.manifest b/cmd/client/build/windows/wails.exe.manifest
new file mode 100644
index 0000000..d82139b
--- /dev/null
+++ b/cmd/client/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/cmd/client/frontend/dist/index.html b/cmd/client/frontend/dist/index.html
new file mode 100644
index 0000000..6a77f85
--- /dev/null
+++ b/cmd/client/frontend/dist/index.html
@@ -0,0 +1,538 @@
+
+
+
+
+
+ vpnem
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-connect on launch
+
+
+
+
+
+
+
+
+
Built-in exclusions
+
+
Custom exclusions
+
+
+
+
+
+
+
+
+
+
Latency
+
+
Log
+
No logs
+
+
+
+
+
+
+
+
+
diff --git a/cmd/client/frontend/wailsjs/go/main/App.d.ts b/cmd/client/frontend/wailsjs/go/main/App.d.ts
new file mode 100755
index 0000000..72187a0
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/go/main/App.d.ts
@@ -0,0 +1,44 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {sync} from '../models';
+import {models} from '../models';
+
+export function AddBypassProcess(arg1:string):Promise;
+
+export function CheckUpdate():Promise;
+
+export function Connect(arg1:string,arg2:string):Promise;
+
+export function Disconnect():Promise;
+
+export function DownloadUpdate():Promise;
+
+export function GetBypassProcesses():Promise>;
+
+export function GetExitIP():Promise;
+
+export function GetGeneratedConfig():Promise;
+
+export function GetLogs():Promise>;
+
+export function GetModes():Promise>;
+
+export function GetRuleSets():Promise>>;
+
+export function GetServers():Promise>;
+
+export function GetStatus():Promise>;
+
+export function MeasureLatency():Promise>;
+
+export function RandomNLServer():Promise;
+
+export function RemoveBypassProcess(arg1:string):Promise;
+
+export function SetAutoConnect(arg1:boolean):Promise;
+
+export function SetRuleSetEnabled(arg1:string,arg2:boolean):Promise;
+
+export function SetSystemProxy(arg1:string):Promise;
+
+export function Sync():Promise;
diff --git a/cmd/client/frontend/wailsjs/go/main/App.js b/cmd/client/frontend/wailsjs/go/main/App.js
new file mode 100755
index 0000000..917c8f0
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/go/main/App.js
@@ -0,0 +1,83 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function AddBypassProcess(arg1) {
+ return window['go']['main']['App']['AddBypassProcess'](arg1);
+}
+
+export function CheckUpdate() {
+ return window['go']['main']['App']['CheckUpdate']();
+}
+
+export function Connect(arg1, arg2) {
+ return window['go']['main']['App']['Connect'](arg1, arg2);
+}
+
+export function Disconnect() {
+ return window['go']['main']['App']['Disconnect']();
+}
+
+export function DownloadUpdate() {
+ return window['go']['main']['App']['DownloadUpdate']();
+}
+
+export function GetBypassProcesses() {
+ return window['go']['main']['App']['GetBypassProcesses']();
+}
+
+export function GetExitIP() {
+ return window['go']['main']['App']['GetExitIP']();
+}
+
+export function GetGeneratedConfig() {
+ return window['go']['main']['App']['GetGeneratedConfig']();
+}
+
+export function GetLogs() {
+ return window['go']['main']['App']['GetLogs']();
+}
+
+export function GetModes() {
+ return window['go']['main']['App']['GetModes']();
+}
+
+export function GetRuleSets() {
+ return window['go']['main']['App']['GetRuleSets']();
+}
+
+export function GetServers() {
+ return window['go']['main']['App']['GetServers']();
+}
+
+export function GetStatus() {
+ return window['go']['main']['App']['GetStatus']();
+}
+
+export function MeasureLatency() {
+ return window['go']['main']['App']['MeasureLatency']();
+}
+
+export function RandomNLServer() {
+ return window['go']['main']['App']['RandomNLServer']();
+}
+
+export function RemoveBypassProcess(arg1) {
+ return window['go']['main']['App']['RemoveBypassProcess'](arg1);
+}
+
+export function SetAutoConnect(arg1) {
+ return window['go']['main']['App']['SetAutoConnect'](arg1);
+}
+
+export function SetRuleSetEnabled(arg1, arg2) {
+ return window['go']['main']['App']['SetRuleSetEnabled'](arg1, arg2);
+}
+
+export function SetSystemProxy(arg1) {
+ return window['go']['main']['App']['SetSystemProxy'](arg1);
+}
+
+export function Sync() {
+ return window['go']['main']['App']['Sync']();
+}
diff --git a/cmd/client/frontend/wailsjs/go/models.ts b/cmd/client/frontend/wailsjs/go/models.ts
new file mode 100755
index 0000000..241727b
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/go/models.ts
@@ -0,0 +1,123 @@
+export namespace models {
+
+ export class Transport {
+ type?: string;
+ path?: string;
+
+ static createFrom(source: any = {}) {
+ return new Transport(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.type = source["type"];
+ this.path = source["path"];
+ }
+ }
+ export class TLS {
+ enabled: boolean;
+ server_name?: string;
+
+ static createFrom(source: any = {}) {
+ return new TLS(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.enabled = source["enabled"];
+ this.server_name = source["server_name"];
+ }
+ }
+ export class Server {
+ tag: string;
+ region: string;
+ type: string;
+ server: string;
+ server_port: number;
+ udp_over_tcp?: boolean;
+ uuid?: string;
+ method?: string;
+ password?: string;
+ tls?: TLS;
+ transport?: Transport;
+
+ static createFrom(source: any = {}) {
+ return new Server(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.tag = source["tag"];
+ this.region = source["region"];
+ this.type = source["type"];
+ this.server = source["server"];
+ this.server_port = source["server_port"];
+ this.udp_over_tcp = source["udp_over_tcp"];
+ this.uuid = source["uuid"];
+ this.method = source["method"];
+ this.password = source["password"];
+ this.tls = this.convertValues(source["tls"], TLS);
+ this.transport = this.convertValues(source["transport"], Transport);
+ }
+
+ convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+ }
+ }
+
+
+}
+
+export namespace sync {
+
+ export class LatencyResult {
+ tag: string;
+ region: string;
+ latency_ms: number;
+
+ static createFrom(source: any = {}) {
+ return new LatencyResult(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.tag = source["tag"];
+ this.region = source["region"];
+ this.latency_ms = source["latency_ms"];
+ }
+ }
+ export class UpdateInfo {
+ available: boolean;
+ version: string;
+ changelog: string;
+ current_version: string;
+
+ static createFrom(source: any = {}) {
+ return new UpdateInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.available = source["available"];
+ this.version = source["version"];
+ this.changelog = source["changelog"];
+ this.current_version = source["current_version"];
+ }
+ }
+
+}
+
diff --git a/cmd/client/frontend/wailsjs/runtime/package.json b/cmd/client/frontend/wailsjs/runtime/package.json
new file mode 100644
index 0000000..1e7c8a5
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/cmd/client/frontend/wailsjs/runtime/runtime.d.ts b/cmd/client/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 0000000..3bbea84
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,330 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
+
+// Notification types
+export interface NotificationOptions {
+ id: string;
+ title: string;
+ subtitle?: string; // macOS and Linux only
+ body?: string;
+ categoryId?: string;
+ data?: { [key: string]: any };
+}
+
+export interface NotificationAction {
+ id?: string;
+ title?: string;
+ destructive?: boolean; // macOS-specific
+}
+
+export interface NotificationCategory {
+ id?: string;
+ actions?: NotificationAction[];
+ hasReplyField?: boolean;
+ replyPlaceholder?: string;
+ replyButtonTitle?: string;
+}
+
+// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
+// Initializes the notification service for the application.
+// This must be called before sending any notifications.
+export function InitializeNotifications(): Promise;
+
+// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
+// Cleans up notification resources and releases any held connections.
+export function CleanupNotifications(): Promise;
+
+// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
+// Checks if notifications are available on the current platform.
+export function IsNotificationAvailable(): Promise;
+
+// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
+// Requests notification authorization from the user (macOS only).
+export function RequestNotificationAuthorization(): Promise;
+
+// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
+// Checks the current notification authorization status (macOS only).
+export function CheckNotificationAuthorization(): Promise;
+
+// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
+// Sends a basic notification with the given options.
+export function SendNotification(options: NotificationOptions): Promise;
+
+// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
+// Sends a notification with action buttons. Requires a registered category.
+export function SendNotificationWithActions(options: NotificationOptions): Promise;
+
+// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
+// Registers a notification category that can be used with SendNotificationWithActions.
+export function RegisterNotificationCategory(category: NotificationCategory): Promise;
+
+// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
+// Removes a previously registered notification category.
+export function RemoveNotificationCategory(categoryId: string): Promise;
+
+// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
+// Removes all pending notifications from the notification center.
+export function RemoveAllPendingNotifications(): Promise;
+
+// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
+// Removes a specific pending notification by its identifier.
+export function RemovePendingNotification(identifier: string): Promise;
+
+// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
+// Removes all delivered notifications from the notification center.
+export function RemoveAllDeliveredNotifications(): Promise;
+
+// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
+// Removes a specific delivered notification by its identifier.
+export function RemoveDeliveredNotification(identifier: string): Promise;
+
+// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
+// Removes a notification by its identifier (cross-platform convenience function).
+export function RemoveNotification(identifier: string): Promise;
\ No newline at end of file
diff --git a/cmd/client/frontend/wailsjs/runtime/runtime.js b/cmd/client/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 0000000..556621e
--- /dev/null
+++ b/cmd/client/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,298 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
+
+export function InitializeNotifications() {
+ return window.runtime.InitializeNotifications();
+}
+
+export function CleanupNotifications() {
+ return window.runtime.CleanupNotifications();
+}
+
+export function IsNotificationAvailable() {
+ return window.runtime.IsNotificationAvailable();
+}
+
+export function RequestNotificationAuthorization() {
+ return window.runtime.RequestNotificationAuthorization();
+}
+
+export function CheckNotificationAuthorization() {
+ return window.runtime.CheckNotificationAuthorization();
+}
+
+export function SendNotification(options) {
+ return window.runtime.SendNotification(options);
+}
+
+export function SendNotificationWithActions(options) {
+ return window.runtime.SendNotificationWithActions(options);
+}
+
+export function RegisterNotificationCategory(category) {
+ return window.runtime.RegisterNotificationCategory(category);
+}
+
+export function RemoveNotificationCategory(categoryId) {
+ return window.runtime.RemoveNotificationCategory(categoryId);
+}
+
+export function RemoveAllPendingNotifications() {
+ return window.runtime.RemoveAllPendingNotifications();
+}
+
+export function RemovePendingNotification(identifier) {
+ return window.runtime.RemovePendingNotification(identifier);
+}
+
+export function RemoveAllDeliveredNotifications() {
+ return window.runtime.RemoveAllDeliveredNotifications();
+}
+
+export function RemoveDeliveredNotification(identifier) {
+ return window.runtime.RemoveDeliveredNotification(identifier);
+}
+
+export function RemoveNotification(identifier) {
+ return window.runtime.RemoveNotification(identifier);
+}
\ No newline at end of file
diff --git a/cmd/client/kill_other.go b/cmd/client/kill_other.go
new file mode 100644
index 0000000..75ac0a5
--- /dev/null
+++ b/cmd/client/kill_other.go
@@ -0,0 +1,16 @@
+//go:build !windows
+
+package main
+
+import (
+ "log"
+ "os/exec"
+ "time"
+)
+
+func killPrevious() {
+ log.Println("killing previous instances")
+ exec.Command("pkill", "-f", "sing-box").Run()
+ // Don't pkill vpnem — we ARE vpnem
+ time.Sleep(500 * time.Millisecond)
+}
diff --git a/cmd/client/kill_windows.go b/cmd/client/kill_windows.go
new file mode 100644
index 0000000..ba8c75a
--- /dev/null
+++ b/cmd/client/kill_windows.go
@@ -0,0 +1,56 @@
+//go:build windows
+
+package main
+
+import (
+ "log"
+ "os"
+ "os/exec"
+ "strconv"
+ "time"
+)
+
+func killPrevious() {
+ myPID := os.Getpid()
+ log.Printf("killing previous instances (my PID: %d)", myPID)
+
+ // Kill other vpnem.exe processes (not ourselves)
+ // Use WMIC to find PIDs, then taskkill each except ours
+ out, _ := exec.Command("wmic", "process", "where",
+ "name='vpnem.exe'", "get", "processid", "/format:list").Output()
+ for _, line := range splitLines(string(out)) {
+ if len(line) > 10 && line[:10] == "ProcessId=" {
+ pidStr := line[10:]
+ pid, err := strconv.Atoi(pidStr)
+ if err == nil && pid != myPID {
+ log.Printf("killing old vpnem.exe PID %d", pid)
+ exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid)).Run()
+ }
+ }
+ }
+
+ // Kill any orphaned sing-box.exe
+ exec.Command("taskkill", "/F", "/IM", "sing-box.exe").Run()
+ time.Sleep(500 * time.Millisecond)
+}
+
+func splitLines(s string) []string {
+ var lines []string
+ start := 0
+ for i := 0; i < len(s); i++ {
+ if s[i] == '\n' || s[i] == '\r' {
+ line := s[start:i]
+ if len(line) > 0 && line[len(line)-1] == '\r' {
+ line = line[:len(line)-1]
+ }
+ if line != "" {
+ lines = append(lines, line)
+ }
+ start = i + 1
+ }
+ }
+ if start < len(s) {
+ lines = append(lines, s[start:])
+ }
+ return lines
+}
diff --git a/cmd/client/main.go b/cmd/client/main.go
new file mode 100644
index 0000000..0b2fd6f
--- /dev/null
+++ b/cmd/client/main.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "embed"
+ "flag"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+//go:embed frontend/dist
+var assets embed.FS
+
+func main() {
+ apiURL := flag.String("api", "https://vpn.em-sysadmin.xyz", "API server URL")
+ dataDir := flag.String("data", defaultDataDir(), "data directory")
+ flag.Parse()
+
+ // Ensure data dir exists
+ os.MkdirAll(*dataDir, 0o755)
+
+ // Setup file logging so we always have diagnostics
+ logPath := filepath.Join(*dataDir, "vpnem.log")
+ logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
+ if err == nil {
+ log.SetOutput(logFile)
+ defer logFile.Close()
+ }
+
+ log.Printf("vpnem starting, data=%s, api=%s, os=%s", *dataDir, *apiURL, runtime.GOOS)
+
+ // Kill previous instances of vpnem and sing-box
+ killPrevious()
+
+ // Check wintun.dll on Windows
+ if runtime.GOOS == "windows" {
+ exe, _ := os.Executable()
+ exeDir := filepath.Dir(exe)
+ wintunPaths := []string{
+ filepath.Join(exeDir, "wintun.dll"),
+ filepath.Join(*dataDir, "wintun.dll"),
+ "wintun.dll",
+ }
+ found := false
+ for _, p := range wintunPaths {
+ if _, err := os.Stat(p); err == nil {
+ log.Printf("wintun.dll found: %s", p)
+ found = true
+ break
+ }
+ }
+ if !found {
+ log.Printf("WARNING: wintun.dll not found! TUN will fail. Searched: %v", wintunPaths)
+ }
+ }
+
+ app := NewApp(*dataDir, *apiURL)
+
+ log.Println("starting Wails UI")
+ if err := wails.Run(&options.App{
+ Title: "vpnem",
+ Width: 480,
+ Height: 600,
+ MinWidth: 400,
+ MinHeight: 500,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ OnStartup: app.startup,
+ OnShutdown: app.shutdown,
+ Bind: []interface{}{
+ app,
+ },
+ }); err != nil {
+ log.Printf("FATAL wails error: %v", err)
+ // On Windows, also write to a visible error file
+ if runtime.GOOS == "windows" {
+ errPath := filepath.Join(*dataDir, "ERROR.txt")
+ os.WriteFile(errPath, []byte("vpnem failed to start:\n"+err.Error()+"\n\nCheck vpnem.log for details.\n"), 0o644)
+ }
+ os.Exit(1)
+ }
+}
+
+func defaultDataDir() string {
+ if runtime.GOOS == "windows" {
+ return `C:\ProgramData\vpnem`
+ }
+ home, _ := os.UserHomeDir()
+ return filepath.Join(home, ".local", "share", "vpnem")
+}
diff --git a/cmd/client/wails.json b/cmd/client/wails.json
new file mode 100644
index 0000000..0f64984
--- /dev/null
+++ b/cmd/client/wails.json
@@ -0,0 +1,7 @@
+{
+ "name": "vpnem",
+ "outputfilename": "vpnem",
+ "author": {
+ "name": "sergei"
+ }
+}
--
cgit v1.2.3