feat: add instance list columns and start action

This commit is contained in:
QiuSW
2026-07-22 16:22:59 +08:00
parent c11a64f931
commit 7014f857f1
8 changed files with 105 additions and 33 deletions
+72 -18
View File
@@ -61,10 +61,11 @@ type directoryPickResult struct {
// 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
ID string
Name string
Browser string
UserDataDir string
Status string
}
type SettingsState struct {
@@ -113,6 +114,9 @@ type Shell struct {
list widget.List
rows []InstanceRow
startClicks map[string]*widget.Clickable
nextInstance uint64
instanceFeedback string
onSave func(SettingsState)
pathSearcher PathSearcher
invalidate func()
@@ -125,16 +129,16 @@ type Shell struct {
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: "已退出"},
{ID: "demo-operations", Name: "运营主账号", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\operations`, Status: "运行中"},
{ID: "demo-ads", Name: "广告投放", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\ads`, Status: "启动中"},
{ID: "demo-assets", Name: "素材采集", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\assets`, Status: "外部占用"},
{ID: "demo-backup", Name: "备用环境", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\backup`, Status: "已退出"},
}, searches: map[PathField]*pathSearchState{
PathChromeExecutable: {},
PathEdgeExecutable: {},
PathDefaultUserData: {},
PathLogDirectory: {},
}, searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
}, startClicks: make(map[string]*widget.Clickable), nextInstance: 4, 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`)
@@ -149,6 +153,12 @@ func (s *Shell) SetInstances(rows []InstanceRow) {
return
}
s.rows = append([]InstanceRow(nil), rows...)
for i := range s.rows {
if s.rows[i].ID == "" {
s.nextInstance++
s.rows[i].ID = fmt.Sprintf("restored-%d", s.nextInstance)
}
}
}
func (s *Shell) SetSettings(value SettingsState) {
@@ -185,7 +195,8 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
}
for s.createClick.Clicked(gtx) {
if name := s.instanceName.Text(); name != "" {
s.rows = append(s.rows, InstanceRow{Name: name, Browser: browserDisplay(s.browserKind.Value), Profile: "Default", Status: "已退出"})
s.nextInstance++
s.rows = append(s.rows, InstanceRow{ID: fmt.Sprintf("instance-%d", s.nextInstance), Name: name, Browser: browserDisplay(s.browserKind.Value), UserDataDir: s.instanceDir.Text(), Status: "已退出"})
s.page = pageInstances
s.formFeedback = ""
} else {
@@ -198,6 +209,11 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
for s.backClick.Clicked(gtx) {
s.page = pageInstances
}
for _, row := range s.rows {
for s.startClickFor(row.ID).Clicked(gtx) {
s.requestStart(row.ID)
}
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(s.sidebar),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
@@ -244,27 +260,45 @@ func (s *Shell) instances(gtx layout.Context) layout.Dimensions {
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.Rigid(s.instanceHeader),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.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.Flexed(30, material.Body1(s.theme, row.Name).Layout),
layout.Flexed(14, material.Body2(s.theme, row.Browser).Layout),
layout.Flexed(26, pathCell(s.theme, row.UserDataDir)),
layout.Flexed(16, statusLabel(s.theme, row.Status)),
layout.Flexed(14, func(gtx layout.Context) layout.Dimensions {
return material.Button(s.theme, s.startClickFor(row.ID), "启动").Layout(gtx)
}),
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),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(material.Caption(s.theme, s.instanceFeedback).Layout),
)
}
func (s *Shell) instanceHeader(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(30, material.Caption(s.theme, "实例名称").Layout),
layout.Flexed(14, material.Caption(s.theme, "浏览器类型").Layout),
layout.Flexed(26, material.Caption(s.theme, "用户数据目录").Layout),
layout.Flexed(16, material.Caption(s.theme, "状态").Layout),
layout.Flexed(14, material.Caption(s.theme, "启动").Layout),
)
}
func pathCell(theme *material.Theme, path string) layout.Widget {
style := material.Body2(theme, path)
style.MaxLines = 2
return style.Layout
}
func statusLabel(theme *material.Theme, status string) layout.Widget {
style := material.Label(theme, unit.Sp(13), status)
style.Color = map[string]color.NRGBA{
@@ -511,6 +545,26 @@ func browserDisplay(value string) string {
return "Chrome"
}
func (s *Shell) startClickFor(id string) *widget.Clickable {
if click := s.startClicks[id]; click != nil {
return click
}
click := new(widget.Clickable)
s.startClicks[id] = click
return click
}
func (s *Shell) requestStart(id string) {
for _, row := range s.rows {
if row.ID != id {
continue
}
s.instanceFeedback = fmt.Sprintf("%s 的启动命令等待 BrowserManager 接入。", row.Name)
return
}
s.instanceFeedback = "找不到要启动的实例。"
}
func (s *Shell) chooseInstanceDirectory() {
if s.directoryPick.running {
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
+17
View File
@@ -21,6 +21,23 @@ func TestShellStartsWithSemanticInstanceStatuses(t *testing.T) {
}
}
func TestShellInstanceRowsContainRequiredColumnsAndIndependentStartControls(t *testing.T) {
shell := NewShell(material.NewTheme())
if len(shell.rows) < 2 {
t.Fatal("expected fixture rows")
}
for _, row := range shell.rows {
if row.ID == "" || row.Name == "" || row.Browser == "" || row.UserDataDir == "" || row.Status == "" {
t.Fatalf("incomplete instance row: %#v", row)
}
}
first := shell.startClickFor(shell.rows[0].ID)
second := shell.startClickFor(shell.rows[1].ID)
if first == second {
t.Fatal("rows share a start button state")
}
}
func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) {
shell := NewShell(material.NewTheme())
started := make(chan struct{}, 1)