diff --git a/internal/i18n/locales/de.toml b/internal/i18n/locales/de.toml index 542d782..933868d 100644 --- a/internal/i18n/locales/de.toml +++ b/internal/i18n/locales/de.toml @@ -580,3 +580,16 @@ other = "Beim Windows-Start starten" [hint_auto_start] 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." + diff --git a/internal/i18n/locales/en.toml b/internal/i18n/locales/en.toml index edc0696..fea37cc 100644 --- a/internal/i18n/locales/en.toml +++ b/internal/i18n/locales/en.toml @@ -582,3 +582,16 @@ other = "Launch at Windows startup" [hint_auto_start] 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." + diff --git a/internal/i18n/locales/zh-CN.toml b/internal/i18n/locales/zh-CN.toml index e3fd0a7..e902802 100644 --- a/internal/i18n/locales/zh-CN.toml +++ b/internal/i18n/locales/zh-CN.toml @@ -580,3 +580,16 @@ other = "开机自启动" [hint_auto_start] other = "将程序添加到 Windows 启动注册表项。" + +[action_bnet_info] +other = "序列号与恢复码..." + +[dialog_bnet_info_title] +other = "战网序列号与恢复码" + +[bnet_info_intro] +other = "请妥善保管恢复码 —— 任何同时拥有序列号和恢复码的人都能恢复完整的账号访问权限。" + +[hint_bnet_restore_code_secret] +other = "切勿泄露恢复码。它是恢复此验证器的唯一途径。" + diff --git a/internal/ui/app.go b/internal/ui/app.go index b54e1d7..5f1531d 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -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 { diff --git a/internal/ui/dialog_bnet_info.go b/internal/ui/dialog_bnet_info.go new file mode 100644 index 0000000..ba3e95a --- /dev/null +++ b/internal/ui/dialog_bnet_info.go @@ -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) +} diff --git a/internal/ui/row_menu.go b/internal/ui/row_menu.go index 0ffeeba..8677558 100644 --- a/internal/ui/row_menu.go +++ b/internal/ui/row_menu.go @@ -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),