feat: add proxy labels and default profiles
This commit is contained in:
+181
-84
@@ -33,11 +33,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
instanceNameColumnWeight = 20
|
||||
instanceBrowserColumnWeight = 10
|
||||
instanceDirectoryColumnWeight = 29
|
||||
instancePortColumnWeight = 10
|
||||
instanceStatusColumnWeight = 13
|
||||
instanceNameColumnWeight = 17
|
||||
instanceBrowserColumnWeight = 9
|
||||
instanceDirectoryColumnWeight = 28
|
||||
instanceProxyColumnWeight = 9
|
||||
instancePortColumnWeight = 9
|
||||
instanceStatusColumnWeight = 10
|
||||
instanceActionColumnWeight = 18
|
||||
)
|
||||
|
||||
@@ -314,6 +315,7 @@ type Shell struct {
|
||||
formFeedback string
|
||||
editFeedback string
|
||||
proxyServer widget.Editor
|
||||
proxyName widget.Editor
|
||||
proxySave widget.Clickable
|
||||
proxyDelete widget.Clickable
|
||||
proxyFeedback string
|
||||
@@ -332,69 +334,71 @@ type Shell struct {
|
||||
pendingProxyDelete string
|
||||
proxyDeleteFocus bool
|
||||
|
||||
list widget.List
|
||||
settingsList widget.List
|
||||
tabsList widget.List
|
||||
rows []InstanceRow
|
||||
selectedInstanceID string
|
||||
rowClicks map[string]*widget.Clickable
|
||||
startClicks map[string]*widget.Clickable
|
||||
editClicks map[string]*widget.Clickable
|
||||
deleteClicks map[string]*widget.Clickable
|
||||
deleteConfirm widget.Clickable
|
||||
deleteCancel widget.Clickable
|
||||
deleteBlocker widget.Clickable
|
||||
startStates map[string]*instanceStartState
|
||||
startResults chan instanceStartResult
|
||||
stopStates map[string]*instanceStopState
|
||||
stopResults chan instanceStopResult
|
||||
refreshClickState instanceRefreshState
|
||||
refreshResults chan instanceRefreshResult
|
||||
tabsResults chan instanceTabsResult
|
||||
managedExitMu sync.Mutex
|
||||
managedExitResults []ManagedInstanceExit
|
||||
pendingManagedExit map[managedExitKey]ManagedInstanceExit
|
||||
unexpectedExits []unexpectedExitNotice
|
||||
unexpectedExitOpen bool
|
||||
unexpectedExitAck widget.Clickable
|
||||
unexpectedExitBlocker widget.Clickable
|
||||
unexpectedExitFocus bool
|
||||
unexpectedExitFocusID string
|
||||
nextInstance uint64
|
||||
instanceFeedback string
|
||||
pendingDeleteID string
|
||||
editingID string
|
||||
editOriginal InstanceRow
|
||||
pendingEditDiscard bool
|
||||
editFocusPending bool
|
||||
focusRestoreID string
|
||||
focusStartID string
|
||||
tabsOpen bool
|
||||
tabsLoading bool
|
||||
tabsRequest uint64
|
||||
tabsInstanceID string
|
||||
tabsFingerprint string
|
||||
tabsTargets []CDPTarget
|
||||
tabsFeedback string
|
||||
tabsCancel context.CancelFunc
|
||||
tabsClose widget.Clickable
|
||||
tabsBlocker widget.Clickable
|
||||
tabsFocusPending bool
|
||||
tabsReturnFocus bool
|
||||
onSave func(SettingsState)
|
||||
onInstancesChanged func([]InstanceRow)
|
||||
onProxiesChanged func([]ProxyOption)
|
||||
instanceStarter InstanceStarter
|
||||
instanceStopper InstanceStopper
|
||||
instanceRefresher InstanceRefresher
|
||||
instanceTabInspector InstanceTabInspector
|
||||
pathSearcher PathSearcher
|
||||
invalidate func()
|
||||
searches map[PathField]*pathSearchState
|
||||
searchResults chan pathSearchResult
|
||||
directoryChooser DirectoryChooser
|
||||
directoryPick directoryPickState
|
||||
directoryResults chan directoryPickResult
|
||||
list widget.List
|
||||
settingsList widget.List
|
||||
tabsList widget.List
|
||||
rows []InstanceRow
|
||||
selectedInstanceID string
|
||||
rowClicks map[string]*widget.Clickable
|
||||
startClicks map[string]*widget.Clickable
|
||||
editClicks map[string]*widget.Clickable
|
||||
deleteClicks map[string]*widget.Clickable
|
||||
deleteConfirm widget.Clickable
|
||||
deleteCancel widget.Clickable
|
||||
deleteBlocker widget.Clickable
|
||||
startStates map[string]*instanceStartState
|
||||
startResults chan instanceStartResult
|
||||
stopStates map[string]*instanceStopState
|
||||
stopResults chan instanceStopResult
|
||||
refreshClickState instanceRefreshState
|
||||
refreshResults chan instanceRefreshResult
|
||||
tabsResults chan instanceTabsResult
|
||||
managedExitMu sync.Mutex
|
||||
managedExitResults []ManagedInstanceExit
|
||||
pendingManagedExit map[managedExitKey]ManagedInstanceExit
|
||||
unexpectedExits []unexpectedExitNotice
|
||||
unexpectedExitOpen bool
|
||||
unexpectedExitAck widget.Clickable
|
||||
unexpectedExitBlocker widget.Clickable
|
||||
unexpectedExitFocus bool
|
||||
unexpectedExitFocusID string
|
||||
nextInstance uint64
|
||||
pendingCreateID string
|
||||
defaultInstanceDirRoot string
|
||||
instanceFeedback string
|
||||
pendingDeleteID string
|
||||
editingID string
|
||||
editOriginal InstanceRow
|
||||
pendingEditDiscard bool
|
||||
editFocusPending bool
|
||||
focusRestoreID string
|
||||
focusStartID string
|
||||
tabsOpen bool
|
||||
tabsLoading bool
|
||||
tabsRequest uint64
|
||||
tabsInstanceID string
|
||||
tabsFingerprint string
|
||||
tabsTargets []CDPTarget
|
||||
tabsFeedback string
|
||||
tabsCancel context.CancelFunc
|
||||
tabsClose widget.Clickable
|
||||
tabsBlocker widget.Clickable
|
||||
tabsFocusPending bool
|
||||
tabsReturnFocus bool
|
||||
onSave func(SettingsState)
|
||||
onInstancesChanged func([]InstanceRow)
|
||||
onProxiesChanged func([]ProxyOption)
|
||||
instanceStarter InstanceStarter
|
||||
instanceStopper InstanceStopper
|
||||
instanceRefresher InstanceRefresher
|
||||
instanceTabInspector InstanceTabInspector
|
||||
pathSearcher PathSearcher
|
||||
invalidate func()
|
||||
searches map[PathField]*pathSearchState
|
||||
searchResults chan pathSearchResult
|
||||
directoryChooser DirectoryChooser
|
||||
directoryPick directoryPickState
|
||||
directoryResults chan directoryPickResult
|
||||
}
|
||||
|
||||
func NewShell(theme *material.Theme) *Shell {
|
||||
@@ -459,10 +463,23 @@ func (s *Shell) SetProxies(options []ProxyOption) {
|
||||
}
|
||||
if !s.proxyExists(s.settingsProxyID) {
|
||||
s.settingsProxyID = ""
|
||||
s.proxyName.SetText("")
|
||||
s.proxyServer.SetText("")
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefaultInstanceUserDataRoot supplies the already resolved executable-side
|
||||
// profile root. It is deliberately a string-only UI boundary: no filesystem
|
||||
// lookup, creation, or permission check happens in the Gio shell.
|
||||
func (s *Shell) SetDefaultInstanceUserDataRoot(root string) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" || !filepath.IsAbs(root) {
|
||||
s.defaultInstanceDirRoot = ""
|
||||
return
|
||||
}
|
||||
s.defaultInstanceDirRoot = filepath.Clean(root)
|
||||
}
|
||||
|
||||
func (s *Shell) SetSettings(value SettingsState) {
|
||||
s.chromePath.SetText(value.ChromePath)
|
||||
s.edgePath.SetText(value.EdgePath)
|
||||
@@ -662,6 +679,17 @@ func (s *Shell) consumeControls(gtx layout.Context) {
|
||||
func (s *Shell) beginCreate() {
|
||||
s.page = pageCreate
|
||||
s.formFeedback = ""
|
||||
s.pendingCreateID = s.allocateInstanceID()
|
||||
s.instanceName.SetText("")
|
||||
s.instanceURL.SetText("")
|
||||
s.createProxyID = ""
|
||||
s.browserKind.Value = "chrome"
|
||||
if defaultDir := s.defaultCreateInstanceDir(s.pendingCreateID); defaultDir != "" {
|
||||
s.instanceDir.SetText(defaultDir)
|
||||
} else {
|
||||
s.instanceDir.SetText("")
|
||||
s.formFeedback = "无法确定程序目录中的默认 User Data Dir;请选择或输入一个绝对路径。"
|
||||
}
|
||||
s.instancePort.SetText(strconv.Itoa(s.recommendRemoteDebugPort("")))
|
||||
}
|
||||
|
||||
@@ -1016,6 +1044,7 @@ func (s *Shell) instanceListRow(row InstanceRow) layout.Widget {
|
||||
layout.Flexed(instanceNameColumnWeight, material.Body1(s.theme, row.Name).Layout),
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Body2(s.theme, row.Browser).Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, pathCell(s.theme, row.UserDataDir)),
|
||||
layout.Flexed(instanceProxyColumnWeight, proxyNameCell(s.theme, s.proxyDisplayName(row.ProxyID))),
|
||||
layout.Flexed(instancePortColumnWeight, remoteDebugPortCell(s.theme, row)),
|
||||
layout.Flexed(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
|
||||
)
|
||||
@@ -1049,6 +1078,7 @@ func (s *Shell) instanceHeader(gtx layout.Context) layout.Dimensions {
|
||||
layout.Flexed(instanceNameColumnWeight, material.Caption(s.theme, "实例名称").Layout),
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Caption(s.theme, "浏览器类型").Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, material.Caption(s.theme, "用户数据目录").Layout),
|
||||
layout.Flexed(instanceProxyColumnWeight, material.Caption(s.theme, "代理").Layout),
|
||||
layout.Flexed(instancePortColumnWeight, material.Caption(s.theme, "调试端口").Layout),
|
||||
layout.Flexed(instanceStatusColumnWeight, material.Caption(s.theme, "状态").Layout),
|
||||
layout.Flexed(instanceActionColumnWeight, func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -1072,6 +1102,12 @@ func remoteDebugPortCell(theme *material.Theme, row InstanceRow) layout.Widget {
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func proxyNameCell(theme *material.Theme, name string) layout.Widget {
|
||||
style := material.Body2(theme, name)
|
||||
style.MaxLines = 1
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func actualRemoteDebugPortText(port int) string {
|
||||
if port > 0 {
|
||||
return strconv.Itoa(port)
|
||||
@@ -1965,6 +2001,8 @@ func (s *Shell) proxySettings(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.settingsField(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerSettings) })),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.settingsField(s.formField("代理名称", "用于实例列表和选择器识别;名称必须唯一", &s.proxyName))),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.settingsField(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer))),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.settingsActionBar(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -2124,7 +2162,7 @@ func (s *Shell) proxyPickerField(gtx layout.Context, target proxyPickerTarget) l
|
||||
click = &s.settingsProxyPick
|
||||
selected = s.settingsProxyID
|
||||
label = "已保存代理"
|
||||
help = "选择项会回填代理地址;选择新建会清空输入框"
|
||||
help = "选择项会回填代理名称和地址;选择新建会清空输入框"
|
||||
}
|
||||
for click.Clicked(gtx) {
|
||||
s.proxyPicker = proxyPickerState{open: true, target: target}
|
||||
@@ -2138,17 +2176,26 @@ func (s *Shell) proxyPickerField(gtx layout.Context, target proxyPickerTarget) l
|
||||
}
|
||||
|
||||
func (s *Shell) proxyLabel(id string) string {
|
||||
if option, exists := s.proxyOption(id); exists {
|
||||
return proxyPickerLabel(option)
|
||||
}
|
||||
return s.proxyDisplayName(id)
|
||||
}
|
||||
|
||||
func (s *Shell) proxyDisplayName(id string) string {
|
||||
if id == "" {
|
||||
return "无代理"
|
||||
}
|
||||
for _, option := range s.proxies {
|
||||
if option.ID == id {
|
||||
return option.Server
|
||||
}
|
||||
if option, exists := s.proxyOption(id); exists {
|
||||
return option.Name
|
||||
}
|
||||
return "代理不可用"
|
||||
}
|
||||
|
||||
func proxyPickerLabel(option ProxyOption) string {
|
||||
return option.Name + " · " + option.Server
|
||||
}
|
||||
|
||||
func (s *Shell) proxyExists(id string) bool {
|
||||
if _, exists := s.proxyOption(id); exists {
|
||||
return true
|
||||
@@ -2186,10 +2233,12 @@ func (s *Shell) selectPickerProxy(id string) {
|
||||
} else if s.proxyPicker.target == proxyPickerSettings {
|
||||
s.settingsProxyID = id
|
||||
if id == "" {
|
||||
s.proxyName.SetText("")
|
||||
s.proxyServer.SetText("")
|
||||
} else {
|
||||
for _, option := range s.proxies {
|
||||
if option.ID == id {
|
||||
s.proxyName.SetText(option.Name)
|
||||
s.proxyServer.SetText(option.Server)
|
||||
break
|
||||
}
|
||||
@@ -2252,7 +2301,14 @@ func (s *Shell) proxyPickerCard(gtx layout.Context) layout.Dimensions {
|
||||
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 material.Body1(s.theme, option.Server).Layout(gtx)
|
||||
name := material.Body1(s.theme, option.Name)
|
||||
name.MaxLines = 1
|
||||
server := material.Caption(s.theme, option.Server)
|
||||
server.MaxLines = 1
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(name.Layout),
|
||||
layout.Rigid(server.Layout),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
@@ -2300,8 +2356,8 @@ func (s *Shell) createInstanceFromForm() {
|
||||
return
|
||||
}
|
||||
userDataDir := strings.TrimSpace(s.instanceDir.Text())
|
||||
if userDataDir == "" {
|
||||
s.formFeedback = "请选择或输入 User Data Dir 后再创建。"
|
||||
if userDataDir == "" || !filepath.IsAbs(userDataDir) {
|
||||
s.formFeedback = "请选择或输入绝对路径的 User Data Dir 后再创建。"
|
||||
return
|
||||
}
|
||||
preferredPort, err := normalizePreferredRemoteDebugPort(s.instancePort.Text())
|
||||
@@ -2313,12 +2369,15 @@ func (s *Shell) createInstanceFromForm() {
|
||||
s.formFeedback = fmt.Sprintf("端口 %d 已被其他 Chub 实例预留或使用。", preferredPort)
|
||||
return
|
||||
}
|
||||
s.nextInstance++
|
||||
instanceID := s.pendingCreateID
|
||||
if instanceID == "" || s.instanceIDExists(instanceID) {
|
||||
instanceID = s.allocateInstanceID()
|
||||
}
|
||||
s.rows = append(s.rows, InstanceRow{
|
||||
ID: fmt.Sprintf("instance-%d", s.nextInstance),
|
||||
ID: instanceID,
|
||||
Name: name,
|
||||
Browser: browserDisplay(s.browserKind.Value),
|
||||
UserDataDir: userDataDir,
|
||||
UserDataDir: filepath.Clean(userDataDir),
|
||||
TargetURL: strings.TrimSpace(s.instanceURL.Text()),
|
||||
ProxyID: s.createProxyID,
|
||||
PreferredRemoteDebugPort: preferredPort,
|
||||
@@ -2326,9 +2385,36 @@ func (s *Shell) createInstanceFromForm() {
|
||||
})
|
||||
s.page = pageInstances
|
||||
s.formFeedback = ""
|
||||
s.pendingCreateID = ""
|
||||
s.notifyInstancesChanged()
|
||||
}
|
||||
|
||||
func (s *Shell) defaultCreateInstanceDir(instanceID string) string {
|
||||
if s.defaultInstanceDirRoot == "" || instanceID == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(s.defaultInstanceDirRoot, instanceID)
|
||||
}
|
||||
|
||||
func (s *Shell) allocateInstanceID() string {
|
||||
for {
|
||||
s.nextInstance++
|
||||
candidate := fmt.Sprintf("instance-%d-%d", time.Now().UnixNano(), s.nextInstance)
|
||||
if !s.instanceIDExists(candidate) && candidate != s.pendingCreateID {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) instanceIDExists(id string) bool {
|
||||
for _, row := range s.rows {
|
||||
if row.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Shell) togglePathSearch(field PathField) {
|
||||
state := s.searches[field]
|
||||
if state == nil {
|
||||
@@ -2469,12 +2555,21 @@ func (s *Shell) deleteClickFor(id string) *widget.Clickable {
|
||||
}
|
||||
|
||||
func (s *Shell) saveProxy() {
|
||||
name, err := domain.NormalizeProxyName(s.proxyName.Text())
|
||||
if err != nil {
|
||||
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.settingsProxyID && strings.EqualFold(option.Name, name) {
|
||||
s.proxyFeedback = "该代理名称已存在,请使用其他名称。"
|
||||
return
|
||||
}
|
||||
if option.ID != s.settingsProxyID && option.Server == server {
|
||||
s.proxyFeedback = "该代理地址已存在,请从下拉框选择它。"
|
||||
return
|
||||
@@ -2483,15 +2578,15 @@ func (s *Shell) saveProxy() {
|
||||
if s.settingsProxyID == "" {
|
||||
s.nextProxy++
|
||||
s.settingsProxyID = fmt.Sprintf("proxy-%d", s.nextProxy)
|
||||
s.proxies = append(s.proxies, ProxyOption{ID: s.settingsProxyID, Name: server, Server: server})
|
||||
s.proxyFeedback = fmt.Sprintf("已添加代理“%s”;后续启动将使用该地址。", server)
|
||||
s.proxies = append(s.proxies, ProxyOption{ID: s.settingsProxyID, Name: name, Server: server})
|
||||
s.proxyFeedback = fmt.Sprintf("已添加代理“%s”;后续启动将使用该地址。", name)
|
||||
} else {
|
||||
updated := false
|
||||
for index := range s.proxies {
|
||||
if s.proxies[index].ID != s.settingsProxyID {
|
||||
continue
|
||||
}
|
||||
s.proxies[index].Name = server
|
||||
s.proxies[index].Name = name
|
||||
s.proxies[index].Server = server
|
||||
updated = true
|
||||
break
|
||||
@@ -2500,8 +2595,9 @@ func (s *Shell) saveProxy() {
|
||||
s.proxyFeedback = "所选代理已不可用,请重新选择或新建。"
|
||||
return
|
||||
}
|
||||
s.proxyFeedback = fmt.Sprintf("已保存代理“%s”;后续启动将使用最新端点。", server)
|
||||
s.proxyFeedback = fmt.Sprintf("已保存代理“%s”;后续启动将使用最新端点。", name)
|
||||
}
|
||||
s.proxyName.SetText(name)
|
||||
s.proxyServer.SetText(server)
|
||||
s.notifyProxiesChanged()
|
||||
}
|
||||
@@ -2552,9 +2648,10 @@ func (s *Shell) confirmProxyDelete() {
|
||||
}
|
||||
if s.settingsProxyID == id {
|
||||
s.settingsProxyID = ""
|
||||
s.proxyName.SetText("")
|
||||
s.proxyServer.SetText("")
|
||||
}
|
||||
s.proxyFeedback = fmt.Sprintf("已删除代理“%s”。", option.Server)
|
||||
s.proxyFeedback = fmt.Sprintf("已删除代理“%s”。", option.Name)
|
||||
s.notifyProxiesChanged()
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user