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 ) // 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 saveClick widget.Clickable cancelClick widget.Clickable chromePath widget.Editor edgePath widget.Editor dataDir widget.Editor logDir widget.Editor closeOnExit widget.Bool 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 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 } 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) } 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) 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`) }