feat: add instance list columns and start action
This commit is contained in:
+1
-1
@@ -138,7 +138,7 @@ func runWindow(logger *slog.Logger) {
|
|||||||
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, CloseOnExit: saved.Settings.CloseOnExit})
|
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, CloseOnExit: saved.Settings.CloseOnExit})
|
||||||
rows := make([]ui.InstanceRow, 0, len(saved.Instances))
|
rows := make([]ui.InstanceRow, 0, len(saved.Instances))
|
||||||
for _, item := range saved.Instances {
|
for _, item := range saved.Instances {
|
||||||
rows = append(rows, ui.InstanceRow{Name: item.Name, Browser: string(item.Launch.Kind), Profile: item.Launch.ProfileDirectory, Status: "已退出"})
|
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: string(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, Status: "已退出"})
|
||||||
}
|
}
|
||||||
shell.SetInstances(rows)
|
shell.SetInstances(rows)
|
||||||
shell.OnSave(func(value ui.SettingsState) {
|
shell.OnSave(func(value ui.SettingsState) {
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
| T-205 | 设置路径字段增加 Label、选择和搜索操作 | T-201 | DONE |
|
| T-205 | 设置路径字段增加 Label、选择和搜索操作 | T-201 | DONE |
|
||||||
| T-206 | 左侧导航、可取消路径搜索与新建实例表单语义 | T-205 | DONE |
|
| T-206 | 左侧导航、可取消路径搜索与新建实例表单语义 | T-205 | DONE |
|
||||||
| T-207 | 接入新建实例的 Windows 目录选择器 | T-206 | DONE |
|
| T-207 | 接入新建实例的 Windows 目录选择器 | T-206 | DONE |
|
||||||
| T-208 | 固化实例列表字段与启动操作列 | T-201 | TODO |
|
| T-208 | 固化实例列表字段与启动操作列 | T-201 | DONE |
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
## 快照
|
## 快照
|
||||||
|
|
||||||
- 日期:2026-07-22
|
- 日期:2026-07-22
|
||||||
- 阶段:Phase 2 产品外壳(T-201 至 T-206 已完成)
|
- 阶段:Phase 2 产品外壳(T-201 至 T-208 已完成)
|
||||||
- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-206 已完成
|
- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-208 已完成
|
||||||
- UI:Gio 双页 Shell 使用左侧“实例/设置”导航;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter
|
- UI:Gio 双页 Shell 使用左侧“实例/设置”导航;实例列表固定显示实例名称、浏览器类型、用户数据目录、状态和启动;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter
|
||||||
- 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目
|
- 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目
|
||||||
- blocker:无;下一个任务为 T-208(固化实例列表字段与启动操作列)。
|
- blocker:无;下一步可进入 BrowserManager application adapter,以及设置页路径选择器和真实 UI 操作接线。
|
||||||
|
|
||||||
## 当前目录
|
## 当前目录
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -3,7 +3,7 @@ id: T-208
|
|||||||
title: 固化实例列表字段与启动操作列
|
title: 固化实例列表字段与启动操作列
|
||||||
phase: 2
|
phase: 2
|
||||||
deps: [T-201]
|
deps: [T-201]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-22
|
created: 2026-07-22
|
||||||
owner: codex
|
owner: codex
|
||||||
---
|
---
|
||||||
@@ -29,8 +29,8 @@ owner: codex
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
- 状态:TODO
|
- 状态:DONE
|
||||||
- 变更:已建立列表字段和操作列契约。
|
- 变更:Gio 与 HTML 原型的实例列表统一为五列;实例行增加稳定 ID 与 User Data Dir;启动按钮按实例 ID 持有独立 Clickable,并提供当前 application adapter 未接入的明确反馈。
|
||||||
- 验证:待实现。
|
- 验证:`gofmt`、HTML script syntax check、`go test ./...`、`go vet ./...`、Windows build/smoke 均通过。
|
||||||
- 阻塞:无。
|
- 阻塞:无。
|
||||||
- 残余风险:真实启动仍需要 BrowserManager application adapter。
|
- 残余风险:真实启动仍需要 BrowserManager application adapter。
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## 评审场景
|
## 评审场景
|
||||||
|
|
||||||
1. 默认实例列表:运行中、启动中、被占用、已退出四种状态同时可见。
|
1. 默认实例列表:每行显示实例名称、浏览器类型、用户数据目录、状态和启动按钮;运行中、启动中、被占用、已退出四种状态同时可见。
|
||||||
2. 点击“新建实例”:检查实例名称、Chrome/Edge RadioButton、User Data Dir 选择入口、启动 URL(可选)和其他运行字段。
|
2. 点击“新建实例”:检查实例名称、Chrome/Edge RadioButton、User Data Dir 选择入口、启动 URL(可选)和其他运行字段。
|
||||||
3. 留空 user data dir 提交:观察字段级错误和输入保留。
|
3. 留空 user data dir 提交:观察字段级错误和输入保留。
|
||||||
4. 点击运行中实例:查看 PID、路径、启动地址和操作入口。
|
4. 点击运行中实例:查看 PID、路径、启动地址和操作入口。
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 顶级导航 | 稳定 page ID + 持久选中状态 |
|
| 顶级导航 | 稳定 page ID + 持久选中状态 |
|
||||||
| 实例列表 | 虚拟化列表;实例 ID 而非行号作为身份 |
|
| 实例列表 | 虚拟化列表;实例 ID 而非行号作为身份 |
|
||||||
|
| 实例列契约 | 固定为实例名称、浏览器类型、用户数据目录、状态、启动;每行启动按钮以实例 ID 独立持有状态 |
|
||||||
| 启动表单 | 持久 editor/选择状态,显式提交,字段错误保留输入 |
|
| 启动表单 | 持久 editor/选择状态,显式提交,字段错误保留输入 |
|
||||||
| 路径搜索 | 独立任务 ID、取消函数与结果通道;迟到结果按任务 ID 丢弃 |
|
| 路径搜索 | 独立任务 ID、取消函数与结果通道;迟到结果按任务 ID 丢弃 |
|
||||||
| 详情抽屉 | 自定义辅助面板,关闭后焦点返回实例行 |
|
| 详情抽屉 | 自定义辅助面板,关闭后焦点返回实例行 |
|
||||||
|
|||||||
@@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
<section class="workspace" aria-labelledby="listTitle">
|
<section class="workspace" aria-labelledby="listTitle">
|
||||||
<div class="toolbar"><div class="search"><span aria-hidden="true">⌕</span><input id="search" type="search" placeholder="搜索实例、路径或 PID" aria-label="搜索实例、路径或 PID"></div><select id="browserFilter" aria-label="筛选浏览器"><option value="all">全部浏览器</option><option value="chrome">Chrome</option><option value="edge">Edge</option></select><select id="statusFilter" aria-label="筛选状态"><option value="all">全部状态</option><option value="running">运行中</option><option value="starting">启动中</option><option value="occupied">被占用</option><option value="exited">已退出</option></select><div class="toolbar-spacer"></div><button class="secondary" id="refresh">↻ 刷新状态</button></div>
|
<div class="toolbar"><div class="search"><span aria-hidden="true">⌕</span><input id="search" type="search" placeholder="搜索实例、路径或 PID" aria-label="搜索实例、路径或 PID"></div><select id="browserFilter" aria-label="筛选浏览器"><option value="all">全部浏览器</option><option value="chrome">Chrome</option><option value="edge">Edge</option></select><select id="statusFilter" aria-label="筛选状态"><option value="all">全部状态</option><option value="running">运行中</option><option value="starting">启动中</option><option value="occupied">被占用</option><option value="exited">已退出</option></select><div class="toolbar-spacer"></div><button class="secondary" id="refresh">↻ 刷新状态</button></div>
|
||||||
<div class="table-wrap"><table><caption id="listTitle" class="sr-only">浏览器实例列表</caption><thead><tr><th scope="col">实例</th><th scope="col">状态</th><th scope="col">PID</th><th scope="col">user data dir</th><th scope="col">最近启动</th><th scope="col"><span class="sr-only">操作</span></th></tr></thead><tbody id="instanceRows"></tbody></table><div class="empty-state" id="emptyState"><strong>没有匹配的实例</strong><span>尝试清除筛选,或新建一个浏览器实例。</span></div></div>
|
<div class="table-wrap"><table><caption id="listTitle" class="sr-only">浏览器实例列表</caption><thead><tr><th scope="col">实例名称</th><th scope="col">浏览器类型</th><th scope="col">用户数据目录</th><th scope="col">状态</th><th scope="col">启动</th></tr></thead><tbody id="instanceRows"></tbody></table><div class="empty-state" id="emptyState"><strong>没有匹配的实例</strong><span>尝试清除筛选,或新建一个浏览器实例。</span></div></div>
|
||||||
<div class="table-footer"><span id="resultCount">显示 4 个实例</span><span>最后检查:刚刚</span></div>
|
<div class="table-footer"><span id="resultCount">显示 4 个实例</span><span>最后检查:刚刚</span></div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -285,9 +285,9 @@
|
|||||||
function renderRows() {
|
function renderRows() {
|
||||||
const rows = filtered(), body = $('instanceRows'); body.innerHTML = '';
|
const rows = filtered(), body = $('instanceRows'); body.innerHTML = '';
|
||||||
rows.forEach(x => { const tr = document.createElement('tr'); tr.className = x.id === selectedId ? 'selected' : ''; tr.tabIndex = 0; tr.dataset.id = x.id; tr.innerHTML = `
|
rows.forEach(x => { const tr = document.createElement('tr'); tr.className = x.id === selectedId ? 'selected' : ''; tr.tabIndex = 0; tr.dataset.id = x.id; tr.innerHTML = `
|
||||||
<td><div class="instance-name"><span class="browser-logo ${x.kind}" aria-hidden="true">${x.kind === 'chrome' ? 'C' : 'E'}</span><div><strong>${x.name}</strong><small>${x.browser} · ${x.id}</small></div></div></td>
|
<td><div class="instance-name"><span class="browser-logo ${x.kind}" aria-hidden="true">${x.kind === 'chrome' ? 'C' : 'E'}</span><div><strong>${x.name}</strong><small>${x.id}</small></div></div></td>
|
||||||
<td><span class="status ${statusClass(x.status)}">${x.statusText}</span></td><td class="mono">${x.pid}</td><td class="mono">${x.dir}</td><td>${x.started}</td>
|
<td>${x.browser}</td><td class="mono">${x.dir}</td><td><span class="status ${statusClass(x.status)}">${x.statusText}</span></td>
|
||||||
<td><div class="row-actions"><button data-action="details" aria-label="查看 ${x.name} 详情">详情</button>${x.status === 'running' || x.status === 'occupied' ? `<button data-action="stop" aria-label="关闭 ${x.name}">关闭</button>` : `<button data-action="start" aria-label="启动 ${x.name}">启动</button>`}</div></td>`;
|
<td><div class="row-actions"><button data-action="start" aria-label="启动 ${x.name}">启动</button></div></td>`;
|
||||||
tr.addEventListener('click', e => { if (!e.target.closest('button')) openDetails(x.id); });
|
tr.addEventListener('click', e => { if (!e.target.closest('button')) openDetails(x.id); });
|
||||||
tr.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDetails(x.id); } });
|
tr.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDetails(x.id); } });
|
||||||
tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectedId = x.id; if (b.dataset.action === 'details') openDetails(x.id); else if (b.dataset.action === 'stop') openConfirm(x.id, false); else openConfirm(x.id, 'start'); }));
|
tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectedId = x.id; if (b.dataset.action === 'details') openDetails(x.id); else if (b.dataset.action === 'stop') openConfirm(x.id, false); else openConfirm(x.id, 'start'); }));
|
||||||
|
|||||||
+72
-18
@@ -61,10 +61,11 @@ type directoryPickResult struct {
|
|||||||
// InstanceRow is the read-only view model used by the first Gio shell.
|
// 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.
|
// Runtime data will replace these fixtures when the application service is wired.
|
||||||
type InstanceRow struct {
|
type InstanceRow struct {
|
||||||
Name string
|
ID string
|
||||||
Browser string
|
Name string
|
||||||
Profile string
|
Browser string
|
||||||
Status string
|
UserDataDir string
|
||||||
|
Status string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsState struct {
|
type SettingsState struct {
|
||||||
@@ -113,6 +114,9 @@ type Shell struct {
|
|||||||
|
|
||||||
list widget.List
|
list widget.List
|
||||||
rows []InstanceRow
|
rows []InstanceRow
|
||||||
|
startClicks map[string]*widget.Clickable
|
||||||
|
nextInstance uint64
|
||||||
|
instanceFeedback string
|
||||||
onSave func(SettingsState)
|
onSave func(SettingsState)
|
||||||
pathSearcher PathSearcher
|
pathSearcher PathSearcher
|
||||||
invalidate func()
|
invalidate func()
|
||||||
@@ -125,16 +129,16 @@ type Shell struct {
|
|||||||
|
|
||||||
func NewShell(theme *material.Theme) *Shell {
|
func NewShell(theme *material.Theme) *Shell {
|
||||||
s := &Shell{theme: theme, rows: []InstanceRow{
|
s := &Shell{theme: theme, rows: []InstanceRow{
|
||||||
{Name: "运营主账号", Browser: "Chrome", Profile: "Default", Status: "运行中"},
|
{ID: "demo-operations", Name: "运营主账号", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\operations`, Status: "运行中"},
|
||||||
{Name: "广告投放", Browser: "Edge", Profile: "Profile 2", Status: "启动中"},
|
{ID: "demo-ads", Name: "广告投放", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\ads`, Status: "启动中"},
|
||||||
{Name: "素材采集", Browser: "Chrome", Profile: "Default", Status: "外部占用"},
|
{ID: "demo-assets", Name: "素材采集", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\assets`, Status: "外部占用"},
|
||||||
{Name: "备用环境", Browser: "Edge", Profile: "Profile 1", Status: "已退出"},
|
{ID: "demo-backup", Name: "备用环境", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\backup`, Status: "已退出"},
|
||||||
}, searches: map[PathField]*pathSearchState{
|
}, searches: map[PathField]*pathSearchState{
|
||||||
PathChromeExecutable: {},
|
PathChromeExecutable: {},
|
||||||
PathEdgeExecutable: {},
|
PathEdgeExecutable: {},
|
||||||
PathDefaultUserData: {},
|
PathDefaultUserData: {},
|
||||||
PathLogDirectory: {},
|
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.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
|
||||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||||
@@ -149,6 +153,12 @@ func (s *Shell) SetInstances(rows []InstanceRow) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.rows = append([]InstanceRow(nil), rows...)
|
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) {
|
func (s *Shell) SetSettings(value SettingsState) {
|
||||||
@@ -185,7 +195,8 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
for s.createClick.Clicked(gtx) {
|
for s.createClick.Clicked(gtx) {
|
||||||
if name := s.instanceName.Text(); name != "" {
|
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.page = pageInstances
|
||||||
s.formFeedback = ""
|
s.formFeedback = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -198,6 +209,11 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
for s.backClick.Clicked(gtx) {
|
for s.backClick.Clicked(gtx) {
|
||||||
s.page = pageInstances
|
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,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Rigid(s.sidebar),
|
layout.Rigid(s.sidebar),
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
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(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||||
layout.Rigid(material.Button(s.theme, &s.newClick, "新建实例").Layout),
|
layout.Rigid(material.Button(s.theme, &s.newClick, "新建实例").Layout),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.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 {
|
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 {
|
return material.List(s.theme, &s.list).Layout(gtx, len(s.rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||||
row := s.rows[i]
|
row := s.rows[i]
|
||||||
return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(30, material.Body1(s.theme, row.Name).Layout),
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
layout.Flexed(14, material.Body2(s.theme, row.Browser).Layout),
|
||||||
layout.Rigid(material.Subtitle1(s.theme, row.Name).Layout),
|
layout.Flexed(26, pathCell(s.theme, row.UserDataDir)),
|
||||||
layout.Rigid(material.Caption(s.theme, row.Browser+" · "+row.Profile).Layout),
|
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 {
|
func statusLabel(theme *material.Theme, status string) layout.Widget {
|
||||||
style := material.Label(theme, unit.Sp(13), status)
|
style := material.Label(theme, unit.Sp(13), status)
|
||||||
style.Color = map[string]color.NRGBA{
|
style.Color = map[string]color.NRGBA{
|
||||||
@@ -511,6 +545,26 @@ func browserDisplay(value string) string {
|
|||||||
return "Chrome"
|
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() {
|
func (s *Shell) chooseInstanceDirectory() {
|
||||||
if s.directoryPick.running {
|
if s.directoryPick.running {
|
||||||
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
|
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
|
||||||
|
|||||||
@@ -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) {
|
func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) {
|
||||||
shell := NewShell(material.NewTheme())
|
shell := NewShell(material.NewTheme())
|
||||||
started := make(chan struct{}, 1)
|
started := make(chan struct{}, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user