feat(v2): compact monitor status row, small buttons, screenshot gallery
Right panel now shows connection+detection status on one line, small start/stop buttons on one line, event text with the red-note inline, and a 3x3 reverse-chronological gallery of recent confirmed screenshots filling the rest. Double-clicking a thumbnail opens a full-image overlay. Gallery items are collected off the UI thread from confirmed updates (deduped by event id) and each ImageOp is built once and cached. Cross-compiles for Windows; no new deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -595,3 +595,12 @@
|
|||||||
- 阻塞:窗口目视需 Windows。
|
- 阻塞:窗口目视需 Windows。
|
||||||
- 决策:画廊数据源用 `Present` 里的确认帧(`update.Image` 是 render 输出的截图内容)+ `update.Events` 记录,按 event_id 去重、UI 线程外收集;`paint.ImageOp` 每项只建一次并缓存,避免逐帧重解;双击用点击时间窗判定,全图弹窗复用遮罩 Stack。
|
- 决策:画廊数据源用 `Present` 里的确认帧(`update.Image` 是 render 输出的截图内容)+ `update.Events` 记录,按 event_id 去重、UI 线程外收集;`paint.ImageOp` 每项只建一次并缓存,避免逐帧重解;双击用点击时间窗判定,全图弹窗复用遮罩 Stack。
|
||||||
- 下一步:改 window.go 布局与画廊,交叉编译通过后用户 Windows 目视。
|
- 下一步:改 window.go 布局与画廊,交叉编译通过后用户 Windows 目视。
|
||||||
|
|
||||||
|
## 【2026-07-23】T-307 V2 监控界面优化(代码完成 + 交叉编译验证)
|
||||||
|
|
||||||
|
- 状态:DOING(代码完成 + Windows 交叉编译通过;窗口目视需 Windows)
|
||||||
|
- 变更:重写 `v2/internal/ui/window.go` 布局——右栏:①运行状态+检测状态压一行(Critical 红)②开始/停止用 `smallButton`(小内边距/字号)同一行 ③"事件"文本与"红色=确认摔倒"说明同一行 ④剩余区域 `layoutGallery` 3×3 倒序展示最近 9 张确认截图 ⑤双击缩略图(350ms 内两次点击)经 `layoutViewer` 全屏遮罩弹全图、"关闭"按钮退出。画廊数据源:`Present` 收到含确认事件的 `update`(`update.Image` 即 render 截图内容)时按 event_id 去重入队(UI 线程外),`layout` 里每项建一次 `paint.ImageOp` 缓存、超 9 张淘汰最老。确认弹窗与图片弹窗共用遮罩 Stack。
|
||||||
|
- 验证:`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor ./internal/ui/... ./cmd/ui-spike/...` 退出码 0。未引入新依赖(仅 stdlib `time`),vendor 不变。本环境无法运行 Gio,布局/双击目视需 Windows。
|
||||||
|
- 阻塞:窗口目视需 Windows。
|
||||||
|
- 决策:缩略图与全图复用同一 `ImageOp`(Gio 只上传一次纹理,缩略图 cell 用 `Fit:Contain` 缩放);`gallery` 存指针避免拷贝 `widget.Clickable`;`viewing` 存 `*galleryItem` 指针,列表重排也稳定。
|
||||||
|
- 下一步:用户 Windows `go run ./cmd/silver-pose` 目视新布局与画廊、双击看原图;据反馈微调间距/比例后置 DONE。
|
||||||
|
|||||||
+210
-54
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
@@ -25,6 +26,8 @@ import (
|
|||||||
"silverpose/v2/internal/monitor"
|
"silverpose/v2/internal/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const galleryCapacity = 9 // 3 columns x 3 rows
|
||||||
|
|
||||||
// Light Windows theme tokens (see docs/ui/silver-pose-ui-ux-spec.md).
|
// Light Windows theme tokens (see docs/ui/silver-pose-ui-ux-spec.md).
|
||||||
var (
|
var (
|
||||||
colorBg = color.NRGBA{R: 0xEA, G: 0xF1, B: 0xF8, A: 0xFF}
|
colorBg = color.NRGBA{R: 0xEA, G: 0xF1, B: 0xF8, A: 0xFF}
|
||||||
@@ -33,7 +36,7 @@ var (
|
|||||||
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
|
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
|
||||||
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
|
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
|
||||||
colorAlert = color.NRGBA{R: 0xC6, G: 0x28, B: 0x28, A: 0xFF}
|
colorAlert = color.NRGBA{R: 0xC6, G: 0x28, B: 0x28, A: 0xFF}
|
||||||
colorScrim = color.NRGBA{A: 0x8C}
|
colorScrim = color.NRGBA{A: 0xB0}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
@@ -48,11 +51,23 @@ type Commands struct {
|
|||||||
Stop func()
|
Stop func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window is an observer of monitor updates. It owns no stream, runtime, fall
|
// galleryIncoming is a confirmed screenshot queued off the UI thread. The Gio
|
||||||
// engine or artifact writer. Background goroutines call Present/PresentAlert/
|
// op is built once on the UI thread when it is promoted into galleryItem.
|
||||||
// ReportIssue, which only mutate shared state under a lock and Invalidate; the
|
type galleryIncoming struct {
|
||||||
// Gio event loop (its own goroutine) reads that state and renders the newest
|
caption string
|
||||||
// frame. This replaces Walk's main.Synchronize and its comctl/manifest coupling.
|
image *image.RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
type galleryItem struct {
|
||||||
|
caption string
|
||||||
|
op paint.ImageOp
|
||||||
|
click widget.Clickable
|
||||||
|
lastClick time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window observes monitor updates. Background goroutines call Present/
|
||||||
|
// PresentAlert/ReportIssue, which only mutate shared state under a lock and
|
||||||
|
// Invalidate; the Gio event loop reads that state and renders.
|
||||||
type Window struct {
|
type Window struct {
|
||||||
appWindow *app.Window
|
appWindow *app.Window
|
||||||
theme *material.Theme
|
theme *material.Theme
|
||||||
@@ -66,6 +81,8 @@ type Window struct {
|
|||||||
imageGen uint64
|
imageGen uint64
|
||||||
pending *alert.Record
|
pending *alert.Record
|
||||||
seenAlerts map[string]struct{}
|
seenAlerts map[string]struct{}
|
||||||
|
incoming []galleryIncoming
|
||||||
|
seenEvents map[string]struct{}
|
||||||
|
|
||||||
// Touched only by the event-loop goroutine.
|
// Touched only by the event-loop goroutine.
|
||||||
imageOp paint.ImageOp
|
imageOp paint.ImageOp
|
||||||
@@ -76,7 +93,9 @@ type Window struct {
|
|||||||
startButton widget.Clickable
|
startButton widget.Clickable
|
||||||
stopButton widget.Clickable
|
stopButton widget.Clickable
|
||||||
ackButton widget.Clickable
|
ackButton widget.Clickable
|
||||||
running bool
|
gallery []*galleryItem
|
||||||
|
viewing *galleryItem
|
||||||
|
viewerClose widget.Clickable
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
||||||
@@ -105,15 +124,13 @@ func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
|||||||
spec: spec,
|
spec: spec,
|
||||||
model: model,
|
model: model,
|
||||||
seenAlerts: make(map[string]struct{}),
|
seenAlerts: make(map[string]struct{}),
|
||||||
|
seenEvents: make(map[string]struct{}),
|
||||||
tabs: []string{"监控", "设置"},
|
tabs: []string{"监控", "设置"},
|
||||||
}
|
}
|
||||||
window.tabButton = make([]widget.Clickable, len(window.tabs))
|
window.tabButton = make([]widget.Clickable, len(window.tabs))
|
||||||
return window, nil
|
return window, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run drives the platform event loop on the calling (main) goroutine and the
|
|
||||||
// window loop on a separate goroutine. It does not return; the process exits
|
|
||||||
// after the window is closed and Stop cleanup has run.
|
|
||||||
func (window *Window) Run() int {
|
func (window *Window) Run() int {
|
||||||
go func() {
|
go func() {
|
||||||
err := window.loop()
|
err := window.loop()
|
||||||
@@ -142,10 +159,6 @@ func (window *Window) loop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) start() {
|
func (window *Window) start() {
|
||||||
if window.running {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
window.running = true
|
|
||||||
if window.commands.Start != nil {
|
if window.commands.Start != nil {
|
||||||
window.commands.Start()
|
window.commands.Start()
|
||||||
}
|
}
|
||||||
@@ -155,10 +168,9 @@ func (window *Window) stop() {
|
|||||||
if window.commands.Stop != nil {
|
if window.commands.Stop != nil {
|
||||||
window.commands.Stop()
|
window.commands.Stop()
|
||||||
}
|
}
|
||||||
window.running = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present stores the newest analysed frame and state, then requests a redraw.
|
// Present stores the newest analysed frame and queues any confirmed screenshot.
|
||||||
func (window *Window) Present(update monitor.Update) {
|
func (window *Window) Present(update monitor.Update) {
|
||||||
window.mu.Lock()
|
window.mu.Lock()
|
||||||
state := highestState(update.Result)
|
state := highestState(update.Result)
|
||||||
@@ -170,6 +182,22 @@ func (window *Window) Present(update monitor.Update) {
|
|||||||
window.latestImage = update.Image
|
window.latestImage = update.Image
|
||||||
window.imageGen++
|
window.imageGen++
|
||||||
}
|
}
|
||||||
|
if update.Image != nil {
|
||||||
|
for _, record := range update.Events {
|
||||||
|
id := record.Event.EventID
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, seen := window.seenEvents[id]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
window.seenEvents[id] = struct{}{}
|
||||||
|
window.incoming = append(window.incoming, galleryIncoming{
|
||||||
|
caption: fmt.Sprintf("%s %s", record.Event.TrackID, time.Now().Format("15:04:05")),
|
||||||
|
image: update.Image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
window.mu.Unlock()
|
window.mu.Unlock()
|
||||||
window.appWindow.Invalidate()
|
window.appWindow.Invalidate()
|
||||||
}
|
}
|
||||||
@@ -208,44 +236,82 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
|
|
||||||
window.mu.Lock()
|
window.mu.Lock()
|
||||||
model := window.model
|
model := window.model
|
||||||
image := window.latestImage
|
frame := window.latestImage
|
||||||
gen := window.imageGen
|
gen := window.imageGen
|
||||||
pending := window.pending
|
pending := window.pending
|
||||||
|
incoming := window.incoming
|
||||||
|
window.incoming = nil
|
||||||
window.mu.Unlock()
|
window.mu.Unlock()
|
||||||
|
|
||||||
hasImage := image != nil
|
for i := range incoming {
|
||||||
if hasImage && gen != window.imageShownGen {
|
item := &galleryItem{caption: incoming[i].caption, op: paint.NewImageOp(incoming[i].image)}
|
||||||
window.imageOp = paint.NewImageOp(image)
|
window.gallery = append([]*galleryItem{item}, window.gallery...)
|
||||||
|
}
|
||||||
|
if len(window.gallery) > galleryCapacity {
|
||||||
|
window.gallery = window.gallery[:galleryCapacity]
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFrame := frame != nil
|
||||||
|
if hasFrame && gen != window.imageShownGen {
|
||||||
|
window.imageOp = paint.NewImageOp(frame)
|
||||||
window.imageShownGen = gen
|
window.imageShownGen = gen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, item := range window.gallery {
|
||||||
|
if item.click.Clicked(gtx) {
|
||||||
|
if !item.lastClick.IsZero() && now.Sub(item.lastClick) < 350*time.Millisecond {
|
||||||
|
window.viewing = item
|
||||||
|
}
|
||||||
|
item.lastClick = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if window.viewerClose.Clicked(gtx) {
|
||||||
|
window.viewing = nil
|
||||||
|
}
|
||||||
|
if window.ackButton.Clicked(gtx) {
|
||||||
|
window.mu.Lock()
|
||||||
|
window.pending = nil
|
||||||
|
window.mu.Unlock()
|
||||||
|
pending = nil
|
||||||
|
}
|
||||||
|
|
||||||
content := func(gtx layout.Context) layout.Dimensions {
|
content := func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(window.layoutTabs),
|
layout.Rigid(window.layoutTabs),
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
if window.active == 0 {
|
if window.active == 0 {
|
||||||
return window.layoutMonitor(gtx, model, hasImage)
|
return window.layoutMonitor(gtx, model, hasFrame)
|
||||||
}
|
}
|
||||||
return window.layoutSettings(gtx, model)
|
return window.layoutSettings(gtx, model)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pending == nil {
|
children := []layout.StackChild{layout.Expanded(content)}
|
||||||
return content(gtx)
|
if window.viewing != nil {
|
||||||
|
children = append(children, layout.Expanded(scrim), layout.Expanded(window.layoutViewer))
|
||||||
}
|
}
|
||||||
return layout.Stack{}.Layout(gtx,
|
if pending != nil {
|
||||||
layout.Expanded(content),
|
record := *pending
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
children = append(children,
|
||||||
paint.FillShape(gtx.Ops, colorScrim, clip.Rect{Max: gtx.Constraints.Min}.Op())
|
layout.Expanded(scrim),
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
|
||||||
}),
|
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return window.layoutDialog(gtx, *pending)
|
return window.layoutDialog(gtx, record)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
if len(children) == 1 {
|
||||||
|
return content(gtx)
|
||||||
|
}
|
||||||
|
return layout.Stack{}.Layout(gtx, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrim(gtx layout.Context) layout.Dimensions {
|
||||||
|
paint.FillShape(gtx.Ops, colorScrim, clip.Rect{Max: gtx.Constraints.Min}.Op())
|
||||||
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
|
func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -269,33 +335,36 @@ func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Flex{}.Layout(gtx, children...)
|
return layout.Flex{}.Layout(gtx, children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasImage bool) layout.Dimensions {
|
func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFrame bool) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
if !hasImage {
|
if !hasFrame {
|
||||||
return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout)
|
return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout)
|
||||||
}
|
}
|
||||||
return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Flexed(2, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(2, func(gtx layout.Context) layout.Dimensions {
|
||||||
return window.layoutStatus(gtx, model)
|
return window.layoutRightPanel(gtx, model)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutStatus(gtx layout.Context, model ViewModel) layout.Dimensions {
|
func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel) layout.Dimensions {
|
||||||
if window.startButton.Clicked(gtx) {
|
if window.startButton.Clicked(gtx) {
|
||||||
window.start()
|
window.start()
|
||||||
}
|
}
|
||||||
if window.stopButton.Clicked(gtx) {
|
if window.stopButton.Clicked(gtx) {
|
||||||
window.stop()
|
window.stop()
|
||||||
}
|
}
|
||||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(material.H6(window.theme, "运行状态").Layout),
|
// Row 1: connection status and detection status on one line.
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout),
|
layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
label := material.Body1(window.theme, model.StateText)
|
label := material.Body1(window.theme, model.StateText)
|
||||||
if model.Critical {
|
if model.Critical {
|
||||||
@@ -303,36 +372,110 @@ func (window *Window) layoutStatus(gtx layout.Context, model ViewModel) layout.D
|
|||||||
}
|
}
|
||||||
return label.Layout(gtx)
|
return label.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
// Row 2: compact start/stop buttons on one line.
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return smallButton(gtx, window.theme, &window.startButton, "开始监控")
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return smallButton(gtx, window.theme, &window.stopButton, "停止监控")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
// Row 3: event text and the red-note on one line.
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(material.Body1(window.theme, model.EventText).Layout),
|
layout.Rigid(material.Body1(window.theme, model.EventText).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
note := material.Body2(window.theme, "红色=确认摔倒")
|
||||||
|
note.Color = colorSubtle
|
||||||
|
return note.Layout(gtx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(material.Button(window.theme, &window.startButton, "开始监控").Layout),
|
layout.Rigid(material.Body2(window.theme, "最近确认截图(双击看原图)").Layout),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
layout.Rigid(material.Button(window.theme, &window.stopButton, "停止监控").Layout),
|
// Remaining area: 3x3 reverse-chronological screenshot gallery.
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Flexed(1, window.layoutGallery),
|
||||||
layout.Rigid(material.Body2(window.theme, "红色仅表示已确认摔倒。连接异常不会触发报警。").Layout),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout.Dimensions {
|
func smallButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
button := material.Button(theme, click, label)
|
||||||
|
button.Inset = layout.Inset{Top: unit.Dp(4), Bottom: unit.Dp(4), Left: unit.Dp(10), Right: unit.Dp(10)}
|
||||||
|
button.TextSize = unit.Sp(13)
|
||||||
|
return button.Layout(gtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) layoutGallery(gtx layout.Context) layout.Dimensions {
|
||||||
|
rows := make([]layout.FlexChild, 0, 3)
|
||||||
|
for r := 0; r < 3; r++ {
|
||||||
|
r := r
|
||||||
|
rows = append(rows, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
cols := make([]layout.FlexChild, 0, 3)
|
||||||
|
for c := 0; c < 3; c++ {
|
||||||
|
index := r*3 + c
|
||||||
|
cols = append(cols, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if index < len(window.gallery) {
|
||||||
|
return window.layoutCell(gtx, window.gallery[index])
|
||||||
|
}
|
||||||
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, cols...)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) layoutCell(gtx layout.Context, item *galleryItem) layout.Dimensions {
|
||||||
|
return item.click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.UniformInset(unit.Dp(3)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return widget.Image{Src: item.op, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) layoutViewer(gtx layout.Context) layout.Dimensions {
|
||||||
|
item := window.viewing
|
||||||
|
if item == nil {
|
||||||
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
|
}
|
||||||
|
return layout.UniformInset(unit.Dp(24)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(material.H6(window.theme, "来源与运行依赖(只读,不显示 RTSP 地址或密码)").Layout),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(material.Body1(window.theme, model.SettingsSummary).Layout),
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Rigid(material.Body1(window.theme, "模型 SHA-256:"+shortHash(window.settings.ModelSHA256)).Layout),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(material.Body1(window.theme, "运行依赖:"+window.settings.RuntimeSummary).Layout),
|
caption := material.Body1(window.theme, item.caption)
|
||||||
layout.Rigid(material.Body1(window.theme, "事件参数:"+window.settings.EventSummary).Layout),
|
caption.Color = colorSurface
|
||||||
layout.Rigid(material.Body2(window.theme, "参数在配置文件中维护;修改后请停止并重新开始监控。").Layout),
|
return caption.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Flexed(1, layout.Spacer{}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return smallButton(gtx, window.theme, &window.viewerClose, "关闭")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return widget.Image{Src: item.op, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
||||||
|
})
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutDialog(gtx layout.Context, record alert.Record) layout.Dimensions {
|
func (window *Window) layoutDialog(gtx layout.Context, record alert.Record) layout.Dimensions {
|
||||||
if window.ackButton.Clicked(gtx) {
|
|
||||||
window.mu.Lock()
|
|
||||||
window.pending = nil
|
|
||||||
window.mu.Unlock()
|
|
||||||
}
|
|
||||||
width := gtx.Dp(420)
|
width := gtx.Dp(420)
|
||||||
if width > gtx.Constraints.Max.X {
|
if width > gtx.Constraints.Max.X {
|
||||||
width = gtx.Constraints.Max.X
|
width = gtx.Constraints.Max.X
|
||||||
@@ -364,6 +507,19 @@ func (window *Window) layoutDialog(gtx layout.Context, record alert.Record) layo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout.Dimensions {
|
||||||
|
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(material.H6(window.theme, "来源与运行依赖(只读,不显示 RTSP 地址或密码)").Layout),
|
||||||
|
layout.Rigid(material.Body1(window.theme, model.SettingsSummary).Layout),
|
||||||
|
layout.Rigid(material.Body1(window.theme, "模型 SHA-256:"+shortHash(window.settings.ModelSHA256)).Layout),
|
||||||
|
layout.Rigid(material.Body1(window.theme, "运行依赖:"+window.settings.RuntimeSummary).Layout),
|
||||||
|
layout.Rigid(material.Body1(window.theme, "事件参数:"+window.settings.EventSummary).Layout),
|
||||||
|
layout.Rigid(material.Body2(window.theme, "参数在配置文件中维护;修改后请停止并重新开始监控。").Layout),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func highestState(result fall.FrameResult) fall.State {
|
func highestState(result fall.FrameResult) fall.State {
|
||||||
state := fall.Normal
|
state := fall.Normal
|
||||||
for _, person := range result.People {
|
for _, person := range result.People {
|
||||||
|
|||||||
Reference in New Issue
Block a user