package ui import ( "image/color" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" ) type page uint8 const ( pageInstances page = iota pageSettings pageCreate ) // InstanceRow is the read-only view model used by the first Gio shell. // Runtime data will replace these fixtures when the application service is wired. type InstanceRow struct { Name string Browser string Profile string Status string } type SettingsState struct { ChromePath string EdgePath string DefaultDir string LogDir string CloseOnExit bool } // Shell owns every interactive Gio widget. Keeping this state outside Layout // prevents focus, editor content, and list scroll position from resetting. type Shell struct { theme *material.Theme page page instancesClick widget.Clickable settingsClick widget.Clickable newClick widget.Clickable startClick widget.Clickable createClick widget.Clickable backClick widget.Clickable chromeKind widget.Clickable edgeKind widget.Clickable saveClick widget.Clickable cancelClick widget.Clickable chromePath widget.Editor edgePath widget.Editor dataDir widget.Editor logDir widget.Editor closeOnExit widget.Bool instanceName widget.Editor instanceDir widget.Editor instanceURL widget.Editor newBrowser string list widget.List rows []InstanceRow onSave func(SettingsState) } func NewShell(theme *material.Theme) *Shell { s := &Shell{theme: theme, rows: []InstanceRow{ {Name: "运营主账号", Browser: "Chrome", Profile: "Default", Status: "运行中"}, {Name: "广告投放", Browser: "Edge", Profile: "Profile 2", Status: "启动中"}, {Name: "素材采集", Browser: "Chrome", Profile: "Default", Status: "外部占用"}, {Name: "备用环境", Browser: "Edge", Profile: "Profile 1", Status: "已退出"}, }} 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`) s.logDir.SetText(`C:\Users\Public\chub\logs`) s.closeOnExit.Value = true s.newBrowser = "Chrome" return s } func (s *Shell) SetInstances(rows []InstanceRow) { if len(rows) == 0 { return } s.rows = append([]InstanceRow(nil), rows...) } func (s *Shell) SetSettings(value SettingsState) { s.chromePath.SetText(value.ChromePath) s.edgePath.SetText(value.EdgePath) s.dataDir.SetText(value.DefaultDir) s.logDir.SetText(value.LogDir) s.closeOnExit.Value = value.CloseOnExit } func (s *Shell) OnSave(fn func(SettingsState)) { s.onSave = fn } func (s *Shell) Layout(gtx layout.Context) layout.Dimensions { for s.instancesClick.Clicked(gtx) { s.page = pageInstances } for s.settingsClick.Clicked(gtx) { s.page = pageSettings } for s.newClick.Clicked(gtx) { s.page = pageCreate } for s.createClick.Clicked(gtx) { if name := s.instanceName.Text(); name != "" { s.rows = append(s.rows, InstanceRow{Name: name, Browser: s.newBrowser, Profile: "Default", Status: "已退出"}) s.page = pageInstances } } for s.backClick.Clicked(gtx) { s.page = pageInstances } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(s.header), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return layout.Inset{Top: unit.Dp(20), Bottom: unit.Dp(20), Left: unit.Dp(28), Right: unit.Dp(28)}.Layout(gtx, s.content) }), ) } func (s *Shell) header(gtx layout.Context) layout.Dimensions { return layout.Inset{Top: unit.Dp(18), Bottom: unit.Dp(10), Left: unit.Dp(28), Right: unit.Dp(28)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, material.H5(s.theme, "Chub 浏览器管理").Layout), layout.Rigid(material.Button(s.theme, &s.instancesClick, "实例").Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(material.Button(s.theme, &s.settingsClick, "设置").Layout), ) }) } func (s *Shell) content(gtx layout.Context) layout.Dimensions { if s.page == pageSettings { return s.settings(gtx) } if s.page == pageCreate { return s.createInstance(gtx) } return s.instances(gtx) } func (s *Shell) instances(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(material.H4(s.theme, "实例").Layout), layout.Rigid(material.Body1(s.theme, "统一查看、启动和管理 Chrome / Edge 浏览器进程").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout), layout.Rigid(material.Button(s.theme, &s.newClick, "新建实例").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return material.List(s.theme, &s.list).Layout(gtx, len(s.rows), func(gtx layout.Context, i int) layout.Dimensions { row := s.rows[i] return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, 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.Subtitle1(s.theme, row.Name).Layout), layout.Rigid(material.Caption(s.theme, row.Browser+" · "+row.Profile).Layout), ) }), layout.Rigid(statusLabel(s.theme, row.Status)), layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout), layout.Rigid(material.Button(s.theme, &s.startClick, "启动").Layout), ) }) }) }), ) } func statusLabel(theme *material.Theme, status string) layout.Widget { style := material.Label(theme, unit.Sp(13), status) style.Color = map[string]color.NRGBA{ "运行中": {R: 24, G: 125, B: 78, A: 255}, "启动中": {R: 175, G: 105, B: 0, A: 255}, "外部占用": {R: 190, G: 75, B: 35, A: 255}, "已退出": {R: 100, G: 105, B: 115, A: 255}, }[status] return style.Layout } func (s *Shell) settings(gtx layout.Context) layout.Dimensions { for s.saveClick.Clicked(gtx) { if s.onSave != nil { s.onSave(SettingsState{ChromePath: s.chromePath.Text(), EdgePath: s.edgePath.Text(), DefaultDir: s.dataDir.Text(), LogDir: s.logDir.Text(), CloseOnExit: s.closeOnExit.Value}) } } for s.cancelClick.Clicked(gtx) { s.resetSettings() } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(material.H4(s.theme, "设置").Layout), layout.Rigid(material.Body1(s.theme, "配置浏览器可执行文件、默认目录和本地运行行为").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout), layout.Rigid(material.Editor(s.theme, &s.chromePath, "Chrome 可执行文件").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Editor(s.theme, &s.edgePath, "Edge 可执行文件").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Editor(s.theme, &s.dataDir, "默认 User Data Dir").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Editor(s.theme, &s.logDir, "日志目录").Layout), 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(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(material.Button(s.theme, &s.saveClick, "保存设置").Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(material.Button(s.theme, &s.cancelClick, "取消").Layout), ) }), ) } func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions { for s.chromeKind.Clicked(gtx) { s.newBrowser = "Chrome" } for s.edgeKind.Clicked(gtx) { s.newBrowser = "Edge" } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(material.H4(s.theme, "新建实例").Layout), layout.Rigid(material.Body1(s.theme, "为一个独立的 Chrome 或 Edge User Data Dir 创建托管实例").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout), layout.Rigid(material.Editor(s.theme, &s.instanceName, "实例名称").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Editor(s.theme, &s.instanceDir, "User Data Dir(绝对路径)").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), layout.Rigid(material.Editor(s.theme, &s.instanceURL, "启动 URL(可选)").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(material.Button(s.theme, &s.chromeKind, "Chrome").Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(material.Button(s.theme, &s.edgeKind, "Edge").Layout), ) }), layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(material.Button(s.theme, &s.createClick, "创建实例").Layout), layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), layout.Rigid(material.Button(s.theme, &s.backClick, "取消").Layout), ) }), ) } func (s *Shell) resetSettings() { 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`) s.logDir.SetText(`C:\Users\Public\chub\logs`) }