feat(ui): 条目管理增强 — 重命名/删除/重排序

每行右侧增加 ⋯ 按钮,弹出菜单支持重命名、删除、上移、下移。
- renameEntry / deleteEntry / moveEntry 三个 appState 方法
- rowActionMenu 弹出菜单 + renameDialog + confirmDeleteDialog
- 删除自动注销快捷键,所有操作触发配置保存
- 保留顶栏 Add/Remove 全局按钮不变
This commit is contained in:
2026-06-12 09:19:07 +08:00
parent c0dd3b9028
commit 352b1d24b8
7 changed files with 461 additions and 1 deletions
+117
View File
@@ -0,0 +1,117 @@
package ui
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
)
// rowAction is what the per-row ⋯ popup returns to the caller.
type rowAction int
const (
rowActionNone rowAction = iota
rowActionRename
rowActionDelete
rowActionMoveUp
rowActionMoveDown
)
// rowActionMenu is the small popup opened by the ⋯ button on each entry
// row. It carries the target entry index so the caller knows which row
// the chosen action applies to. moveUp/moveDown are disabled (rendered
// but inert) when the index is at a boundary.
type rowActionMenu struct {
idx int
canMoveUp bool
canMoveDown bool
renameBtn widget.Clickable
deleteBtn widget.Clickable
moveUpBtn widget.Clickable
moveDownBtn widget.Clickable
cancelBtn widget.Clickable
}
func newRowActionMenu(idx, total int) *rowActionMenu {
return &rowActionMenu{
idx: idx,
canMoveUp: idx > 0,
canMoveDown: idx < total-1,
}
}
// Pick reports the user's choice and whether the menu should close.
func (m *rowActionMenu) Pick(gtx layout.Context) (rowAction, bool) {
switch {
case m.renameBtn.Clicked(gtx):
return rowActionRename, true
case m.deleteBtn.Clicked(gtx):
return rowActionDelete, true
case m.moveUpBtn.Clicked(gtx):
if !m.canMoveUp {
return rowActionNone, false
}
return rowActionMoveUp, true
case m.moveDownBtn.Clicked(gtx):
if !m.canMoveDown {
return rowActionNone, false
}
return rowActionMoveDown, true
case m.cancelBtn.Clicked(gtx):
return rowActionNone, true
}
return rowActionNone, false
}
func (m *rowActionMenu) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
fillBackground(gtx, activePalette.ScrimBg)
row := func(btn *widget.Clickable, label string, enabled bool) layout.FlexChild {
return layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Top: unit.Dp(4), Bottom: unit.Dp(4)}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
b := material.Button(th, btn, label)
if !enabled {
// Visually dim the disabled state; Pick also
// refuses to dispatch the action so clicks
// are inert beyond the colour change.
b.Background = activePalette.RingBg
b.Color = activePalette.MutedFg
}
return b.Layout(gtx)
})
})
}
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Max.X = gtx.Dp(260)
return widget.Border{
Color: activePalette.DialogBorder,
CornerRadius: unit.Dp(4),
Width: unit.Dp(1),
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
fillBackground(gtx, activePalette.DialogBg)
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.H6(th, i18n.T("dialog_row_actions_title")).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
row(&m.renameBtn, i18n.T("action_rename"), true),
row(&m.deleteBtn, i18n.T("action_delete"), true),
row(&m.moveUpBtn, i18n.T("action_move_up"), m.canMoveUp),
row(&m.moveDownBtn, i18n.T("action_move_down"), m.canMoveDown),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(material.Button(th, &m.cancelBtn, i18n.T("btn_cancel")).Layout),
)
}),
)
})
})
})
}