feat(ui): Battle.Net 序列号/恢复码展示 + 热键复制确认

- dialog_bnet_info.go: 新增 Battle.Net 信息对话框,显示序列号
  和恢复码,支持一键复制恢复码
- row_menu: 新增 rowActionBnetInfo 动作,仅对 Battle.Net 条目可见
- 热键行为确认:autoType 已自动复制到剪贴板 + 键入前台窗口
- i18n: action_bnet_info / dialog_bnet_info_title / bnet_info_intro
  / hint_bnet_restore_code_secret 三语翻译
This commit is contained in:
2026-06-12 11:22:26 +08:00
parent 1605b5b7ae
commit 089efa088d
6 changed files with 138 additions and 2 deletions
+16 -1
View File
@@ -127,6 +127,7 @@ type appState struct {
renameDlg *renameDialog
confirmDelDlg *confirmDeleteDialog
iconPickerDlg *iconPickerDialog
bnetInfoDlg *bnetInfoDialog
rowTargetIdx int
store *store
@@ -497,7 +498,13 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
total := len(st.entries)
st.mu.Unlock()
st.rowTargetIdx = moreTargetIdx
st.rowMenu = newRowActionMenu(moreTargetIdx, total)
vendor := ""
st.mu.Lock()
if moreTargetIdx >= 0 && moreTargetIdx < len(st.entries) {
vendor = st.entries[moreTargetIdx].Auth.Name()
}
st.mu.Unlock()
st.rowMenu = newRowActionMenu(moreTargetIdx, total, vendor)
}
// First-run welcome dialog. Routed before all other dialogs so the
@@ -743,6 +750,14 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
}
st.mu.Unlock()
st.iconPickerDlg = newIconPickerDialog(current)
case rowActionBnetInfo:
st.mu.Lock()
if idx >= 0 && idx < len(st.entries) {
if bn, ok := st.entries[idx].Auth.(*authenticator.BattleNetAuthenticator); ok {
st.bnetInfoDlg = newBnetInfoDialog(bn)
}
}
st.mu.Unlock()
}
w.Invalidate()
} else {
+77
View File
@@ -0,0 +1,77 @@
package ui
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"git.wxccs.org/iceking2nd/winauth-go/internal/authenticator"
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
"git.wxccs.org/iceking2nd/winauth-go/internal/win32"
)
// bnetInfoDialog shows the Battle.Net serial number and restore code.
// The restore code can be copied to the clipboard.
type bnetInfoDialog struct {
serial string
restoreCode string
copyBtn widget.Clickable
closeBtn widget.Clickable
copied bool
}
func newBnetInfoDialog(bn *authenticator.BattleNetAuthenticator) *bnetInfoDialog {
return &bnetInfoDialog{
serial: bn.Serial,
restoreCode: bn.RestoreCode(),
}
}
func (d *bnetInfoDialog) Layout(
gtx layout.Context, th *material.Theme,
onClose func(),
) layout.Dimensions {
if d.closeBtn.Clicked(gtx) {
onClose()
return layout.Dimensions{Size: gtx.Constraints.Max}
}
if d.copyBtn.Clicked(gtx) {
if err := win32.SetClipboardText(d.restoreCode); err == nil {
d.copied = true
}
}
body := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body2(th, i18n.T("bnet_info_intro")).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(material.Body2(th, i18n.T("label_serial")+": ").Layout),
layout.Rigid(material.Body1(th, d.serial).Layout),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(material.Body2(th, i18n.T("label_restore_code")+": ").Layout),
layout.Rigid(material.Body1(th, d.restoreCode).Layout),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
copyLabel := i18n.T("btn_copy")
if d.copied {
copyLabel = i18n.T("msg_copied")
}
return material.Button(th, &d.copyBtn, copyLabel).Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(material.Caption(th, i18n.T("hint_bnet_restore_code_secret")).Layout),
)
}
return modalCardCancel(gtx, th, i18n.T("dialog_bnet_info_title"),
i18n.T("btn_close"), &d.closeBtn, body)
}
+6 -1
View File
@@ -19,6 +19,7 @@ const (
rowActionMoveUp
rowActionMoveDown
rowActionChangeIcon
rowActionBnetInfo
)
// rowActionMenu is the small popup opened by the ⋯ button on each entry
@@ -29,20 +30,23 @@ type rowActionMenu struct {
idx int
canMoveUp bool
canMoveDown bool
isBnet bool
renameBtn widget.Clickable
deleteBtn widget.Clickable
moveUpBtn widget.Clickable
moveDownBtn widget.Clickable
changeIconBtn widget.Clickable
bnetInfoBtn widget.Clickable
cancelBtn widget.Clickable
}
func newRowActionMenu(idx, total int) *rowActionMenu {
func newRowActionMenu(idx, total int, vendor string) *rowActionMenu {
return &rowActionMenu{
idx: idx,
canMoveUp: idx > 0,
canMoveDown: idx < total-1,
isBnet: vendor == "battlenet",
}
}
@@ -107,6 +111,7 @@ func (m *rowActionMenu) Layout(gtx layout.Context, th *material.Theme) layout.Di
row(&m.renameBtn, i18n.T("action_rename"), true),
row(&m.deleteBtn, i18n.T("action_delete"), true),
row(&m.changeIconBtn, i18n.T("action_change_icon"), true),
row(&m.bnetInfoBtn, i18n.T("action_bnet_info"), m.isBnet),
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),