Files
cdp_hub/internal/ui/shell.go
T

313 lines
11 KiB
Go
Raw Normal View History

2026-07-22 15:13:55 +08:00
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
2026-07-22 15:24:25 +08:00
pageCreate
2026-07-22 15:13:55 +08:00
)
// 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
}
2026-07-22 15:17:08 +08:00
type SettingsState struct {
ChromePath string
EdgePath string
DefaultDir string
LogDir string
CloseOnExit bool
}
2026-07-22 15:13:55 +08:00
// 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
2026-07-22 15:24:25 +08:00
createClick widget.Clickable
backClick widget.Clickable
chromeKind widget.Clickable
edgeKind widget.Clickable
2026-07-22 15:30:03 +08:00
chromePick widget.Clickable
chromeSearch widget.Clickable
edgePick widget.Clickable
edgeSearch widget.Clickable
dataPick widget.Clickable
dataSearch widget.Clickable
logPick widget.Clickable
logSearch widget.Clickable
2026-07-22 15:13:55 +08:00
saveClick widget.Clickable
cancelClick widget.Clickable
2026-07-22 15:24:25 +08:00
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
2026-07-22 15:30:03 +08:00
pathFeedback string
2026-07-22 15:13:55 +08:00
2026-07-22 15:17:08 +08:00
list widget.List
rows []InstanceRow
onSave func(SettingsState)
2026-07-22 15:13:55 +08:00
}
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
2026-07-22 15:24:25 +08:00
s.newBrowser = "Chrome"
2026-07-22 15:13:55 +08:00
return s
}
2026-07-22 15:17:08 +08:00
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 }
2026-07-22 15:13:55 +08:00
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
}
2026-07-22 15:24:25 +08:00
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
}
2026-07-22 15:13:55 +08:00
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)
}
2026-07-22 15:24:25 +08:00
if s.page == pageCreate {
return s.createInstance(gtx)
}
2026-07-22 15:13:55 +08:00
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 {
2026-07-22 15:30:03 +08:00
for s.chromePick.Clicked(gtx) {
s.pathFeedback = "请选择 Chrome 可执行文件"
}
for s.chromeSearch.Clicked(gtx) {
s.pathFeedback = "正在搜索 Chrome 可执行文件…"
}
for s.edgePick.Clicked(gtx) {
s.pathFeedback = "请选择 Edge 可执行文件"
}
for s.edgeSearch.Clicked(gtx) {
s.pathFeedback = "正在搜索 Edge 可执行文件…"
}
for s.dataPick.Clicked(gtx) {
s.pathFeedback = "请选择默认 User Data Dir"
}
for s.dataSearch.Clicked(gtx) {
s.pathFeedback = "正在搜索默认 User Data Dir…"
}
for s.logPick.Clicked(gtx) {
s.pathFeedback = "请选择日志目录"
}
for s.logSearch.Clicked(gtx) {
s.pathFeedback = "正在搜索日志目录…"
}
2026-07-22 15:17:08 +08:00
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})
}
2026-07-22 15:13:55 +08:00
}
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),
2026-07-22 15:30:03 +08:00
layout.Rigid(s.pathField("Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
2026-07-22 15:13:55 +08:00
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
2026-07-22 15:30:03 +08:00
layout.Rigid(s.pathField("Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
2026-07-22 15:13:55 +08:00
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
2026-07-22 15:30:03 +08:00
layout.Rigid(s.pathField("默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
2026-07-22 15:13:55 +08:00
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
2026-07-22 15:30:03 +08:00
layout.Rigid(s.pathField("日志目录", &s.logDir, &s.logPick, &s.logSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(material.Caption(s.theme, s.pathFeedback).Layout),
2026-07-22 15:13:55 +08:00
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),
)
}),
)
}
2026-07-22 15:30:03 +08:00
func (s *Shell) pathField(label string, editor *widget.Editor, pick, search *widget.Clickable) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Right: unit.Dp(12)}.Layout(gtx, material.Body2(s.theme, label).Layout)
}),
layout.Flexed(1, material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(material.Button(s.theme, search, "搜索").Layout),
)
}
}
2026-07-22 15:24:25 +08:00
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),
)
}),
)
}
2026-07-22 15:13:55 +08:00
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`)
}