From 5543702ae6c82d6c2fe7ffce41c0f40097cf3c7a Mon Sep 17 00:00:00 2001 From: Justin Do Date: Thu, 13 May 2021 01:35:28 +0700 Subject: [PATCH] Update ui restore page (#386) * Create the base layout for the restore page - add a custom card and restore button layout. * Add custom editor for inputting seed - add dropdown menu to select a seed from a list of seed suggestions - add wallet restore functionality to the updated UI --- ui/create_restore_page.go | 586 +++++++++++++++++++++++------------- ui/decredmaterial/editor.go | 49 +++ ui/decredmaterial/line.go | 18 +- ui/modal_templates.go | 14 +- ui/util.go | 8 + 5 files changed, 444 insertions(+), 231 deletions(-) diff --git a/ui/create_restore_page.go b/ui/create_restore_page.go index b2dafde81..057af5b26 100644 --- a/ui/create_restore_page.go +++ b/ui/create_restore_page.go @@ -7,8 +7,11 @@ import ( "gioui.org/io/key" "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/paint" "gioui.org/text" "gioui.org/widget" + "gioui.org/widget/material" "github.com/planetdecred/dcrlibwallet" "github.com/planetdecred/godcr/ui/decredmaterial" @@ -22,10 +25,15 @@ const PageCreateRestore = "CreateRestore" type ( seedEditors struct { focusIndex int - editors []decredmaterial.Editor + editors []decredmaterial.RestoreEditor } ) +type seedItemMenu struct { + text string + button *widget.Clickable +} + type createRestore struct { theme *decredmaterial.Theme info *wallet.MultiWalletInfo @@ -35,24 +43,22 @@ type createRestore struct { showRestore bool restoring bool showPassword bool - showWarning bool seedPhrase string suggestionLimit int suggestions []string allSuggestions []string seedClicked bool - lastOffsetRight int - lastOffsetLeft int focused []int + seedMenu []seedItemMenu + openPopupIndex int closeCreateRestore decredmaterial.IconButton hideRestoreWallet decredmaterial.IconButton create decredmaterial.Button unlock decredmaterial.Button - showPasswordModal decredmaterial.Button + restoreWalletBtn decredmaterial.Button hidePasswordModal decredmaterial.Button showRestoreWallet decredmaterial.Button - showResetModal decredmaterial.Button resetSeedFields decredmaterial.Button hideResetModal decredmaterial.Button @@ -61,18 +67,20 @@ type createRestore struct { matchSpendingPassword decredmaterial.Editor addWallet decredmaterial.Button errLabel decredmaterial.Label + optionsMenuCard decredmaterial.Card - seedEditors seedEditors + passwordStrength decredmaterial.ProgressBarStyle - seedListLeft *layout.List - seedListRight *layout.List - autoCompleteList *layout.List + seedEditors seedEditors - seedSuggestions []decredmaterial.Button + seedList *layout.List + restoreContainer layout.List createModal *decredmaterial.Modal warningModal *decredmaterial.Modal modalTitleLabel decredmaterial.Label + + alertIcon *widget.Image } // Loading lays out the loading widget with a faded background @@ -85,22 +93,31 @@ func (win *Window) CreateRestorePage(common pageCommon) layout.Widget { errorReceiver: make(chan error), errLabel: common.theme.Body1(""), - spendingPassword: common.theme.Editor(new(widget.Editor), "Enter password"), - walletName: common.theme.Editor(new(widget.Editor), "Enter wallet name"), - matchSpendingPassword: common.theme.Editor(new(widget.Editor), "Enter password again"), + spendingPassword: common.theme.EditorPassword(new(widget.Editor), "Spending password"), + walletName: common.theme.Editor(new(widget.Editor), "Wallet name (optional)"), + matchSpendingPassword: common.theme.EditorPassword(new(widget.Editor), "Confirm spending password"), addWallet: common.theme.Button(new(widget.Clickable), "create wallet"), hideResetModal: common.theme.Button(new(widget.Clickable), "cancel"), suggestionLimit: 3, createModal: common.theme.Modal(), warningModal: common.theme.Modal(), modalTitleLabel: common.theme.H6(""), + passwordStrength: win.theme.ProgressBar(0), + openPopupIndex: -1, + restoreContainer: layout.List{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }, } + pg.optionsMenuCard = decredmaterial.Card{Color: pg.theme.Color.Surface} + pg.optionsMenuCard.Radius = decredmaterial.CornerRadius{NE: 5, NW: 5, SE: 5, SW: 5} + pg.create = common.theme.Button(new(widget.Clickable), "create wallet") pg.unlock = common.theme.Button(new(widget.Clickable), "unlock wallet") pg.unlock.Background = common.theme.Color.Success - pg.showPasswordModal = common.theme.Button(new(widget.Clickable), "proceed") + pg.restoreWalletBtn = common.theme.Button(new(widget.Clickable), "Restore") pg.showRestoreWallet = common.theme.Button(new(widget.Clickable), "Restore an existing wallet") pg.showRestoreWallet.Background = color.NRGBA{} pg.showRestoreWallet.Color = common.theme.Color.Hint @@ -109,7 +126,7 @@ func (win *Window) CreateRestorePage(common pageCommon) layout.Widget { pg.closeCreateRestore.Background = color.NRGBA{} pg.closeCreateRestore.Color = common.theme.Color.Hint - pg.hideRestoreWallet = common.theme.IconButton(new(widget.Clickable), mustIcon(widget.NewIcon(icons.NavigationArrowBack))) + pg.hideRestoreWallet = common.theme.IconButton(new(widget.Clickable), mustIcon(widget.NewIcon(icons.NavigationClose))) pg.hideRestoreWallet.Background = color.NRGBA{} pg.hideRestoreWallet.Color = common.theme.Color.Hint @@ -117,38 +134,40 @@ func (win *Window) CreateRestorePage(common pageCommon) layout.Widget { pg.hidePasswordModal.Color = common.theme.Color.Danger pg.hidePasswordModal.Background = color.NRGBA{R: 238, G: 238, B: 238, A: 255} - pg.showResetModal = common.theme.Button(new(widget.Clickable), "reset") - pg.showResetModal.Color = common.theme.Color.Hint - pg.showResetModal.Background = color.NRGBA{} + pg.resetSeedFields = common.theme.Button(new(widget.Clickable), "Clear all") + pg.resetSeedFields.Color = common.theme.Color.Hint + pg.resetSeedFields.Background = color.NRGBA{} - pg.resetSeedFields = common.theme.Button(new(widget.Clickable), "yes, reset") - pg.resetSeedFields.Color = common.theme.Color.Danger - pg.resetSeedFields.Background = color.NRGBA{R: 238, G: 238, B: 238, A: 255} + pg.alertIcon = common.icons.alertGray + pg.alertIcon.Scale = 1.0 + pg.restoreWalletBtn.Inset = layout.Inset{ + Top: values.MarginPadding12, + Bottom: values.MarginPadding12, + Right: values.MarginPadding50, + Left: values.MarginPadding50, + } + pg.restoreWalletBtn.Background = common.theme.Color.InactiveGray + pg.restoreWalletBtn.TextSize = values.TextSize16 pg.errLabel.Color = pg.theme.Color.Danger + pg.passwordStrength.Color = pg.theme.Color.LightGray + for i := 0; i <= 32; i++ { - // pg.seedEditors = append(pg.seedEditors, common.theme.Editor(new(widget.Editor), fmt.Sprintf("%d", i+1))) widgetEditor := new(widget.Editor) widgetEditor.SingleLine, widgetEditor.Submit = true, true - pg.seedEditors.editors = append(pg.seedEditors.editors, win.theme.Editor(widgetEditor, fmt.Sprintf("%d", i+1))) + pg.seedEditors.editors = append(pg.seedEditors.editors, win.theme.RestoreEditor(widgetEditor, "", fmt.Sprintf("%d", i+1))) } pg.seedEditors.focusIndex = -1 // init suggestion buttons - for i := 0; i < pg.suggestionLimit; i++ { - pg.seedSuggestions = append(pg.seedSuggestions, win.theme.Button(new(widget.Clickable), "")) - } + pg.initSeedMenu() - pg.seedListLeft, pg.seedListRight = &layout.List{Axis: layout.Vertical}, &layout.List{Axis: layout.Vertical} + pg.seedList = &layout.List{Axis: layout.Vertical} pg.spendingPassword.Editor.SingleLine, pg.matchSpendingPassword.Editor.SingleLine = true, true pg.walletName.Editor.SingleLine = true - pg.autoCompleteList = &layout.List{Axis: layout.Horizontal} - pg.allSuggestions = dcrlibwallet.PGPWordList() - pg.lastOffsetRight = pg.seedListRight.Position.Offset - pg.lastOffsetLeft = pg.seedListLeft.Position.Offset return func(gtx C) D { pg.handle(common) @@ -157,23 +176,25 @@ func (win *Window) CreateRestorePage(common pageCommon) layout.Widget { } func (pg *createRestore) layout(gtx layout.Context, common pageCommon) layout.Dimensions { + if pg.info.LoadedWallets > 0 { + pg.restoring = true + pg.showRestore = true + } return common.Layout(gtx, func(gtx C) D { pd := values.MarginPadding15 dims := layout.Flex{Axis: layout.Vertical, Spacing: layout.SpaceBetween}.Layout(gtx, - layout.Flexed(1, func(gtx C) D { - return layout.Inset{Top: pd, Left: pd, Right: pd}.Layout(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Flexed(1, func(gtx C) D { - if common.states.creating { - return pg.processing(gtx) - } else if pg.showRestore { - return pg.restore(gtx) - } else { - return pg.mainContent(gtx) - } - }), - ) - }) + layout.Rigid(func(gtx C) D { + if common.states.creating { + return layout.Inset{Top: pd, Left: pd, Right: pd}.Layout(gtx, func(gtx C) D { + return pg.processing(gtx) + }) + } else if pg.showRestore { + return pg.restore(gtx) + } else { + return layout.Inset{Top: pd, Left: pd, Right: pd}.Layout(gtx, func(gtx C) D { + return pg.mainContent(gtx) + }) + } }), layout.Rigid(func(gtx C) D { if pg.showPassword { @@ -229,44 +250,8 @@ func (pg *createRestore) layout(gtx layout.Context, common pageCommon) layout.Di } return layout.Dimensions{} }), - layout.Rigid(func(gtx C) D { - if pg.showWarning { - // pg.warningModal.SetTitle("Reset Seed Input") - var msg = "You are about clearing all the seed input fields. Are you sure you want to proceed with this action?" - w := []func(gtx C) D{ - func(gtx C) D { - txt := common.theme.H6(msg) - txt.Color = common.theme.Color.Danger - txt.Alignment = text.Middle - return txt.Layout(gtx) - }, - func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.UniformInset(values.MarginPadding5).Layout(gtx, func(gtx C) D { - return pg.resetSeedFields.Layout(gtx) - }) - }), - layout.Rigid(func(gtx C) D { - return layout.UniformInset(values.MarginPadding5).Layout(gtx, func(gtx C) D { - pg.hidePasswordModal.Background = common.theme.Color.Primary - pg.hidePasswordModal.Color = color.NRGBA{R: 255, G: 255, B: 255, A: 255} - return pg.hidePasswordModal.Layout(gtx) - }) - }), - ) - }) - }, - } - return pg.warningModal.Layout(gtx, w, 1300) - } - return layout.Dimensions{} - }), ) - return common.UniformPadding(gtx, func(gtx C) D { - return dims - }) + return dims }) } @@ -324,63 +309,220 @@ func (pg *createRestore) mainContent(gtx layout.Context) layout.Dimensions { } func (pg *createRestore) restore(gtx layout.Context) layout.Dimensions { - dims := layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.W.Layout(gtx, func(gtx C) D { - return pg.hideRestoreWallet.Layout(gtx) + op.TransformOp{}.Add(gtx.Ops) + paint.Fill(gtx.Ops, pg.theme.Color.LightGray) + dims := layout.Stack{Alignment: layout.S}.Layout(gtx, + layout.Expanded(func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Inset{Left: values.MarginPadding20}.Layout(gtx, func(gtx C) D { + return layout.W.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Inset{Top: values.MarginPadding6}.Layout(gtx, func(gtx C) D { + return pg.hideRestoreWallet.Layout(gtx) + }) + }), + layout.Rigid(func(gtx C) D { + return layout.Inset{Top: values.MarginPadding16}.Layout(gtx, func(gtx C) D { + return pg.theme.H6("Restore wallet").Layout(gtx) + }) + }), + ) + }) + }) + }), + layout.Rigid(func(gtx C) D { + m := values.MarginPadding24 + v := values.MarginPadding6 + return Container{padding: layout.Inset{Right: m, Left: m, Top: v, Bottom: m}}.Layout(gtx, func(gtx C) D { + pageContent := []func(gtx C) D{ + func(gtx C) D { + return pg.restorePageSections(gtx, "Enter your seed phase", "1/3", func(gtx C) D { + return pg.enterSeedPhase(gtx) + }) + }, + func(gtx C) D { + return pg.restorePageSections(gtx, "Create spending password", "2/3", func(gtx C) D { + return pg.createPasswordPhase(gtx) + }) + }, + func(gtx C) D { + return pg.restorePageSections(gtx, "Chose a wallet name", "3/3", func(gtx C) D { + return pg.renameWalletPhase(gtx) + }) + }, + } + return layout.Inset{Bottom: values.MarginPadding60}.Layout(gtx, func(gtx C) D { + return pg.restoreContainer.Layout(gtx, len(pageContent), func(gtx C, i int) D { + return layout.Inset{Bottom: values.MarginPadding4}.Layout(gtx, pageContent[i]) + }) + }) + }) + }), + ) + }), + layout.Stacked(func(gtx C) D { + gtx.Constraints.Min.Y = gtx.Constraints.Max.Y + return layout.S.Layout(gtx, func(gtx C) D { + return layout.Inset{Left: values.MarginPadding1}.Layout(gtx, func(gtx C) D { + return pg.restoreButtonSection(gtx) + }) }) }), + ) + return dims +} + +func (pg *createRestore) restoreButtonSection(gtx layout.Context) layout.Dimensions { + card := pg.theme.Card() + card.Radius = decredmaterial.CornerRadius{NE: 0, NW: 0, SE: 0, SW: 0} + return card.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.E.Layout(gtx, func(gtx C) D { + return layout.Inset{ + Top: values.MarginPadding16, + Bottom: values.MarginPadding16, + Right: values.MarginPadding16, + }.Layout(gtx, func(gtx C) D { + return pg.restoreWalletBtn.Layout(gtx) + }) + }) + }), + ) + }) +} + +func (pg *createRestore) enterSeedPhase(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - txt := pg.theme.H3("Restore from seed phrase") - txt.Alignment = text.Middle - return pg.centralize(gtx, func(gtx C) D { - return txt.Layout(gtx) - }) + inset := layout.Inset{ + Right: values.MarginPadding5, + } + return layout.Flex{}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return inset.Layout(gtx, func(gtx C) D { + return pg.inputsGroup(gtx, pg.seedList, 7, 0) + }) + }), + layout.Flexed(1, func(gtx C) D { + return inset.Layout(gtx, func(gtx C) D { + return pg.inputsGroup(gtx, pg.seedList, 7, 1) + }) + }), + layout.Flexed(1, func(gtx C) D { + return inset.Layout(gtx, func(gtx C) D { + return pg.inputsGroup(gtx, pg.seedList, 7, 2) + }) + }), + layout.Flexed(1, func(gtx C) D { + return inset.Layout(gtx, func(gtx C) D { + return pg.inputsGroup(gtx, pg.seedList, 6, 3) + }) + }), + layout.Flexed(1, func(gtx C) D { + return pg.inputsGroup(gtx, pg.seedList, 6, 4) + }), + ) }), layout.Rigid(func(gtx C) D { - txt := pg.theme.H6("Enter your seed phrase in the correct order") - txt.Alignment = text.Middle - return pg.centralize(gtx, func(gtx C) D { - return txt.Layout(gtx) - }) + return pg.errLabel.Layout(gtx) }), layout.Rigid(func(gtx C) D { - return layout.Inset{Top: values.MarginPadding10, Bottom: values.MarginPadding10}.Layout(gtx, func(gtx C) D { - return pg.centralize(gtx, func(gtx C) D { - return pg.errLabel.Layout(gtx) - }) - }) + return pg.resetSeedFields.Layout(gtx) }), - layout.Flexed(1, func(gtx C) D { - return layout.Center.Layout(gtx, func(gtx C) D { - return layout.Flex{}.Layout(gtx, - layout.Rigid(func(gtx C) D { - gtx.Constraints.Max.X = gtx.Constraints.Max.X / 2 - return pg.inputsGroup(gtx, pg.seedListLeft, 16, 0) - }), - layout.Rigid(func(gtx C) D { - return pg.inputsGroup(gtx, pg.seedListRight, 17, 16) - }), - ) + ) + +} + +func (pg *createRestore) createPasswordPhase(gtx layout.Context) layout.Dimensions { + phaseContents := []func(gtx C) D{ + func(gtx C) D { + card := pg.theme.Card() + card.Color = pg.theme.Color.LightGray + msg := "This spending password is required to sign transactions. Make sure to use a strong password and keep it safe." + return card.Layout(gtx, func(gtx C) D { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return layout.UniformInset(values.MarginPadding16).Layout(gtx, func(gtx C) D { + return layout.Flex{}.Layout(gtx, + layout.Rigid(func(gtx C) D { + inset := layout.Inset{ + Right: values.MarginPadding10, + Top: values.MarginPadding3, + } + return inset.Layout(gtx, func(gtx C) D { + return pg.alertIcon.Layout(gtx) + }) + }), + layout.Rigid(func(gtx C) D { + return pg.theme.Body1(msg).Layout(gtx) + }), + ) + }) }) - }), - layout.Rigid(func(gtx C) D { - return pg.centralize(gtx, func(gtx C) D { - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + }, + func(gtx C) D { + return pg.spendingPassword.Layout(gtx) + }, + func(gtx C) D { + return pg.passwordStrength.Layout(gtx) + }, + func(gtx C) D { + return pg.matchSpendingPassword.Layout(gtx) + }, + } + + return (&layout.List{Axis: layout.Vertical}).Layout(gtx, len(phaseContents), func(gtx C, i int) D { + return layout.UniformInset(values.MarginPadding5).Layout(gtx, phaseContents[i]) + }) +} + +func (pg *createRestore) renameWalletPhase(gtx layout.Context) layout.Dimensions { + return pg.walletName.Layout(gtx) +} + +func (pg *createRestore) restorePageSections(gtx layout.Context, title string, phaseProgress string, body layout.Widget) layout.Dimensions { + return layout.Inset{Bottom: values.MarginPadding10}.Layout(gtx, func(gtx C) D { + return pg.theme.Card().Layout(gtx, func(gtx C) D { + return layout.UniformInset(values.MarginPadding16).Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - return layout.Inset{Top: values.MarginPadding15, Bottom: values.MarginPadding15, - Right: values.MarginPadding10}.Layout(gtx, func(gtx C) D { - return pg.showPasswordModal.Layout(gtx) + m := values.MarginPadding10 + v := values.MarginPadding5 + return Container{padding: layout.Inset{Right: v, Left: v, Bottom: m}}.Layout(gtx, func(gtx C) D { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + txt := pg.theme.Body1(title) + return layout.Flex{ + Axis: layout.Horizontal, + Spacing: layout.SpaceBetween, + }.Layout(gtx, + layout.Rigid(func(gtx C) D { + return txt.Layout(gtx) + }), + layout.Rigid(func(gtx C) D { + border := widget.Border{ + Color: pg.theme.Color.Gray1, + CornerRadius: values.MarginPadding14, + Width: values.MarginPadding1, + } + phase := pg.theme.Body2(phaseProgress) + return border.Layout(gtx, func(gtx C) D { + m := values.MarginPadding8 + v := values.MarginPadding5 + return Container{padding: layout.Inset{Right: m, Left: m, Top: v, Bottom: v}}.Layout(gtx, func(gtx C) D { + return phase.Layout(gtx) + }) + }) + }), + ) }) }), - layout.Rigid(func(gtx C) D { - return pg.showResetModal.Layout(gtx) - }), + layout.Rigid(body), ) }) - }), - ) - return dims + }) + }) } func (pg *createRestore) processing(gtx layout.Context) layout.Dimensions { @@ -399,83 +541,37 @@ func (pg *createRestore) processing(gtx layout.Context) layout.Dimensions { })) } -func (pg *createRestore) inputsGroup(gtx layout.Context, l *layout.List, len int, startIndex int) layout.Dimensions { - return l.Layout(gtx, len, func(gtx C, i int) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Baseline}.Layout(gtx, +func (pg *createRestore) inputsGroup(gtx layout.Context, l *layout.List, len, startIndex int) layout.Dimensions { + return layout.Stack{Alignment: layout.N}.Layout(gtx, + layout.Expanded(func(gtx C) D { + return l.Layout(gtx, len, func(gtx C, i int) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - return layout.Inset{Left: values.MarginPadding20, Bottom: values.MarginPadding20}.Layout(gtx, func(gtx C) D { - return pg.seedEditors.editors[i+startIndex].Layout(gtx) + return layout.Inset{Bottom: values.MarginPadding5}.Layout(gtx, func(gtx C) D { + pg.layoutSeedMenu(gtx, i*5+startIndex) + return pg.seedEditors.editors[i*5+startIndex].Layout(gtx) }) }), ) - }), - layout.Rigid(func(gtx C) D { - return layout.Inset{Top: values.MarginPadding5, Left: values.MarginPadding20}.Layout(gtx, func(gtx C) D { - return pg.autoComplete(gtx, i, startIndex) - }) - }), - ) - }) -} - -func (pg *createRestore) autoComplete(gtx layout.Context, index, startIndex int) layout.Dimensions { - if index+startIndex != pg.seedEditors.focusIndex { - return layout.Dimensions{} - } - - return pg.autoCompleteList.Layout(gtx, len(pg.suggestions), func(gtx C, i int) D { - return layout.Inset{Right: values.MarginPadding5}.Layout(gtx, func(gtx C) D { - return pg.seedSuggestions[i].Layout(gtx) - }) - }) + }) + }), + ) } func (pg *createRestore) onSuggestionSeedsClicked() { index := pg.seedEditors.focusIndex - for _, b := range pg.seedSuggestions { - for b.Button.Clicked() { - pg.seedEditors.editors[index].Editor.SetText(b.Text) - pg.seedEditors.editors[index].Editor.MoveCaret(len(b.Text), 0) + for _, b := range pg.seedMenu { + for b.button.Clicked() { + pg.seedEditors.editors[index].Edit.Editor.SetText(b.text) + pg.seedEditors.editors[index].Edit.Editor.MoveCaret(len(b.text), 0) pg.seedClicked = true if index != 32 { - pg.seedEditors.editors[index+1].Editor.Focus() + pg.seedEditors.editors[index+1].Edit.Editor.Focus() } } } } -// scrollUp scrolls up the editor list to display seed suggestions if focused editor is the last -func (pg *createRestore) scrollUp() { - if !pg.seedListLeft.Position.BeforeEnd { - pg.seedListLeft.Position.Offset += 100 - pg.lastOffsetLeft += 100 - } - - if !pg.seedListRight.Position.BeforeEnd { - pg.seedListRight.Position.Offset += 100 - pg.lastOffsetRight += 100 - } -} - -func (pg *createRestore) hideSuggestionsOnScroll() { - leftOffset := pg.seedListLeft.Position.Offset - rightOffset := pg.seedListRight.Position.Offset - if leftOffset > pg.lastOffsetLeft || leftOffset < pg.lastOffsetLeft { - if pg.seedListLeft.Position.BeforeEnd { - pg.seedEditors.focusIndex = -1 - pg.lastOffsetLeft = leftOffset - } - } - if rightOffset > pg.lastOffsetRight || rightOffset < pg.lastOffsetRight { - if pg.seedListRight.Position.BeforeEnd { - pg.seedEditors.focusIndex = -1 - pg.lastOffsetRight = rightOffset - } - } -} - func diff(a, b []int) []int { temp := map[int]int{} for _, s := range a { @@ -500,14 +596,13 @@ func (pg *createRestore) editorSeedsEventsHandler() { for i := 0; i < len(pg.seedEditors.editors); i++ { editor := &pg.seedEditors.editors[i] - if editor.Editor.Focused() { + if editor.Edit.Editor.Focused() { focused = append(focused, i) } - for _, e := range editor.Editor.Events() { + for _, e := range editor.Edit.Editor.Events() { switch e.(type) { case widget.ChangeEvent: - pg.scrollUp() // hide suggestions if seed clicked if pg.seedClicked { pg.seedEditors.focusIndex = -1 @@ -515,14 +610,24 @@ func (pg *createRestore) editorSeedsEventsHandler() { } else { pg.seedEditors.focusIndex = i } + text := editor.Edit.Editor.Text() + if text == "" { + pg.openPopupIndex = -1 + } else { + pg.openPopupIndex = i + } - pg.suggestions = pg.suggestionSeeds(editor.Editor.Text()) + pg.resetSeedFields.Color = pg.theme.Color.Primary + pg.suggestions = pg.suggestionSeeds(text) for k, s := range pg.suggestions { - pg.seedSuggestions[k].Text = s + pg.seedMenu[k] = seedItemMenu{ + text: s, + button: new(widget.Clickable), + } } case widget.SubmitEvent: if i != 32 { - pg.seedEditors.editors[i+1].Editor.Focus() + pg.seedEditors.editors[i+1].Edit.Editor.Focus() } } } @@ -532,7 +637,43 @@ func (pg *createRestore) editorSeedsEventsHandler() { pg.seedEditors.focusIndex = -1 } pg.focused = focused - pg.hideSuggestionsOnScroll() +} + +func (pg *createRestore) initSeedMenu() { + for i := 0; i < pg.suggestionLimit; i++ { + pg.seedMenu = append(pg.seedMenu, seedItemMenu{ + text: "", + button: new(widget.Clickable), + }) + } +} + +func (pg *createRestore) layoutSeedMenu(gtx layout.Context, optionsSeedMenuIndex int) { + if pg.openPopupIndex != optionsSeedMenuIndex || pg.openPopupIndex != pg.seedEditors.focusIndex { + return + } + + inset := layout.Inset{ + Top: values.MarginPadding35, + Left: values.MarginPadding0, + } + + m := op.Record(gtx.Ops) + inset.Layout(gtx, func(gtx C) D { + border := widget.Border{Color: pg.theme.Color.LightGray, CornerRadius: values.MarginPadding5, Width: values.MarginPadding2} + return border.Layout(gtx, func(gtx C) D { + return pg.optionsMenuCard.Layout(gtx, func(gtx C) D { + return (&layout.List{Axis: layout.Vertical}).Layout(gtx, len(pg.seedMenu), func(gtx C, i int) D { + return material.Clickable(gtx, pg.seedMenu[i].button, func(gtx C) D { + return layout.UniformInset(values.MarginPadding10).Layout(gtx, func(gtx C) D { + return pg.theme.Body2(pg.seedMenu[i].text).Layout(gtx) + }) + }) + }) + }) + }) + }) + op.Defer(gtx.Ops, m.Stop()) } func (pg createRestore) suggestionSeeds(text string) []string { @@ -571,12 +712,16 @@ func (pg *createRestore) validatePasswords() string { match := pg.matchSpendingPassword.Editor.Text() if match == "" { pg.matchSpendingPassword.HintColor = pg.theme.Color.Danger - pg.errLabel.Text = "enter new wallet password again and it cannot be empty" + pg.errLabel.Text = "Enter new wallet password again and it cannot be empty" return "" } if match != pass { - pg.errLabel.Text = "new wallet passwords does not match" + pg.errLabel.Text = "Passwords does not match" + return "" + } + + if !pg.validateSeeds() { return "" } @@ -593,13 +738,13 @@ func (pg *createRestore) validateSeeds() bool { pg.errLabel.Text = "" for i, editor := range pg.seedEditors.editors { - if editor.Editor.Text() == "" { - pg.seedEditors.editors[i].HintColor = pg.theme.Color.Danger - pg.errLabel.Text = "all seed fields are required" + if editor.Edit.Editor.Text() == "" { + pg.seedEditors.editors[i].Edit.HintColor = pg.theme.Color.Danger + pg.errLabel.Text = "All seed fields are required" return false } - pg.seedPhrase += editor.Editor.Text() + " " + pg.seedPhrase += editor.Edit.Editor.Text() + " " } if !dcrlibwallet.VerifySeed(pg.seedPhrase) { @@ -612,7 +757,7 @@ func (pg *createRestore) validateSeeds() bool { func (pg *createRestore) resetSeeds() { for i := 0; i < len(pg.seedEditors.editors); i++ { - pg.seedEditors.editors[i].Editor.SetText("") + pg.seedEditors.editors[i].Edit.Editor.SetText("") } } @@ -631,9 +776,14 @@ func (pg *createRestore) centralize(gtx layout.Context, content layout.Widget) l func (pg *createRestore) handle(common pageCommon) { for pg.hideRestoreWallet.Button.Clicked() { - pg.showRestore = false - pg.restoring = false - pg.errLabel.Text = "" + if pg.info.LoadedWallets <= 0 { + pg.showRestore = false + pg.restoring = false + pg.errLabel.Text = "" + } else { + pg.resetSeeds() + common.changePage(PageWallet) + } } for pg.showRestoreWallet.Button.Clicked() { @@ -676,30 +826,31 @@ func (pg *createRestore) handle(common pageCommon) { }() } - for pg.showPasswordModal.Button.Clicked() { - if pg.showRestore { - if !pg.validateSeeds() { - return - } + if pg.restoreWalletBtn.Button.Clicked() { + pass := pg.validatePasswords() + if !pg.validateSeeds() || pass == "" { + return } - pg.showPassword = true + pg.wal.RestoreWallet(pg.seedPhrase, pass, pg.errorReceiver) + pg.resetSeeds() + common.states.creating = true + pg.resetPasswords() + pg.resetPage() } for pg.hidePasswordModal.Button.Clicked() { pg.showPassword = false - pg.showWarning = false pg.errLabel.Text = "" pg.resetPasswords() } - for pg.showResetModal.Button.Clicked() { - pg.showWarning = true + if pg.matchSpendingPassword.Editor.Len() > 0 && pg.spendingPassword.Editor.Len() > 0 { + pg.restoreWalletBtn.Background = pg.theme.Color.Primary } for pg.resetSeedFields.Button.Clicked() { pg.resetSeeds() pg.seedEditors.focusIndex = -1 - pg.showWarning = false } if pg.addWallet.Button.Clicked() { @@ -726,9 +877,9 @@ func (pg *createRestore) handle(common pageCommon) { if evt.Name == key.NameTab { if len(pg.suggestions) == 1 { focus := pg.seedEditors.focusIndex - pg.seedEditors.editors[focus].Editor.SetText(pg.suggestions[0]) + pg.seedEditors.editors[focus].Edit.Editor.SetText(pg.suggestions[0]) pg.seedClicked = true - pg.seedEditors.editors[focus].Editor.MoveCaret(len(pg.suggestions[0]), -1) + pg.seedEditors.editors[focus].Edit.Editor.MoveCaret(len(pg.suggestions[0]), -1) } } case err := <-pg.errorReceiver: @@ -741,6 +892,7 @@ func (pg *createRestore) handle(common pageCommon) { default: } + computePasswordStrength(&pg.passwordStrength, common.theme, pg.spendingPassword.Editor) pg.editorSeedsEventsHandler() pg.onSuggestionSeedsClicked() } diff --git a/ui/decredmaterial/editor.go b/ui/decredmaterial/editor.go index ea01e0ee0..8a3542170 100644 --- a/ui/decredmaterial/editor.go +++ b/ui/decredmaterial/editor.go @@ -15,6 +15,14 @@ import ( "golang.org/x/exp/shiny/materialdesign/icons" ) +type RestoreEditor struct { + t *Theme + Edit Editor + TitleLabel Label + LineColor color.NRGBA + height int +} + type Editor struct { t *Theme material.EditorStyle @@ -51,6 +59,19 @@ func (t *Theme) EditorPassword(editor *widget.Editor, hint string) Editor { return e } +func (t *Theme) RestoreEditor(editor *widget.Editor, hint string, title string) RestoreEditor { + + e := t.Editor(editor, hint) + e.Bordered = false + return RestoreEditor{ + t: t, + Edit: e, + TitleLabel: t.Body2(title), + LineColor: t.Color.Gray1, + height: 30, + } +} + func (t *Theme) Editor(editor *widget.Editor, hint string) Editor { errorLabel := t.Caption("") errorLabel.Color = t.Color.Danger @@ -246,6 +267,34 @@ func (e Editor) handleEvents() { } } +func (re RestoreEditor) Layout(gtx layout.Context) layout.Dimensions { + border := widget.Border{Color: re.LineColor, CornerRadius: values.MarginPadding8, Width: values.MarginPadding2} + return border.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Inset{ + Left: unit.Dp(10), + Right: unit.Dp(10), + }.Layout(gtx, func(gtx C) D { + return re.TitleLabel.Layout(gtx) + }) + }), + layout.Rigid(func(gtx C) D { + l := re.t.SeparatorVertical(re.height, 2) + l.Color = re.t.Color.Gray1 + return layout.Inset{}.Layout(gtx, func(gtx C) D { + return l.Layout(gtx) + }) + }), + layout.Rigid(func(gtx C) D { + edit := re.Edit.Layout(gtx) + re.height = edit.Size.Y + return edit + }), + ) + }) +} + func (e *Editor) SetRequiredErrorText(txt string) { e.requiredErrorText = txt } diff --git a/ui/decredmaterial/line.go b/ui/decredmaterial/line.go index 847d44354..f14986065 100644 --- a/ui/decredmaterial/line.go +++ b/ui/decredmaterial/line.go @@ -13,12 +13,20 @@ import ( type ( // Line represents a rectangle widget with an initial thickness of 1 Line struct { - Height int - Width int - Color color.NRGBA + Height int + Width int + Color color.NRGBA + isVertical bool } ) +// SeparatorVertical returns a vertical line widget instance +func (t *Theme) SeparatorVertical(height, width int) Line { + vLine := t.Line(height, width) + vLine.isVertical = true + return vLine +} + // Line returns a line widget instance func (t *Theme) Line(height, width int) Line { if height == 0 { @@ -47,6 +55,10 @@ func (l Line) Layout(gtx C) D { l.Width = gtx.Constraints.Max.X } + if l.isVertical && l.Height == 0 { + l.Height = gtx.Constraints.Max.Y + } + line := image.Rectangle{ Max: image.Point{ X: l.Width, diff --git a/ui/modal_templates.go b/ui/modal_templates.go index f57dc9883..f284f5438 100644 --- a/ui/modal_templates.go +++ b/ui/modal_templates.go @@ -6,7 +6,6 @@ import ( "gioui.org/unit" "gioui.org/widget" - "github.com/planetdecred/dcrlibwallet" "github.com/planetdecred/godcr/ui/decredmaterial" "github.com/planetdecred/godcr/ui/values" "golang.org/x/exp/shiny/materialdesign/icons" @@ -454,7 +453,7 @@ func (m *ModalTemplate) handle(th *decredmaterial.Theme, load *modalLoad) (templ load.cancel.(func())() } - m.computePasswordStrength(th, m.spendingPassword.Editor) + computePasswordStrength(&m.passwordStrength, th, m.spendingPassword.Editor) template = m.createNewWallet() m.walletName.Hint = "Wallet name" @@ -522,7 +521,7 @@ func (m *ModalTemplate) handle(th *decredmaterial.Theme, load *modalLoad) (templ load.cancel.(func())() } - m.computePasswordStrength(th, m.spendingPassword.Editor) + computePasswordStrength(&m.passwordStrength, th, m.spendingPassword.Editor) m.spendingPassword.Hint = "New spending password" m.matchSpendingPassword.Hint = "Confirm new spending password" @@ -583,7 +582,7 @@ func (m *ModalTemplate) handle(th *decredmaterial.Theme, load *modalLoad) (templ load.cancel.(func())() } - m.computePasswordStrength(th, m.spendingPassword.Editor) + computePasswordStrength(&m.passwordStrength, th, m.spendingPassword.Editor) m.spendingPassword.Hint = "Startup password" m.matchSpendingPassword.Hint = "Confirm startup password" @@ -669,13 +668,6 @@ func (m *ModalTemplate) passwordsMatch(editors ...*widget.Editor) bool { return true } -func (m *ModalTemplate) computePasswordStrength(th *decredmaterial.Theme, editors ...*widget.Editor) { - password := editors[0] - strength := dcrlibwallet.ShannonEntropy(password.Text()) / 4.0 - m.passwordStrength.Progress = float32(strength * 100) - m.passwordStrength.Color = th.Color.Success -} - // resetFields clears all modal fields when the modal is closed func (m *ModalTemplate) resetFields() { m.matchSpendingPassword.Editor.SetText("") diff --git a/ui/util.go b/ui/util.go index 1ea38b4a4..22be90b85 100644 --- a/ui/util.go +++ b/ui/util.go @@ -14,6 +14,7 @@ import ( "gioui.org/gesture" "gioui.org/widget" "github.com/planetdecred/dcrlibwallet" + "github.com/planetdecred/godcr/ui/decredmaterial" "github.com/planetdecred/godcr/wallet" "golang.org/x/text/message" ) @@ -118,3 +119,10 @@ func goToURL(url string) { log.Error(err) } } + +func computePasswordStrength(pb *decredmaterial.ProgressBarStyle, th *decredmaterial.Theme, editors ...*widget.Editor) { + password := editors[0] + strength := dcrlibwallet.ShannonEntropy(password.Text()) / 4.0 + pb.Progress = float32(strength * 100) + pb.Color = th.Color.Success +}