feat: add settings page visual boundaries
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
| T-305 | GUI 受管实例优雅停止与启停状态机 | T-304 | DONE |
|
||||
| T-306 | 精简代理下拉编辑与实例行安全操作布局 | T-304,T-305 | DONE |
|
||||
| T-307 | 受管浏览器意外退出监控与状态提示 | T-101,T-305,T-306 | DONE |
|
||||
| T-308 | 设置页分区与控件边框层级 | T-306 | DONE |
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
## 快照
|
||||
|
||||
- 日期:2026-07-25
|
||||
- 阶段:Phase 3 真实实例操作(T-301 至 T-307、T-201 至 T-208 已完成)
|
||||
- 日期:2026-07-26
|
||||
- 阶段:Phase 3 真实实例操作(T-301 至 T-308、T-201 至 T-208 已完成)
|
||||
- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、loopback CDP 端口分配与端点校验、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-208、T-301 至 T-307 已完成
|
||||
- UI:Gio 双页 Shell 使用左侧“实例/设置”导航;实例页以等宽“新建实例 / 刷新实例状态”命令区开始,窄内容区自动堆叠。列表固定显示实例名称、浏览器类型、用户数据目录、调试端口、状态和带边框的操作列;操作顺序固定为启动/停止(或重新检测)、编辑、删除,删除与相邻操作保持更大间距以降低误点。主操作在已退出时启动,在 Chub 托管运行或调试不可用时显示停止,在启动/停止中禁用,在外部关联、外部占用或未知占用时仅重新检测;优雅停止通过后台 adapter 验证 registry 的 PID/profile 身份并等待其释放,绝不按进程名关闭或接管外部 Chrome/Edge。刷新通过后台回调只检查已保存实例,以配置快照丢弃编辑或删除后的过期结果;它结合 Chub registry、指定 profile 的外部占用证据和 loopback CDP 端点更新状态,但不扫描、接管或关闭其他 Chrome/Edge。双击、Enter 或编辑图标打开实例编辑弹层,支持名称、浏览器类型、User Data Dir、启动 URL、完整地址代理选择和只读实际端口;保存保留未公开启动选项,Escape 对脏表单先请求确认,活跃/外部关联实例锁定身份约束字段。设置页使用完整代理地址选择器、地址输入及保存/删除操作;选择会回填地址,保存按稳定 `proxyId` 新增或更新,删除未引用代理前必须确认,仍被实例引用的代理会被拒绝。启动通过异步回调接到 Windows 浏览器启动器:未占用目录从已保存的起始端口(空值默认 9666)选择 loopback CDP 端口,并在启动时按已选代理 ID 解析最新的 `--proxy-server` 参数;同目录外部浏览器只有在 `DevToolsActivePort` 与 CDP 端点可验证时才显示“外部已关联”,且不会接管其生命周期。删除使用确认弹层,只删除 Chub 实例配置而不删除 User Data Dir。新建、编辑和删除实例会保存到本地配置;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter
|
||||
- T-307:本应用会话启动的受管 Chrome/Edge 根进程由单实例后台 `Wait` 监控;意外退出按 ID、启动代次和 PID 验证后立即变为“已退出”,清空运行时 PID/端口,并以可关闭、可合并的提示告知用户。提示关闭后焦点回到启动操作;Chub 请求停止、外部实例、过期事件和应用关闭取消监控均不提示。
|
||||
- T-308:设置页以可滚动的外层工作区承载浏览器路径、默认目录、运行行为和代理分区;分区、字段、状态提示和保存操作区使用统一的浅色圆角描边。路径字段在紧凑内容区会将输入与“选择/搜索”操作纵向重排,切换页面后滚动、输入与异步搜索状态仍由持久 Gio widget 保留。
|
||||
- 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目
|
||||
- blocker:无;T-307 已完成,下一项从 Backlog 或后续需求确定。
|
||||
- blocker:无;T-308 已完成,下一项从 Backlog 或后续需求确定。
|
||||
|
||||
## 当前目录
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
id: T-308
|
||||
title: 设置页分区与控件边框层级
|
||||
phase: 3
|
||||
deps: [T-306]
|
||||
status: DONE
|
||||
created: 2026-07-26
|
||||
owner: codex
|
||||
---
|
||||
|
||||
## 需求与背景
|
||||
|
||||
设置页目前只有部分卡片和操作区具有边界,浏览器路径、目录、端口、代理和退出行为等组件在 Gio 实现中缺少清晰的视觉归属。需要在不改变现有保存、搜索、代理编辑或键盘交互的前提下,为设置模块整体及关键组件补充克制、一致的边框层级。
|
||||
|
||||
## 方案与边界
|
||||
|
||||
- 设置页采用三层层级:页面内容外层工作区、功能分区卡片、字段/状态/操作条。描边使用浅色 1dp、圆角与既有实例列表一致,不使用装饰性阴影。
|
||||
- 浏览器路径、默认目录、运行行为和代理分别具有可识别的分区;路径、端口和代理地址的标签、说明、输入与关联操作保持在同一个字段边界内。代理删除仍使用既有确认流程。
|
||||
- 底部“保存设置/取消”保持现有命令和顺序,仅以带上分隔线的操作区与表单内容分离。紧凑宽度下分区和字段按阅读顺序纵向排列,不裁剪按钮或文本。
|
||||
- 仅调整设置页的视觉容器和间距;不修改实例页、创建/编辑实例表单、配置格式、浏览器启动、代理校验及异步搜索逻辑。
|
||||
- 同步更新 `docs/ui/browser-manager.html`,作为设计交接基准;HTML 原型不代表 Gio 原生主题、DPI 或无障碍已完全验证。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 设置整体、浏览器路径、默认目录、运行行为、代理、路径字段、端口字段、状态提示和保存操作区均具有清晰且不过重的边框层级。
|
||||
- 既有四个路径的选择/搜索/取消搜索、远程调试起始端口、退出时关闭托管浏览器、代理选择/保存/删除确认、保存/取消设置行为保持不变。
|
||||
- 窄窗口按纵向重排,标签与帮助文本可读,按钮不被裁剪;Tab 顺序仍与视觉和阅读顺序一致。
|
||||
- HTML 原型脚本可解析;`gofmt`、`go test ./...`、`go vet ./...`、`go build -o build/chub.exe ./cmd/chub` 和 Windows smoke 通过。
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:设置内容改为持久 `widget.List` 承载的可滚动工作区;Gio 使用外层工作区、路径/目录/运行/代理分区卡片、字段容器、状态提示与操作条的浅色圆角 1dp 边框。路径字段在窄内容区会将编辑框与“选择/搜索”操作纵向重排,既有路径搜索、代理编辑和保存命令不变。HTML 原型同步了相同的工作区、卡片、字段、状态和操作区边框层级。
|
||||
- 验证:`gofmt -w internal/ui/shell.go`、`go test ./internal/ui`、HTML 内嵌脚本语法检查、`scripts/smoke-windows.ps1`(含 `go test ./...`、`go vet ./...`、Windows 构建和 CLI JSON contract)均通过。
|
||||
- 阻塞:无。
|
||||
@@ -174,16 +174,22 @@
|
||||
.toast { position: fixed; right: 22px; bottom: 22px; z-index: 30; max-width: min(420px, calc(100vw - 44px)); padding: 12px 15px; border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); color: var(--text); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .16s ease, transform .16s ease; }
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
.page-hidden { display: none !important; }
|
||||
.settings-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); gap: 18px; }
|
||||
.settings-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 22px; display: grid; gap: 20px; }
|
||||
.settings-workspace { padding: 16px; border: 1px solid var(--line); border-radius: calc(var(--radius-card) + 2px); background: var(--surface-2); }
|
||||
.settings-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); gap: 16px; }
|
||||
.settings-card { background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-card); padding: 20px; display: grid; gap: 16px; }
|
||||
.settings-card h2 { margin: 0; font-size: 18px; letter-spacing: -.02em; }
|
||||
.settings-card > p { margin: -12px 0 0; color: var(--muted); line-height: 1.5; }
|
||||
.settings-card > p { margin: -10px 0 0; color: var(--muted); line-height: 1.5; }
|
||||
.settings-card .form-section, .settings-card .setting-list { padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
|
||||
.settings-card .form-section { gap: 12px; }
|
||||
.settings-card .field { padding: 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); }
|
||||
.settings-card .field:focus-within { border-color: var(--accent); box-shadow: var(--focus); }
|
||||
.settings-card .check-line { padding: 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); }
|
||||
.path-field { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; }
|
||||
.radio-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; min-height: 38px; }
|
||||
.radio-row label { font-weight: 500; display: inline-flex; gap: 7px; align-items: center; }
|
||||
.settings-status { min-height: 42px; padding: 11px 12px; border-radius: 9px; background: var(--success-soft); color: var(--success); font-size: 12px; line-height: 1.45; }
|
||||
.settings-status { min-height: 42px; padding: 11px 12px; border: 1px solid color-mix(in srgb, var(--success) 35%, var(--line)); border-radius: 9px; background: var(--success-soft); color: var(--success); font-size: 12px; line-height: 1.45; }
|
||||
.settings-status.warning { background: var(--warning-soft); color: var(--warning); }
|
||||
.settings-actions { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--line); padding-top: 18px; }
|
||||
.settings-actions { display: flex; justify-content: flex-end; gap: 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); padding: 12px; }
|
||||
.setting-list { display: grid; gap: 12px; }
|
||||
.setting-list .detail-line { padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
||||
.setting-list .detail-line:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
@@ -253,7 +259,7 @@
|
||||
</section>
|
||||
</section>
|
||||
<section class="content page-hidden" id="settingsPage" aria-label="应用设置">
|
||||
<div class="settings-grid">
|
||||
<div class="settings-workspace"><div class="settings-grid">
|
||||
<form class="settings-card" id="settingsForm">
|
||||
<div><h2>浏览器与目录</h2><p>这些设置用于新建实例的默认值;已有实例的配置不会被静默修改。</p></div>
|
||||
<div class="form-section"><h3>浏览器路径</h3>
|
||||
@@ -268,7 +274,7 @@
|
||||
<div class="settings-actions"><button type="button" class="secondary" id="discardSettings">取消</button><button type="submit" class="primary">保存设置</button></div>
|
||||
</form>
|
||||
<aside class="settings-card"><div><h2>运行行为</h2><p>影响后续启动和应用关闭时的默认行为。</p></div><div class="setting-list"><label class="check-line"><input type="checkbox" id="closeOnExit"><span><b>退出 Chub 时关闭本工具启动的实例</b><br><small class="help">默认关闭。不会影响外部浏览器。</small></span></label><div class="field"><label for="remoteDebugPort">远程调试起始端口</label><input id="remoteDebugPort" type="number" min="1024" max="65535" value="9666" inputmode="numeric"><small>留空时使用 9666。启动实例仅绑定 127.0.0.1;端口被占用时顺序尝试下一个可用端口。</small></div><div class="settings-status" id="settingsStatus">设置已保存 · 最后更新:刚刚</div></div></aside>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+144
-47
@@ -302,6 +302,7 @@ type Shell struct {
|
||||
proxyDeleteFocus bool
|
||||
|
||||
list widget.List
|
||||
settingsList widget.List
|
||||
rows []InstanceRow
|
||||
selectedInstanceID string
|
||||
rowClicks map[string]*widget.Clickable
|
||||
@@ -372,6 +373,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
s.editBrowserKind.Value = "chrome"
|
||||
s.editPort.ReadOnly = true
|
||||
s.list.Axis = layout.Vertical
|
||||
s.settingsList.Axis = layout.Vertical
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1476,51 +1478,125 @@ func (s *Shell) settings(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, "配置浏览器可执行文件、默认目录和本地运行行为").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||
layout.Rigid(s.pathField(PathChromeExecutable, "Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField(PathEdgeExecutable, "Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField(PathDefaultUserData, "默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(s.remoteDebugPortField),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.pathFeedback).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(s.proxySettings),
|
||||
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),
|
||||
)
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return material.List(s.theme, &s.settingsList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
|
||||
return layout.Inset{Bottom: unit.Dp(8)}.Layout(gtx, s.settingsWorkspace)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) settingsWorkspace(gtx layout.Context) layout.Dimensions {
|
||||
return s.settingsSurface(color.NRGBA{R: 199, G: 207, B: 218, A: 255}, unit.Dp(10), unit.Dp(12), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.settingsSection("浏览器路径", "分别指定 Chrome 与 Edge 的可执行文件;留空时仍可使用自动发现。", func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.pathField(PathChromeExecutable, "Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.pathField(PathEdgeExecutable, "Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
|
||||
)
|
||||
})),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.settingsSection("默认目录", "这些目录只作为新建实例和诊断日志的默认位置,不会移动或删除已有 profile。", func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.pathField(PathDefaultUserData, "默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
|
||||
)
|
||||
})),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.settingsSection("运行行为", "影响后续启动时的端口分配,以及退出 Chub 时的默认处理。", func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.remoteDebugPortField),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.settingsField(func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(s.theme, &s.closeOnExit, "退出时关闭托管浏览器").Layout(gtx)
|
||||
})),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.settingsFeedback(s.pathFeedback)),
|
||||
)
|
||||
})),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.settingsSection("代理", "仅保存无认证 scheme://host:port 端点;下拉框和实例表单显示完整地址。", s.proxySettings)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.settingsActionBar(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),
|
||||
)
|
||||
})),
|
||||
)
|
||||
})(gtx)
|
||||
}
|
||||
|
||||
func (s *Shell) settingsSection(title, description string, content layout.Widget) layout.Widget {
|
||||
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(8), unit.Dp(12), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.H6(s.theme, title).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, description).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(content),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Shell) settingsField(content layout.Widget) layout.Widget {
|
||||
return s.settingsSurface(color.NRGBA{R: 225, G: 230, B: 238, A: 255}, unit.Dp(6), unit.Dp(10), content)
|
||||
}
|
||||
|
||||
func (s *Shell) settingsActionBar(content layout.Widget) layout.Widget {
|
||||
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(6), unit.Dp(8), content)
|
||||
}
|
||||
|
||||
func (s *Shell) settingsFeedback(message string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(6), unit.Dp(8), material.Caption(s.theme, message).Layout)(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) settingsSurface(border color.NRGBA, radius, padding unit.Dp, content layout.Widget) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rect := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
if rect.Empty() {
|
||||
return layout.Dimensions{Size: rect.Max}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, border, clip.UniformRRect(rect, gtx.Dp(radius)).Op(gtx.Ops))
|
||||
inner := rect.Inset(gtx.Dp(1))
|
||||
if !inner.Empty() {
|
||||
paint.FillShape(gtx.Ops, s.theme.Palette.Bg, clip.UniformRRect(inner, gtx.Dp(radius-unit.Dp(1))).Op(gtx.Ops))
|
||||
}
|
||||
return layout.Dimensions{Size: rect.Max}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: padding, Right: padding, Bottom: padding, Left: padding}.Layout(gtx, content)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) proxySettings(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.H6(s.theme, "代理").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "仅保存无认证 scheme://host:port 端点;下拉框和实例表单显示完整地址。").Layout),
|
||||
layout.Rigid(s.settingsField(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerSettings) })),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerSettings) }),
|
||||
layout.Rigid(s.settingsField(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer))),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(s.settingsActionBar(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, &s.proxySave, "保存代理").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(s.proxyDeleteButton),
|
||||
)
|
||||
}),
|
||||
})),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.proxyFeedback).Layout),
|
||||
layout.Rigid(s.settingsFeedback(s.proxyFeedback)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1536,26 +1612,47 @@ func (s *Shell) proxyDeleteButton(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
|
||||
func (s *Shell) remoteDebugPortField(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "留空时使用 9666;仅绑定 127.0.0.1,启动时顺序尝试下一个可用端口").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.remoteDebugPort, "例如:9666").Layout),
|
||||
)
|
||||
return s.settingsField(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "留空时使用 9666;仅绑定 127.0.0.1,启动时顺序尝试下一个可用端口").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.remoteDebugPort, "例如:9666").Layout),
|
||||
)
|
||||
})(gtx)
|
||||
}
|
||||
|
||||
func (s *Shell) pathField(field 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, s.searchButtonLabel(field)).Layout),
|
||||
)
|
||||
return s.settingsField(func(gtx layout.Context) layout.Dimensions {
|
||||
pathActions := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, search, s.searchButtonLabel(field)).Layout),
|
||||
)
|
||||
}
|
||||
children := []layout.FlexChild{
|
||||
layout.Rigid(material.Body2(s.theme, label).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
}
|
||||
if gtx.Constraints.Max.X <= gtx.Dp(640) {
|
||||
children = append(children,
|
||||
layout.Rigid(material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(pathActions),
|
||||
)
|
||||
} else {
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(pathActions),
|
||||
)
|
||||
}))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
})(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user