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
+13
View File
@@ -580,3 +580,16 @@ other = "Beim Windows-Start starten"
[hint_auto_start] [hint_auto_start]
other = "Trägt die Anwendung in den Windows-Autostart-Registrierungsschlüssel ein." other = "Trägt die Anwendung in den Windows-Autostart-Registrierungsschlüssel ein."
[action_bnet_info]
other = "Seriennummer & Wiederherstellungscode..."
[dialog_bnet_info_title]
other = "Battle.Net-Seriennummer & Wiederherstellungscode"
[bnet_info_intro]
other = "Bewahre den Wiederherstellungscode geheim — jeder, der sowohl Seriennummer als auch Wiederherstellungscode besitzt, kann den vollen Kontozugriff wiederherstellen."
[hint_bnet_restore_code_secret]
other = "Teile den Wiederherstellungscode niemals. Er ist die einzige Möglichkeit, diesen Authentifikator wiederherzustellen."
+13
View File
@@ -582,3 +582,16 @@ other = "Launch at Windows startup"
[hint_auto_start] [hint_auto_start]
other = "Adds the application to the Windows startup registry key." other = "Adds the application to the Windows startup registry key."
[action_bnet_info]
other = "Serial & restore code..."
[dialog_bnet_info_title]
other = "Battle.Net serial & restore code"
[bnet_info_intro]
other = "Keep the restore code secret — anyone who has both the serial and the restore code can recover full account access."
[hint_bnet_restore_code_secret]
other = "Never share the restore code. It is the only way to recover this authenticator."
+13
View File
@@ -580,3 +580,16 @@ other = "开机自启动"
[hint_auto_start] [hint_auto_start]
other = "将程序添加到 Windows 启动注册表项。" other = "将程序添加到 Windows 启动注册表项。"
[action_bnet_info]
other = "序列号与恢复码..."
[dialog_bnet_info_title]
other = "战网序列号与恢复码"
[bnet_info_intro]
other = "请妥善保管恢复码 —— 任何同时拥有序列号和恢复码的人都能恢复完整的账号访问权限。"
[hint_bnet_restore_code_secret]
other = "切勿泄露恢复码。它是恢复此验证器的唯一途径。"
+16 -1
View File
@@ -127,6 +127,7 @@ type appState struct {
renameDlg *renameDialog renameDlg *renameDialog
confirmDelDlg *confirmDeleteDialog confirmDelDlg *confirmDeleteDialog
iconPickerDlg *iconPickerDialog iconPickerDlg *iconPickerDialog
bnetInfoDlg *bnetInfoDialog
rowTargetIdx int rowTargetIdx int
store *store store *store
@@ -497,7 +498,13 @@ func drawFrame(gtx layout.Context, th *material.Theme, st *appState, w *app.Wind
total := len(st.entries) total := len(st.entries)
st.mu.Unlock() st.mu.Unlock()
st.rowTargetIdx = moreTargetIdx 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 // 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.mu.Unlock()
st.iconPickerDlg = newIconPickerDialog(current) 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() w.Invalidate()
} else { } 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 rowActionMoveUp
rowActionMoveDown rowActionMoveDown
rowActionChangeIcon rowActionChangeIcon
rowActionBnetInfo
) )
// rowActionMenu is the small popup opened by the ⋯ button on each entry // rowActionMenu is the small popup opened by the ⋯ button on each entry
@@ -29,20 +30,23 @@ type rowActionMenu struct {
idx int idx int
canMoveUp bool canMoveUp bool
canMoveDown bool canMoveDown bool
isBnet bool
renameBtn widget.Clickable renameBtn widget.Clickable
deleteBtn widget.Clickable deleteBtn widget.Clickable
moveUpBtn widget.Clickable moveUpBtn widget.Clickable
moveDownBtn widget.Clickable moveDownBtn widget.Clickable
changeIconBtn widget.Clickable changeIconBtn widget.Clickable
bnetInfoBtn widget.Clickable
cancelBtn widget.Clickable cancelBtn widget.Clickable
} }
func newRowActionMenu(idx, total int) *rowActionMenu { func newRowActionMenu(idx, total int, vendor string) *rowActionMenu {
return &rowActionMenu{ return &rowActionMenu{
idx: idx, idx: idx,
canMoveUp: idx > 0, canMoveUp: idx > 0,
canMoveDown: idx < total-1, 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.renameBtn, i18n.T("action_rename"), true),
row(&m.deleteBtn, i18n.T("action_delete"), true), row(&m.deleteBtn, i18n.T("action_delete"), true),
row(&m.changeIconBtn, i18n.T("action_change_icon"), 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.moveUpBtn, i18n.T("action_move_up"), m.canMoveUp),
row(&m.moveDownBtn, i18n.T("action_move_down"), m.canMoveDown), row(&m.moveDownBtn, i18n.T("action_move_down"), m.canMoveDown),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),