Files
winauth-go/internal/ui/dialog_restore_backup.go
T
iceking2nd fd4d756823 feat(ui): 导出/备份 — otpauth URI 列表 + 加密备份导出/恢复
- qr/export.go: EntryToOtpAuth + OtpAuth.URI,将 config.Entry 反向
  转换为标准 otpauth:// URI(Battle.Net/Steam 标记为 partial)
- dialog_export.go: Tab 切换对话框,otpauth URI 列表可复制 +
  加密备份文件导出(独立密码,0o600 权限)
- dialog_restore_backup.go: 从 .winauth.bak 恢复,密码验证后合并
- settings_menu: 新增 Export / Restore Backup 菜单项
- i18n: en/zh-CN/de 三语翻译
2026-06-12 10:44:51 +08:00

95 lines
2.6 KiB
Go

package ui
import (
"errors"
"fmt"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"git.wxccs.org/iceking2nd/winauth-go/internal/config"
"git.wxccs.org/iceking2nd/winauth-go/internal/global"
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
)
// restoreBackupDialog loads an encrypted .winauth.bak file produced by the
// export dialog. The user supplies the path and the backup's password; on
// success the loaded entries are handed back to the caller for merging.
type restoreBackupDialog struct {
pathEd widget.Editor
pwEd widget.Editor
restoreB widget.Clickable
cancelB widget.Clickable
errorMsg string
}
func newRestoreBackupDialog() *restoreBackupDialog {
d := &restoreBackupDialog{}
d.pathEd.SingleLine = true
d.pwEd.SingleLine = true
d.pwEd.Mask = '*'
return d
}
// restoreBackupResult carries the outcome of the dialog.
type restoreBackupResult struct {
cfg *config.Config
cancel bool
}
func (d *restoreBackupDialog) Layout(
gtx layout.Context, th *material.Theme,
onDone func(restoreBackupResult),
) layout.Dimensions {
if d.cancelB.Clicked(gtx) {
d.wipe()
onDone(restoreBackupResult{cancel: true})
return layout.Dimensions{Size: gtx.Constraints.Max}
}
if d.restoreB.Clicked(gtx) {
path := d.pathEd.Text()
if path == "" {
d.errorMsg = i18n.T("msg_empty_backup_path")
} else {
pw := []byte(d.pwEd.Text())
cfg, err := config.LoadYAML(path, pw)
for i := range pw {
pw[i] = 0
}
switch {
case errors.Is(err, config.ErrPasswordRequired):
d.errorMsg = i18n.T("msg_password_required")
case errors.Is(err, config.ErrPasswordWrong):
d.errorMsg = i18n.T("msg_password_wrong")
case err != nil:
d.errorMsg = fmt.Sprintf(i18n.T("msg_restore_failed"), err.Error())
global.Log.WithField("func", "internal.ui.restoreBackupDialog.Layout").
WithError(err).Warn("restore backup failed")
default:
d.wipe()
onDone(restoreBackupResult{cfg: cfg})
return layout.Dimensions{Size: gtx.Constraints.Max}
}
}
}
body := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body2(th, i18n.T("restore_backup_intro")).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(labeledEditor(th, i18n.T("label_backup_path"), &d.pathEd, "backup.winauth.bak")),
layout.Rigid(labeledEditor(th, i18n.T("label_password"), &d.pwEd, "")),
layout.Rigid(errorLabel(th, d.errorMsg)),
)
}
return modalCard(gtx, th, i18n.T("dialog_restore_backup_title"),
i18n.T("btn_restore"), i18n.T("btn_cancel"),
&d.restoreB, &d.cancelB, body)
}
func (d *restoreBackupDialog) wipe() {
d.pwEd.SetText("")
}