package gui import ( "fmt" "sort" "strings" "time" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget/material" "vpn-client/internal/config" "vpn-client/internal/vless" "vpn-client/internal/wireguard" ) func (u *ui) addWireGuardManual() { name := strings.TrimSpace(u.wgNameEditor.Text()) configText := strings.TrimSpace(u.wgConfigEditor.Text()) if name == "" || configText == "" { u.message = "WireGuard name and config text are required." u.messageColor = dangerColor return } if err := wireguard.AddConfig(name, configText); err != nil { u.message = err.Error() u.messageColor = dangerColor return } u.wgNameEditor.SetText("") u.wgConfigEditor.SetText("") u.message = "WireGuard config added." u.messageColor = successColor u.refresh() } func (u *ui) addWireGuardFromFile() { name := strings.TrimSpace(u.wgFileNameEditor.Text()) filePath := strings.TrimSpace(u.wgFilePathEditor.Text()) if name == "" || filePath == "" { u.message = "WireGuard name and file path are required." u.messageColor = dangerColor return } if err := wireguard.AddConfigFromFile(name, filePath); err != nil { u.message = err.Error() u.messageColor = dangerColor return } u.wgFileNameEditor.SetText("") u.wgFilePathEditor.SetText("") u.message = "WireGuard config imported from file." u.messageColor = successColor u.refresh() } func (u *ui) deleteWireGuardConfig(name string) { if err := wireguard.DeleteConfig(name); err != nil { u.message = err.Error() u.messageColor = dangerColor return } u.message = "WireGuard config removed." u.messageColor = successColor u.refresh() } func (u *ui) subscriptionConfigs(name string) []config.VLESSConfig { items := make([]config.VLESSConfig, 0) for _, cfg := range u.configs.VLESS { if cfg.Subscription == name { items = append(items, cfg) } } return items } func (u *ui) subscriptionSummary(name string) string { items := u.subscriptionConfigs(name) if len(items) == 0 { return "No imported configs yet." } preview := strings.TrimPrefix(items[0].Name, fmt.Sprintf("[%s] ", name)) if len(items) == 1 { return fmt.Sprintf("1 imported config: %s", trimForPreview(preview, 42)) } return fmt.Sprintf("%d imported configs. First: %s", len(items), trimForPreview(preview, 36)) } func (u *ui) testSubscriptionConfigs(name string) error { items := u.subscriptionConfigs(name) if len(items) == 0 { return fmt.Errorf("no imported configs found for %s", name) } type result struct { name string ping float64 ok bool } results := make([]result, 0, len(items)) for _, cfg := range items { ok, ping, _ := vless.PingServer(cfg.URL, 3*time.Second) results = append(results, result{ name: strings.TrimPrefix(cfg.Name, fmt.Sprintf("[%s] ", name)), ping: ping, ok: ok, }) } successful := results[:0] for _, r := range results { if r.ok { successful = append(successful, r) } } if len(successful) == 0 { return fmt.Errorf("all %d configs from %s are unreachable", len(results), name) } sort.Slice(successful, func(i, j int) bool { return successful[i].ping < successful[j].ping }) best := successful[0] u.message = fmt.Sprintf("%s: %d/%d reachable, best %s at %.0f ms.", name, len(successful), len(results), trimForPreview(best.name, 34), best.ping) u.messageColor = successColor return nil } func (u *ui) layoutWireGuard(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Spacing: layout.SpaceBetween}.Layout( gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return u.card(gtx, panelColor, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(18)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.H5(u.theme, "Add manually") lbl.Color = textColor return lbl.Layout(gtx) }), layout.Rigid(spacerH(10)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutEditor(gtx, &u.wgNameEditor, "Config name") }), layout.Rigid(spacerH(10)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutEditor(gtx, &u.wgConfigEditor, "WireGuard config text") }), layout.Rigid(spacerH(12)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutButton(gtx, &u.addWGManualBtn, "Save manual config", accentColor) }), ) }) }) }), layout.Rigid(spacerW(12)), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return u.card(gtx, panelColor, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(18)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.H5(u.theme, "Import from file") lbl.Color = textColor return lbl.Layout(gtx) }), layout.Rigid(spacerH(10)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutEditor(gtx, &u.wgFileNameEditor, "Config name") }), layout.Rigid(spacerH(10)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutEditor(gtx, &u.wgFilePathEditor, "Path to .conf file") }), layout.Rigid(spacerH(12)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutButton(gtx, &u.addWGFileBtn, "Import file", accentSoftColor) }), ) }) }) }), ) }), layout.Rigid(spacerH(16)), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return u.card(gtx, panelColor, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(18)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { if len(u.configs.WireGuard) == 0 { lbl := material.Body1(u.theme, "No WireGuard configs yet.") lbl.Color = mutedColor return lbl.Layout(gtx) } return u.wgList.Layout(gtx, len(u.configs.WireGuard), func(gtx layout.Context, i int) layout.Dimensions { cfg := u.configs.WireGuard[i] return layout.Inset{Bottom: unit.Dp(12)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return u.card(gtx, panelAltColor, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.H6(u.theme, cfg.Name) lbl.Color = textColor return lbl.Layout(gtx) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { meta := material.Body2(u.theme, trimForPreview(cfg.Config, 110)) meta.Color = mutedColor return meta.Layout(gtx) }), layout.Rigid(spacerH(12)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{}.Layout( gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutButton(gtx, &u.wgConnectBtns[i], "Connect", accentColor) }), layout.Rigid(spacerW(8)), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return u.layoutButton(gtx, &u.wgDeleteBtns[i], "Delete", dangerColor) }), ) }), ) }) }) }) }) }) }) }), ) }