feat: add reusable proxy configuration
This commit is contained in:
+422
-6
@@ -58,6 +58,14 @@ type PathSearcher func(context.Context, PathField, string) (string, error)
|
||||
|
||||
type DirectoryChooser func(context.Context) (string, error)
|
||||
|
||||
// ProxyOption is a non-secret proxy item exposed to the UI. Server has already
|
||||
// been validated as an unauthenticated scheme://host:port endpoint.
|
||||
type ProxyOption struct {
|
||||
ID string
|
||||
Name string
|
||||
Server string
|
||||
}
|
||||
|
||||
type InstanceStartOutcome struct {
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
@@ -142,6 +150,18 @@ const (
|
||||
directoryPickEdit
|
||||
)
|
||||
|
||||
type proxyPickerTarget uint8
|
||||
|
||||
const (
|
||||
proxyPickerCreate proxyPickerTarget = iota
|
||||
proxyPickerEdit
|
||||
)
|
||||
|
||||
type proxyPickerState struct {
|
||||
open bool
|
||||
target proxyPickerTarget
|
||||
}
|
||||
|
||||
// InstanceRow is the Gio view model for one configured browser instance.
|
||||
// Runtime fields are refreshed independently and are never saved by the edit form.
|
||||
type InstanceRow struct {
|
||||
@@ -150,6 +170,7 @@ type InstanceRow struct {
|
||||
Browser string
|
||||
UserDataDir string
|
||||
TargetURL string
|
||||
ProxyID string
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
OccupancySource string
|
||||
@@ -199,6 +220,8 @@ type Shell struct {
|
||||
instanceName widget.Editor
|
||||
instanceDir widget.Editor
|
||||
instanceURL widget.Editor
|
||||
createProxyPick widget.Clickable
|
||||
editProxyPick widget.Clickable
|
||||
browserKind widget.Enum
|
||||
editName widget.Editor
|
||||
editDir widget.Editor
|
||||
@@ -216,6 +239,22 @@ type Shell struct {
|
||||
pathFeedback string
|
||||
formFeedback string
|
||||
editFeedback string
|
||||
proxyName widget.Editor
|
||||
proxyServer widget.Editor
|
||||
proxySave widget.Clickable
|
||||
proxyCancel widget.Clickable
|
||||
proxyFeedback string
|
||||
proxies []ProxyOption
|
||||
createProxyID string
|
||||
editProxyID string
|
||||
editingProxyID string
|
||||
nextProxy uint64
|
||||
proxyEditClicks map[string]*widget.Clickable
|
||||
proxyDeleteClicks map[string]*widget.Clickable
|
||||
proxyPicker proxyPickerState
|
||||
proxyPickerBlocker widget.Clickable
|
||||
proxyPickerNone widget.Clickable
|
||||
proxyPickerChoices map[string]*widget.Clickable
|
||||
|
||||
list widget.List
|
||||
rows []InstanceRow
|
||||
@@ -241,6 +280,7 @@ type Shell struct {
|
||||
focusRestoreID string
|
||||
onSave func(SettingsState)
|
||||
onInstancesChanged func([]InstanceRow)
|
||||
onProxiesChanged func([]ProxyOption)
|
||||
instanceStarter InstanceStarter
|
||||
instanceRefresher InstanceRefresher
|
||||
pathSearcher PathSearcher
|
||||
@@ -263,7 +303,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
PathEdgeExecutable: {},
|
||||
PathDefaultUserData: {},
|
||||
PathLogDirectory: {},
|
||||
}, rowClicks: make(map[string]*widget.Clickable), startClicks: make(map[string]*widget.Clickable), editClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), startStates: make(map[string]*instanceStartState), nextInstance: 4, startResults: make(chan instanceStartResult, 8), refreshResults: make(chan instanceRefreshResult, 1), searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
|
||||
}, rowClicks: make(map[string]*widget.Clickable), startClicks: make(map[string]*widget.Clickable), editClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), proxyEditClicks: make(map[string]*widget.Clickable), proxyDeleteClicks: make(map[string]*widget.Clickable), proxyPickerChoices: make(map[string]*widget.Clickable), startStates: make(map[string]*instanceStartState), nextInstance: 4, startResults: make(chan instanceStartResult, 8), refreshResults: make(chan instanceRefreshResult, 1), searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
|
||||
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||
@@ -290,6 +330,26 @@ func (s *Shell) SetInstances(rows []InstanceRow) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) SetProxies(options []ProxyOption) {
|
||||
s.proxies = append([]ProxyOption(nil), options...)
|
||||
for _, option := range s.proxies {
|
||||
if option.ID == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(option.ID, "proxy-") {
|
||||
if value, err := strconv.ParseUint(strings.TrimPrefix(option.ID, "proxy-"), 10, 64); err == nil && value > s.nextProxy {
|
||||
s.nextProxy = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if !s.proxyExists(s.createProxyID) {
|
||||
s.createProxyID = ""
|
||||
}
|
||||
if !s.proxyExists(s.editProxyID) {
|
||||
s.editProxyID = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) SetSettings(value SettingsState) {
|
||||
s.chromePath.SetText(value.ChromePath)
|
||||
s.edgePath.SetText(value.EdgePath)
|
||||
@@ -307,6 +367,8 @@ func (s *Shell) OnSave(fn func(SettingsState)) { s.onSave = fn }
|
||||
|
||||
func (s *Shell) OnInstancesChanged(fn func([]InstanceRow)) { s.onInstancesChanged = fn }
|
||||
|
||||
func (s *Shell) OnProxiesChanged(fn func([]ProxyOption)) { s.onProxiesChanged = fn }
|
||||
|
||||
func (s *Shell) OnStartInstance(starter InstanceStarter, invalidate func()) {
|
||||
s.instanceStarter = starter
|
||||
s.invalidate = invalidate
|
||||
@@ -348,7 +410,7 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
)
|
||||
}
|
||||
if s.pendingDeleteID == "" && s.editingID == "" {
|
||||
if s.pendingDeleteID == "" && s.editingID == "" && !s.proxyPicker.open {
|
||||
return mainLayout(gtx)
|
||||
}
|
||||
if s.pendingDeleteID != "" {
|
||||
@@ -364,9 +426,22 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
||||
layout.Stacked(s.editDiscardConfirmation),
|
||||
)
|
||||
}
|
||||
if s.editingID != "" {
|
||||
if !s.proxyPicker.open {
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(mainLayout),
|
||||
layout.Stacked(s.editDialog),
|
||||
)
|
||||
}
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(mainLayout),
|
||||
layout.Stacked(s.editDialog),
|
||||
layout.Stacked(s.proxyPickerDialog),
|
||||
)
|
||||
}
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(mainLayout),
|
||||
layout.Stacked(s.editDialog),
|
||||
layout.Stacked(s.proxyPickerDialog),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -429,7 +504,9 @@ func (s *Shell) consumeKeyboard(gtx layout.Context) {
|
||||
}
|
||||
switch keyEvent.Name {
|
||||
case key.NameEscape:
|
||||
if s.pendingEditDiscard {
|
||||
if s.proxyPicker.open {
|
||||
s.proxyPicker.open = false
|
||||
} else if s.pendingEditDiscard {
|
||||
s.pendingEditDiscard = false
|
||||
} else if s.editingID != "" {
|
||||
s.cancelEdit()
|
||||
@@ -502,6 +579,7 @@ func (s *Shell) beginEdit(id string) {
|
||||
s.editDir.SetText(row.UserDataDir)
|
||||
s.editURL.SetText(row.TargetURL)
|
||||
s.editPort.SetText(remoteDebugPortText(row.RemoteDebugPort))
|
||||
s.editProxyID = row.ProxyID
|
||||
if strings.EqualFold(row.Browser, "Edge") {
|
||||
s.editBrowserKind.Value = "edge"
|
||||
} else {
|
||||
@@ -525,7 +603,7 @@ func (s *Shell) editIsDirty() bool {
|
||||
if s.editingID == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(s.editName.Text()) != s.editOriginal.Name || strings.TrimSpace(s.editURL.Text()) != s.editOriginal.TargetURL {
|
||||
if strings.TrimSpace(s.editName.Text()) != s.editOriginal.Name || strings.TrimSpace(s.editURL.Text()) != s.editOriginal.TargetURL || s.editProxyID != s.editOriginal.ProxyID {
|
||||
return true
|
||||
}
|
||||
if s.editLocked(s.editOriginal) {
|
||||
@@ -573,6 +651,11 @@ func (s *Shell) saveEdit() {
|
||||
}
|
||||
row.Name = name
|
||||
row.TargetURL = targetURL
|
||||
if !s.proxyExists(s.editProxyID) && s.editProxyID != "" {
|
||||
s.editFeedback = "所选代理已不可用,请重新选择。"
|
||||
return
|
||||
}
|
||||
row.ProxyID = s.editProxyID
|
||||
if !s.replaceInstanceRow(row) {
|
||||
s.closeEdit("要编辑的实例已不存在。")
|
||||
return
|
||||
@@ -599,6 +682,7 @@ func (s *Shell) closeEdit(feedback string) {
|
||||
s.pendingEditDiscard = false
|
||||
s.editFeedback = ""
|
||||
s.editFocusPending = false
|
||||
s.proxyPicker.open = false
|
||||
s.focusRestoreID = id
|
||||
s.instanceFeedback = feedback
|
||||
}
|
||||
@@ -858,6 +942,8 @@ func (s *Shell) editDialogCard(row InstanceRow) layout.Widget {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.editURL)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerEdit) }),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("当前调试端口", "运行时由全局起始端口分配,此处只读", &s.editPort)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(statusLabel(s.theme, row.Status)),
|
||||
@@ -1087,6 +1173,20 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
for s.logSearch.Clicked(gtx) {
|
||||
s.togglePathSearch(PathLogDirectory)
|
||||
}
|
||||
for s.proxySave.Clicked(gtx) {
|
||||
s.saveProxy()
|
||||
}
|
||||
for s.proxyCancel.Clicked(gtx) {
|
||||
s.cancelProxyEdit()
|
||||
}
|
||||
for _, option := range append([]ProxyOption(nil), s.proxies...) {
|
||||
for s.proxyEditClickFor(option.ID).Clicked(gtx) {
|
||||
s.beginProxyEdit(option.ID)
|
||||
}
|
||||
for s.proxyDeleteClickFor(option.ID).Clicked(gtx) {
|
||||
s.deleteProxy(option.ID)
|
||||
}
|
||||
}
|
||||
for s.saveClick.Clicked(gtx) {
|
||||
settings, err := s.settingsState()
|
||||
if err != nil {
|
||||
@@ -1119,6 +1219,8 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(material.CheckBox(s.theme, &s.closeOnExit, "退出时关闭托管浏览器").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
||||
layout.Rigid(s.proxySettings),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, &s.saveClick, "保存设置").Layout),
|
||||
@@ -1129,6 +1231,61 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) proxySettings(gtx layout.Context) layout.Dimensions {
|
||||
saveLabel := "添加代理"
|
||||
if s.editingProxyID != "" {
|
||||
saveLabel = "保存代理"
|
||||
}
|
||||
children := []layout.FlexChild{
|
||||
layout.Rigid(material.H6(s.theme, "代理").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "仅保存无认证 scheme://host:port 端点;实例可从选择器复用。").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.formField("代理名称", "用于实例表单显示", &s.proxyName)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, &s.proxySave, saveLabel).Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
cancelGtx := gtx
|
||||
if s.editingProxyID == "" {
|
||||
cancelGtx = gtx.Disabled()
|
||||
}
|
||||
return material.Button(s.theme, &s.proxyCancel, "取消编辑").Layout(cancelGtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
}
|
||||
if s.proxyFeedback != "" {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(material.Caption(s.theme, s.proxyFeedback).Layout))
|
||||
}
|
||||
if len(s.proxies) == 0 {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Caption(s.theme, "尚无代理;实例将以无代理方式启动。").Layout))
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
option := option
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, option.Name).Layout),
|
||||
layout.Rigid(material.Caption(s.theme, option.Server).Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(s.instanceIconButton(s.proxyEditClickFor(option.ID), instanceEditIcon, "编辑代理 "+option.Name, s.theme.Palette.ContrastBg)),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(s.instanceIconButton(s.proxyDeleteClickFor(option.ID), instanceDeleteIcon, "删除代理 "+option.Name, color.NRGBA{R: 188, G: 51, B: 51, A: 255})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
}
|
||||
|
||||
func (s *Shell) remoteDebugPortField(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
|
||||
@@ -1164,6 +1321,8 @@ func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.instanceURL)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerCreate) }),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(material.Body2(s.theme, "浏览器类型").Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
@@ -1196,6 +1355,134 @@ func (s *Shell) formField(label, help string, editor *widget.Editor) layout.Widg
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) proxyPickerField(gtx layout.Context, target proxyPickerTarget) layout.Dimensions {
|
||||
click := &s.createProxyPick
|
||||
selected := s.createProxyID
|
||||
if target == proxyPickerEdit {
|
||||
click = &s.editProxyPick
|
||||
selected = s.editProxyID
|
||||
}
|
||||
for click.Clicked(gtx) {
|
||||
s.proxyPicker = proxyPickerState{open: true, target: target}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "代理").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "选择设置中保存的无认证代理;留空表示不使用代理").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, click, s.proxyLabel(selected)).Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) proxyLabel(id string) string {
|
||||
if id == "" {
|
||||
return "无代理"
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
if option.ID == id {
|
||||
return option.Name
|
||||
}
|
||||
}
|
||||
return "代理不可用"
|
||||
}
|
||||
|
||||
func (s *Shell) proxyExists(id string) bool {
|
||||
if id == "" {
|
||||
return true
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
if option.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Shell) proxyPickerChoiceFor(id string) *widget.Clickable {
|
||||
if click := s.proxyPickerChoices[id]; click != nil {
|
||||
return click
|
||||
}
|
||||
click := new(widget.Clickable)
|
||||
s.proxyPickerChoices[id] = click
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) selectPickerProxy(id string) {
|
||||
if !s.proxyExists(id) {
|
||||
return
|
||||
}
|
||||
if s.proxyPicker.target == proxyPickerEdit {
|
||||
s.editProxyID = id
|
||||
} else {
|
||||
s.createProxyID = id
|
||||
}
|
||||
s.proxyPicker.open = false
|
||||
}
|
||||
|
||||
func (s *Shell) proxyPickerDialog(gtx layout.Context) layout.Dimensions {
|
||||
if !s.proxyPicker.open {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
return s.proxyPickerBlocker.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
||||
paint.Fill(gtx.Ops, color.NRGBA{R: 0, G: 0, B: 0, A: 92})
|
||||
for s.proxyPickerBlocker.Clicked(gtx) {
|
||||
s.proxyPicker.open = false
|
||||
}
|
||||
for s.proxyPickerNone.Clicked(gtx) {
|
||||
s.selectPickerProxy("")
|
||||
}
|
||||
for _, option := range append([]ProxyOption(nil), s.proxies...) {
|
||||
for s.proxyPickerChoiceFor(option.ID).Clicked(gtx) {
|
||||
s.selectPickerProxy(option.ID)
|
||||
}
|
||||
}
|
||||
return layout.Center.Layout(gtx, s.proxyPickerCard)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Shell) proxyPickerCard(gtx layout.Context) layout.Dimensions {
|
||||
if maxWidth := gtx.Dp(440); gtx.Constraints.Max.X > maxWidth {
|
||||
gtx.Constraints.Max.X = maxWidth
|
||||
}
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rect := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
defer clip.UniformRRect(rect, gtx.Dp(8)).Push(gtx.Ops).Pop()
|
||||
paint.Fill(gtx.Ops, s.theme.Palette.Bg)
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(20), Right: unit.Dp(24), Bottom: unit.Dp(20), Left: unit.Dp(24)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
children := []layout.FlexChild{
|
||||
layout.Rigid(material.H6(s.theme, "选择代理").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "仅显示和使用无认证代理端点。按 Escape 或点击背景取消。").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.proxyPickerNone, "无代理").Layout),
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
option := option
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return s.proxyPickerChoiceFor(option.ID).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(7), Bottom: unit.Dp(7), Left: unit.Dp(10), Right: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(s.theme, option.Name).Layout),
|
||||
layout.Rigid(material.Caption(s.theme, option.Server).Layout),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) instanceDirField(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "User Data Dir").Layout),
|
||||
@@ -1229,6 +1516,7 @@ func (s *Shell) createInstanceFromForm() {
|
||||
Browser: browserDisplay(s.browserKind.Value),
|
||||
UserDataDir: userDataDir,
|
||||
TargetURL: strings.TrimSpace(s.instanceURL.Text()),
|
||||
ProxyID: s.createProxyID,
|
||||
Status: "已退出",
|
||||
})
|
||||
s.page = pageInstances
|
||||
@@ -1375,6 +1663,134 @@ func (s *Shell) deleteClickFor(id string) *widget.Clickable {
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) proxyEditClickFor(id string) *widget.Clickable {
|
||||
if click := s.proxyEditClicks[id]; click != nil {
|
||||
return click
|
||||
}
|
||||
click := new(widget.Clickable)
|
||||
s.proxyEditClicks[id] = click
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) proxyDeleteClickFor(id string) *widget.Clickable {
|
||||
if click := s.proxyDeleteClicks[id]; click != nil {
|
||||
return click
|
||||
}
|
||||
click := new(widget.Clickable)
|
||||
s.proxyDeleteClicks[id] = click
|
||||
return click
|
||||
}
|
||||
|
||||
func (s *Shell) beginProxyEdit(id string) {
|
||||
for _, option := range s.proxies {
|
||||
if option.ID != id {
|
||||
continue
|
||||
}
|
||||
s.editingProxyID = id
|
||||
s.proxyName.SetText(option.Name)
|
||||
s.proxyServer.SetText(option.Server)
|
||||
s.proxyFeedback = ""
|
||||
return
|
||||
}
|
||||
s.proxyFeedback = "找不到要编辑的代理。"
|
||||
}
|
||||
|
||||
func (s *Shell) cancelProxyEdit() {
|
||||
s.editingProxyID = ""
|
||||
s.proxyName.SetText("")
|
||||
s.proxyServer.SetText("")
|
||||
s.proxyFeedback = "已取消代理编辑。"
|
||||
}
|
||||
|
||||
func (s *Shell) saveProxy() {
|
||||
name := strings.TrimSpace(s.proxyName.Text())
|
||||
if name == "" {
|
||||
s.proxyFeedback = "请输入代理名称。"
|
||||
return
|
||||
}
|
||||
server, err := domain.NormalizeProxyServer(s.proxyServer.Text())
|
||||
if err != nil || server == "" {
|
||||
s.proxyFeedback = "代理地址必须是无认证的 scheme://host:port。"
|
||||
return
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
if option.ID != s.editingProxyID && strings.EqualFold(option.Name, name) {
|
||||
s.proxyFeedback = "代理名称已存在,请使用不同名称。"
|
||||
return
|
||||
}
|
||||
}
|
||||
if s.editingProxyID == "" {
|
||||
s.nextProxy++
|
||||
s.proxies = append(s.proxies, ProxyOption{ID: fmt.Sprintf("proxy-%d", s.nextProxy), Name: name, Server: server})
|
||||
s.proxyFeedback = fmt.Sprintf("已添加代理“%s”。", name)
|
||||
} else {
|
||||
updated := false
|
||||
for index := range s.proxies {
|
||||
if s.proxies[index].ID != s.editingProxyID {
|
||||
continue
|
||||
}
|
||||
s.proxies[index].Name = name
|
||||
s.proxies[index].Server = server
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
if !updated {
|
||||
s.proxyFeedback = "找不到要编辑的代理。"
|
||||
return
|
||||
}
|
||||
s.proxyFeedback = fmt.Sprintf("已保存代理“%s”;后续启动将使用最新端点。", name)
|
||||
}
|
||||
s.editingProxyID = ""
|
||||
s.proxyName.SetText("")
|
||||
s.proxyServer.SetText("")
|
||||
s.notifyProxiesChanged()
|
||||
}
|
||||
|
||||
func (s *Shell) deleteProxy(id string) {
|
||||
if count := s.proxyUseCount(id); count > 0 {
|
||||
s.proxyFeedback = fmt.Sprintf("该代理仍被 %d 个实例使用,不能删除。", count)
|
||||
return
|
||||
}
|
||||
for index, option := range s.proxies {
|
||||
if option.ID != id {
|
||||
continue
|
||||
}
|
||||
s.proxies = append(s.proxies[:index], s.proxies[index+1:]...)
|
||||
delete(s.proxyEditClicks, id)
|
||||
delete(s.proxyDeleteClicks, id)
|
||||
delete(s.proxyPickerChoices, id)
|
||||
if s.createProxyID == id {
|
||||
s.createProxyID = ""
|
||||
}
|
||||
if s.editProxyID == id {
|
||||
s.editProxyID = ""
|
||||
}
|
||||
if s.editingProxyID == id {
|
||||
s.cancelProxyEdit()
|
||||
}
|
||||
s.proxyFeedback = fmt.Sprintf("已删除代理“%s”。", option.Name)
|
||||
s.notifyProxiesChanged()
|
||||
return
|
||||
}
|
||||
s.proxyFeedback = "找不到要删除的代理。"
|
||||
}
|
||||
|
||||
func (s *Shell) proxyUseCount(id string) int {
|
||||
count := 0
|
||||
for _, row := range s.rows {
|
||||
if row.ProxyID == id {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (s *Shell) notifyProxiesChanged() {
|
||||
if s.onProxiesChanged != nil {
|
||||
s.onProxiesChanged(append([]ProxyOption(nil), s.proxies...))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) requestStart(id string) {
|
||||
row, ok := s.instanceRow(id)
|
||||
if !ok {
|
||||
@@ -1535,7 +1951,7 @@ func (s *Shell) consumeRefreshResults() {
|
||||
}
|
||||
|
||||
func instanceRefreshFingerprint(row InstanceRow) string {
|
||||
return strings.Join([]string{row.ID, row.Name, row.Browser, row.UserDataDir, row.TargetURL}, "\x00")
|
||||
return strings.Join([]string{row.ID, row.Name, row.Browser, row.UserDataDir, row.TargetURL, row.ProxyID}, "\x00")
|
||||
}
|
||||
|
||||
func (s *Shell) requestDelete(id string) {
|
||||
|
||||
Reference in New Issue
Block a user