Expand Gio GUI feature coverage
This commit is contained in:
235
internal/gui/advanced.go
Normal file
235
internal/gui/advanced.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"vpn-client/internal/subscription"
|
"vpn-client/internal/subscription"
|
||||||
"vpn-client/internal/vless"
|
"vpn-client/internal/vless"
|
||||||
"vpn-client/internal/vpn"
|
"vpn-client/internal/vpn"
|
||||||
|
"vpn-client/internal/wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tabID string
|
type tabID string
|
||||||
@@ -31,6 +32,7 @@ type tabID string
|
|||||||
const (
|
const (
|
||||||
tabDashboard tabID = "dashboard"
|
tabDashboard tabID = "dashboard"
|
||||||
tabVLESS tabID = "vless"
|
tabVLESS tabID = "vless"
|
||||||
|
tabWireGuard tabID = "wireguard"
|
||||||
tabSubscriptions tabID = "subscriptions"
|
tabSubscriptions tabID = "subscriptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,22 +68,29 @@ type ui struct {
|
|||||||
|
|
||||||
tabButtons map[tabID]*widget.Clickable
|
tabButtons map[tabID]*widget.Clickable
|
||||||
|
|
||||||
refreshBtn widget.Clickable
|
refreshBtn widget.Clickable
|
||||||
disconnectBtn widget.Clickable
|
disconnectBtn widget.Clickable
|
||||||
openLogsBtn widget.Clickable
|
openLogsBtn widget.Clickable
|
||||||
addConfigBtn widget.Clickable
|
addConfigBtn widget.Clickable
|
||||||
addSubBtn widget.Clickable
|
addSubBtn widget.Clickable
|
||||||
testURLBtn widget.Clickable
|
testURLBtn widget.Clickable
|
||||||
|
addWGManualBtn widget.Clickable
|
||||||
|
addWGFileBtn widget.Clickable
|
||||||
|
|
||||||
vlessConnectBtns []widget.Clickable
|
vlessConnectBtns []widget.Clickable
|
||||||
vlessDeleteBtns []widget.Clickable
|
vlessDeleteBtns []widget.Clickable
|
||||||
vlessPingBtns []widget.Clickable
|
vlessPingBtns []widget.Clickable
|
||||||
|
|
||||||
|
wgConnectBtns []widget.Clickable
|
||||||
|
wgDeleteBtns []widget.Clickable
|
||||||
|
|
||||||
subUpdateBtns []widget.Clickable
|
subUpdateBtns []widget.Clickable
|
||||||
subDeleteBtns []widget.Clickable
|
subDeleteBtns []widget.Clickable
|
||||||
|
subTestBtns []widget.Clickable
|
||||||
|
|
||||||
contentList layout.List
|
contentList layout.List
|
||||||
vlessList layout.List
|
vlessList layout.List
|
||||||
|
wgList layout.List
|
||||||
subList layout.List
|
subList layout.List
|
||||||
|
|
||||||
configNameEditor widget.Editor
|
configNameEditor widget.Editor
|
||||||
@@ -89,6 +98,10 @@ type ui struct {
|
|||||||
subNameEditor widget.Editor
|
subNameEditor widget.Editor
|
||||||
subURLEditor widget.Editor
|
subURLEditor widget.Editor
|
||||||
testURLEditor widget.Editor
|
testURLEditor widget.Editor
|
||||||
|
wgNameEditor widget.Editor
|
||||||
|
wgConfigEditor widget.Editor
|
||||||
|
wgFileNameEditor widget.Editor
|
||||||
|
wgFilePathEditor widget.Editor
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(window *app.Window) error {
|
func Run(window *app.Window) error {
|
||||||
@@ -126,10 +139,12 @@ func newUI(window *app.Window) *ui {
|
|||||||
tabButtons: map[tabID]*widget.Clickable{
|
tabButtons: map[tabID]*widget.Clickable{
|
||||||
tabDashboard: new(widget.Clickable),
|
tabDashboard: new(widget.Clickable),
|
||||||
tabVLESS: new(widget.Clickable),
|
tabVLESS: new(widget.Clickable),
|
||||||
|
tabWireGuard: new(widget.Clickable),
|
||||||
tabSubscriptions: new(widget.Clickable),
|
tabSubscriptions: new(widget.Clickable),
|
||||||
},
|
},
|
||||||
contentList: layout.List{Axis: layout.Vertical},
|
contentList: layout.List{Axis: layout.Vertical},
|
||||||
vlessList: layout.List{Axis: layout.Vertical},
|
vlessList: layout.List{Axis: layout.Vertical},
|
||||||
|
wgList: layout.List{Axis: layout.Vertical},
|
||||||
subList: layout.List{Axis: layout.Vertical},
|
subList: layout.List{Axis: layout.Vertical},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +153,19 @@ func newUI(window *app.Window) *ui {
|
|||||||
model.subNameEditor.SingleLine = true
|
model.subNameEditor.SingleLine = true
|
||||||
model.subURLEditor.SingleLine = true
|
model.subURLEditor.SingleLine = true
|
||||||
model.testURLEditor.SingleLine = true
|
model.testURLEditor.SingleLine = true
|
||||||
|
model.wgNameEditor.SingleLine = true
|
||||||
|
model.wgFileNameEditor.SingleLine = true
|
||||||
|
model.wgFilePathEditor.SingleLine = true
|
||||||
|
|
||||||
model.configNameEditor.Submit = true
|
model.configNameEditor.Submit = true
|
||||||
model.configURLEditor.Submit = true
|
model.configURLEditor.Submit = true
|
||||||
model.subNameEditor.Submit = true
|
model.subNameEditor.Submit = true
|
||||||
model.subURLEditor.Submit = true
|
model.subURLEditor.Submit = true
|
||||||
model.testURLEditor.Submit = true
|
model.testURLEditor.Submit = true
|
||||||
|
model.wgNameEditor.Submit = true
|
||||||
|
model.wgConfigEditor.Submit = true
|
||||||
|
model.wgFileNameEditor.Submit = true
|
||||||
|
model.wgFilePathEditor.Submit = true
|
||||||
|
|
||||||
model.configURLEditor.SetText("vless://")
|
model.configURLEditor.SetText("vless://")
|
||||||
model.testURLEditor.SetText("vless://")
|
model.testURLEditor.SetText("vless://")
|
||||||
@@ -190,6 +212,12 @@ func (u *ui) handleClicks(gtx layout.Context) {
|
|||||||
for u.testURLBtn.Clicked(gtx) {
|
for u.testURLBtn.Clicked(gtx) {
|
||||||
u.testURL()
|
u.testURL()
|
||||||
}
|
}
|
||||||
|
for u.addWGManualBtn.Clicked(gtx) {
|
||||||
|
u.addWireGuardManual()
|
||||||
|
}
|
||||||
|
for u.addWGFileBtn.Clicked(gtx) {
|
||||||
|
u.addWireGuardFromFile()
|
||||||
|
}
|
||||||
|
|
||||||
for i := range u.vlessConnectBtns {
|
for i := range u.vlessConnectBtns {
|
||||||
for u.vlessConnectBtns[i].Clicked(gtx) {
|
for u.vlessConnectBtns[i].Clicked(gtx) {
|
||||||
@@ -223,6 +251,23 @@ func (u *ui) handleClicks(gtx layout.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range u.wgConnectBtns {
|
||||||
|
for u.wgConnectBtns[i].Clicked(gtx) {
|
||||||
|
if i < len(u.configs.WireGuard) {
|
||||||
|
name := u.configs.WireGuard[i].Name
|
||||||
|
u.runAction("Connecting WireGuard "+name+"...", func() error {
|
||||||
|
return wireguard.Connect(name, config.LogsDir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for u.wgDeleteBtns[i].Clicked(gtx) {
|
||||||
|
if i < len(u.configs.WireGuard) {
|
||||||
|
name := u.configs.WireGuard[i].Name
|
||||||
|
u.deleteWireGuardConfig(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i := range u.subUpdateBtns {
|
for i := range u.subUpdateBtns {
|
||||||
for u.subUpdateBtns[i].Clicked(gtx) {
|
for u.subUpdateBtns[i].Clicked(gtx) {
|
||||||
if i < len(u.subs.Subscriptions) {
|
if i < len(u.subs.Subscriptions) {
|
||||||
@@ -238,6 +283,14 @@ func (u *ui) handleClicks(gtx layout.Context) {
|
|||||||
u.deleteSubscription(name)
|
u.deleteSubscription(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for u.subTestBtns[i].Clicked(gtx) {
|
||||||
|
if i < len(u.subs.Subscriptions) {
|
||||||
|
name := u.subs.Subscriptions[i].Name
|
||||||
|
u.runAction("Testing configs from "+name+"...", func() error {
|
||||||
|
return u.testSubscriptionConfigs(name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,8 +347,11 @@ func (u *ui) ensureButtons() {
|
|||||||
u.vlessConnectBtns = ensureClickables(u.vlessConnectBtns, len(u.configs.VLESS))
|
u.vlessConnectBtns = ensureClickables(u.vlessConnectBtns, len(u.configs.VLESS))
|
||||||
u.vlessDeleteBtns = ensureClickables(u.vlessDeleteBtns, len(u.configs.VLESS))
|
u.vlessDeleteBtns = ensureClickables(u.vlessDeleteBtns, len(u.configs.VLESS))
|
||||||
u.vlessPingBtns = ensureClickables(u.vlessPingBtns, len(u.configs.VLESS))
|
u.vlessPingBtns = ensureClickables(u.vlessPingBtns, len(u.configs.VLESS))
|
||||||
|
u.wgConnectBtns = ensureClickables(u.wgConnectBtns, len(u.configs.WireGuard))
|
||||||
|
u.wgDeleteBtns = ensureClickables(u.wgDeleteBtns, len(u.configs.WireGuard))
|
||||||
u.subUpdateBtns = ensureClickables(u.subUpdateBtns, len(u.subs.Subscriptions))
|
u.subUpdateBtns = ensureClickables(u.subUpdateBtns, len(u.subs.Subscriptions))
|
||||||
u.subDeleteBtns = ensureClickables(u.subDeleteBtns, len(u.subs.Subscriptions))
|
u.subDeleteBtns = ensureClickables(u.subDeleteBtns, len(u.subs.Subscriptions))
|
||||||
|
u.subTestBtns = ensureClickables(u.subTestBtns, len(u.subs.Subscriptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureClickables(list []widget.Clickable, count int) []widget.Clickable {
|
func ensureClickables(list []widget.Clickable, count int) []widget.Clickable {
|
||||||
@@ -629,6 +685,8 @@ func (u *ui) layoutBody(gtx layout.Context) layout.Dimensions {
|
|||||||
switch u.tab {
|
switch u.tab {
|
||||||
case tabVLESS:
|
case tabVLESS:
|
||||||
return u.layoutVLESS(gtx)
|
return u.layoutVLESS(gtx)
|
||||||
|
case tabWireGuard:
|
||||||
|
return u.layoutWireGuard(gtx)
|
||||||
case tabSubscriptions:
|
case tabSubscriptions:
|
||||||
return u.layoutSubscriptions(gtx)
|
return u.layoutSubscriptions(gtx)
|
||||||
default:
|
default:
|
||||||
@@ -649,6 +707,10 @@ func (u *ui) layoutTabs(gtx layout.Context) layout.Dimensions {
|
|||||||
return u.layoutTab(gtx, tabVLESS, "VLESS")
|
return u.layoutTab(gtx, tabVLESS, "VLESS")
|
||||||
}),
|
}),
|
||||||
layout.Rigid(spacerW(10)),
|
layout.Rigid(spacerW(10)),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.layoutTab(gtx, tabWireGuard, "WireGuard")
|
||||||
|
}),
|
||||||
|
layout.Rigid(spacerW(10)),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return u.layoutTab(gtx, tabSubscriptions, "Subscriptions")
|
return u.layoutTab(gtx, tabSubscriptions, "Subscriptions")
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user