Release 2.2.0: Happ-style multi-node list with ping and auto best connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 07:39:46 +03:00
co-authored by Cursor
parent 37e31ae742
commit 8c513f6e08
17 changed files with 527 additions and 99 deletions
+101 -13
View File
@@ -109,7 +109,7 @@ func main() {
WindowOptions: webview2.WindowOptions{
Title: "Navis 2",
Width: 500,
Height: 820,
Height: 900,
Center: true,
IconId: 1,
},
@@ -124,11 +124,12 @@ func main() {
}()
applyWindowIcon(w.Window())
w.SetSize(500, 820, webview2.HintNone)
w.SetSize(500, 900, webview2.HintNone)
mustBind(w, "getState", a.getState)
mustBind(w, "connect", a.connect)
mustBind(w, "disconnect", a.disconnect)
mustBind(w, "connectProfile", a.connectProfile)
mustBind(w, "saveProfile", a.saveProfile)
mustBind(w, "createProfile", a.createProfile)
mustBind(w, "selectProfile", a.selectProfile)
@@ -136,6 +137,7 @@ func main() {
mustBind(w, "installCore", a.installCore)
mustBind(w, "openURL", openURL)
mustBind(w, "pingServers", a.pingServers)
mustBind(w, "pingBest", a.pingBest)
mustBind(w, "checkUpdate", a.checkUpdate)
mustBind(w, "applyUpdate", a.applyUpdate)
mustBind(w, "saveHy2", a.saveHy2)
@@ -254,9 +256,31 @@ func (a *app) deleteProfile(name string) error {
}
func (a *app) connect() error {
return a.connectProfile("")
}
// connectProfile switches to the named profile (if set) and connects.
// If already connected to another profile, disconnects first.
func (a *app) connectProfile(name string) error {
a.mu.Lock()
defer a.mu.Unlock()
name = strings.TrimSpace(name)
st := a.mgr.Status()
if st.Connected {
if name == "" || st.Profile == name {
return nil
}
if err := a.mgr.Disconnect(); err != nil {
return err
}
}
if name != "" {
if err := a.mgr.SetActiveProfile(name); err != nil {
return err
}
}
if p, err := a.mgr.Config().ActiveProfile(); err != nil {
return err
} else if strings.TrimSpace(p.Proxy) == "" {
@@ -301,24 +325,88 @@ func (a *app) importSubscription(rawURL string) (int, error) {
return a.mgr.ImportSubscription(rawURL)
}
type pingBestResult struct {
Pings []netcheck.Result `json:"pings"`
BestName string `json:"best_name,omitempty"`
BestMs int64 `json:"best_ms,omitempty"`
Selected bool `json:"selected"`
Connected bool `json:"connected"`
}
func (a *app) pingServers() ([]netcheck.Result, error) {
res, err := a.runPings()
return res, err
}
// pingBest measures all nodes, activates the fastest OK one, and optionally connects.
func (a *app) pingBest(autoConnect bool) (pingBestResult, error) {
pings, err := a.runPings()
out := pingBestResult{Pings: pings}
if err != nil {
return out, err
}
best, ok := netcheck.BestOK(pings)
if !ok {
return out, fmt.Errorf("нет доступных серверов")
}
out.BestName = best.Name
out.BestMs = best.Ms
a.mu.Lock()
defer a.mu.Unlock()
st := a.mgr.Status()
alreadyBest := st.Connected && st.Profile == best.Name
if st.Connected && !alreadyBest {
if err := a.mgr.Disconnect(); err != nil {
return out, err
}
}
if !alreadyBest {
if err := a.mgr.SetActiveProfile(best.Name); err != nil {
return out, err
}
}
out.Selected = true
if !autoConnect {
return out, nil
}
if alreadyBest {
out.Connected = true
return out, nil
}
if p, err := a.mgr.Config().ActiveProfile(); err != nil {
return out, err
} else if strings.TrimSpace(p.Proxy) == "" {
return out, fmt.Errorf("пустая ссылка у лучшего сервера")
}
if _, err := a.mgr.EnsureCore(""); err != nil {
return out, err
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := a.mgr.Connect(ctx, ""); err != nil {
return out, err
}
out.Connected = true
return out, nil
}
func (a *app) runPings() ([]netcheck.Result, error) {
a.mu.Lock()
list := a.mgr.Profiles()
a.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
out := make([]netcheck.Result, 0, len(list))
targets := make([]netcheck.Target, 0, len(list))
for _, p := range list {
proxy := p.Proxy
if proxy == "" {
out = append(out, netcheck.Result{Name: p.Name, Error: "нет ссылки сервера"})
continue
}
out = append(out, netcheck.PingProfile(ctx, p.Name, config.Protocol(p.Protocol), proxy))
targets = append(targets, netcheck.Target{
Name: p.Name,
Protocol: config.Protocol(p.Protocol),
Proxy: p.Proxy,
})
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
out := netcheck.PingAll(ctx, targets)
a.mu.Lock()
a.pings = out
a.mu.Unlock()