Add advanced synchronize dialog and APK tooling

This commit is contained in:
Joe Julian
2026-03-29 21:38:46 -07:00
parent 6e2760c514
commit 96ec583c7e
11 changed files with 1154 additions and 80 deletions
+402 -77
View File
@@ -88,10 +88,10 @@ type recentVaultRecord struct {
}
type recentRemoteRecord struct {
BaseURL string `json:"baseUrl"`
Path string `json:"path"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
BaseURL string `json:"baseUrl"`
Path string `json:"path"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
LastGroup []string `json:"lastGroup,omitempty"`
}
@@ -99,6 +99,20 @@ type uiPreferences struct {
GroupControlsHidden bool `json:"groupControlsHidden"`
}
type syncSourceMode string
const (
syncSourceLocal syncSourceMode = "local"
syncSourceRemote syncSourceMode = "remote"
)
type syncDirection string
const (
syncDirectionPull syncDirection = "pull"
syncDirectionPush syncDirection = "push"
)
type ui struct {
mode string
theme *material.Theme
@@ -143,10 +157,15 @@ type ui struct {
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable
closeAdvancedSync widget.Clickable
runAdvancedSync widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
@@ -169,11 +188,16 @@ type ui struct {
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable
showSyncRemote widget.Clickable
showSyncPull widget.Clickable
showSyncPush widget.Clickable
rememberRemoteAuth widget.Bool
entryClicks []widget.Clickable
historyClicks []widget.Clickable
@@ -200,9 +224,20 @@ type ui struct {
copyIcon *widget.Icon
expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon
chevronDownIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
@@ -268,6 +303,11 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
remotePath: widget.Editor{SingleLine: true, Submit: false},
remoteUsername: widget.Editor{SingleLine: true, Submit: false},
remotePassword: widget.Editor{SingleLine: true, Submit: false, Mask: '•'},
syncLocalPath: widget.Editor{SingleLine: true, Submit: false},
syncRemoteBaseURL: widget.Editor{SingleLine: true, Submit: false},
syncRemotePath: widget.Editor{SingleLine: true, Submit: false},
syncRemoteUsername: widget.Editor{SingleLine: true, Submit: false},
syncRemotePassword: widget.Editor{SingleLine: true, Submit: false, Mask: '•'},
masterPassword: widget.Editor{SingleLine: true, Submit: false},
keyFilePath: widget.Editor{SingleLine: true, Submit: false},
entryID: widget.Editor{SingleLine: true, Submit: false},
@@ -303,6 +343,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
recentRemotesPath: paths.RecentRemotesPath,
recentVaultGroups: map[string][]string{},
now: time.Now,
syncSourceMode: syncSourceLocal,
syncDirection: syncDirectionPull,
}
u.state.Session = sess
u.phoneSplit.Value = 0.46
@@ -311,6 +353,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy)
u.expandMoreIcon, _ = widget.NewIcon(icons.NavigationExpandMore)
u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess)
u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown)
u.passwordProfile.SetText("strong")
u.keyboardFocus = focusSearch
u.setCustomFieldRows(nil)
@@ -648,6 +691,77 @@ func (u *ui) synchronizeAction() error {
if err := u.state.Synchronize(); err != nil {
return err
}
u.syncMenuOpen = false
u.filter()
return nil
}
func (u *ui) openAdvancedSyncDialog() {
u.syncDialogOpen = true
u.syncMenuOpen = false
u.showSyncPassword = false
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
}
}
func (u *ui) advancedSyncAction() error {
switch u.syncDirection {
case syncDirectionPush:
return u.advancedSyncToAction()
default:
return u.advancedSyncFromAction()
}
}
func (u *ui) advancedSyncFromAction() error {
switch u.syncSourceMode {
case syncSourceRemote:
client := webdav.Client{
BaseURL: strings.TrimSpace(u.syncRemoteBaseURL.Text()),
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
Password: u.syncRemotePassword.Text(),
}
if err := u.state.SynchronizeFromRemote(client, strings.TrimSpace(u.syncRemotePath.Text())); err != nil {
return err
}
default:
path := strings.TrimSpace(u.syncLocalPath.Text())
if path == "" {
return errors.New(errVaultPathRequired)
}
if err := u.state.SynchronizeFromLocal(path); err != nil {
return err
}
}
u.syncDialogOpen = false
u.showSyncPassword = false
u.filter()
return nil
}
func (u *ui) advancedSyncToAction() error {
switch u.syncSourceMode {
case syncSourceRemote:
client := webdav.Client{
BaseURL: strings.TrimSpace(u.syncRemoteBaseURL.Text()),
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
Password: u.syncRemotePassword.Text(),
}
if err := u.state.SynchronizeToRemote(client, strings.TrimSpace(u.syncRemotePath.Text())); err != nil {
return err
}
default:
path := strings.TrimSpace(u.syncLocalPath.Text())
if path == "" {
return errors.New(errVaultPathRequired)
}
if err := u.state.SynchronizeToLocal(path); err != nil {
return err
}
}
u.syncDialogOpen = false
u.showSyncPassword = false
u.filter()
return nil
}
@@ -1320,6 +1434,19 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.synchronizeVault.Clicked(gtx) {
u.runAction("synchronize vault", u.synchronizeAction)
}
for u.toggleSyncMenu.Clicked(gtx) {
u.syncMenuOpen = !u.syncMenuOpen
}
for u.openAdvancedSync.Clicked(gtx) {
u.openAdvancedSyncDialog()
}
for u.closeAdvancedSync.Clicked(gtx) {
u.syncDialogOpen = false
u.showSyncPassword = false
}
for u.runAdvancedSync.Clicked(gtx) {
u.runAction("advanced synchronize vault", u.advancedSyncAction)
}
for u.unlockVault.Clicked(gtx) {
u.runAction("unlock vault", u.unlockAction)
}
@@ -1341,6 +1468,18 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.showRemoteLifecycle.Clicked(gtx) {
u.lifecycleMode = "remote"
}
for u.showSyncLocal.Clicked(gtx) {
u.syncSourceMode = syncSourceLocal
}
for u.showSyncRemote.Clicked(gtx) {
u.syncSourceMode = syncSourceRemote
}
for u.showSyncPull.Clicked(gtx) {
u.syncDirection = syncDirectionPull
}
for u.showSyncPush.Clicked(gtx) {
u.syncDirection = syncDirectionPush
}
for u.lockVault.Clicked(gtx) {
u.runAction("lock vault", u.lockAction)
}
@@ -1358,6 +1497,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.pickKeyFile.Clicked(gtx) {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
}
for u.pickSyncLocalPath.Clicked(gtx) {
u.runAction("choose sync path", func() error { return u.chooseExistingFileAction(&u.syncLocalPath) })
}
for i := range u.recentVaultClicks {
for u.recentVaultClicks[i].Clicked(gtx) {
if i < len(u.recentVaults) {
@@ -1462,66 +1604,84 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.togglePasswordInline.Clicked(gtx) {
u.showPassword = !u.showPassword
}
for u.toggleSyncPassword.Clicked(gtx) {
u.showSyncPassword = !u.showSyncPassword
if u.showSyncPassword {
u.syncRemotePassword.Mask = 0
} else {
u.syncRemotePassword.Mask = '•'
}
}
if _, changed := u.search.Update(gtx); changed {
u.filter()
}
inset := layout.UniformInset(unit.Dp(16))
return layout.Background{}.Layout(gtx, fill(bgColor), func(gtx layout.Context) layout.Dimensions {
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(u.header),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.bannerSurface().Kind == bannerNone {
return layout.Dimensions{}
}
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx, fill(bgColor), func(gtx layout.Context) layout.Dimensions {
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(u.banner),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if u.shouldShowLifecycleSetup() {
return u.lifecycleScreen(gtx)
}
if u.shouldUseLockedSinglePane() {
return u.detailPanel(gtx)
}
if u.mode == "phone" || gtx.Constraints.Max.X < gtx.Dp(unit.Dp(720)) {
u.phoneSpan = gtx.Constraints.Max.Y
listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value)
if listHeight < gtx.Dp(unit.Dp(180)) {
listHeight = gtx.Dp(unit.Dp(180))
}
if listHeight > gtx.Constraints.Max.Y-gtx.Dp(unit.Dp(220)) {
listHeight = gtx.Constraints.Max.Y - gtx.Dp(unit.Dp(220))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.Y = listHeight
gtx.Constraints.Max.Y = listHeight
return u.listPanel(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(u.phoneSlider),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
func() layout.FlexChild {
if u.shouldUseCompactPhoneDetailPane() {
return layout.Rigid(u.detailPanel)
layout.Rigid(u.header),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.bannerSurface().Kind == bannerNone {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(u.banner),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if u.shouldShowLifecycleSetup() {
return u.lifecycleScreen(gtx)
}
if u.shouldUseLockedSinglePane() {
return u.detailPanel(gtx)
}
if u.mode == "phone" || gtx.Constraints.Max.X < gtx.Dp(unit.Dp(720)) {
u.phoneSpan = gtx.Constraints.Max.Y
listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value)
if listHeight < gtx.Dp(unit.Dp(180)) {
listHeight = gtx.Dp(unit.Dp(180))
}
return layout.Flexed(1, u.detailPanel)
}(),
)
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Flexed(0.38, u.listPanel),
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
layout.Flexed(0.62, u.detailPanel),
if listHeight > gtx.Constraints.Max.Y-gtx.Dp(unit.Dp(220)) {
listHeight = gtx.Constraints.Max.Y - gtx.Dp(unit.Dp(220))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.Y = listHeight
gtx.Constraints.Max.Y = listHeight
return u.listPanel(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(u.phoneSlider),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
func() layout.FlexChild {
if u.shouldUseCompactPhoneDetailPane() {
return layout.Rigid(u.detailPanel)
}
return layout.Flexed(1, u.detailPanel)
}(),
)
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Flexed(0.38, u.listPanel),
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
layout.Flexed(0.62, u.detailPanel),
)
}),
)
}),
)
})
})
})
})
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
if !u.syncDialogOpen {
return layout.Dimensions{}
}
return u.syncDialog(gtx)
}),
)
}
func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
@@ -1539,6 +1699,134 @@ func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
})
}
func (u *ui) syncDialog(gtx layout.Context) layout.Dimensions {
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, color.NRGBA{A: 90}, clip.Rect{Max: gtx.Constraints.Max}.Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
width := gtx.Dp(unit.Dp(620))
if width > gtx.Constraints.Max.X {
width = gtx.Constraints.Max.X - gtx.Dp(unit.Dp(24))
}
if width < 1 {
width = gtx.Constraints.Max.X
}
gtx.Constraints.Min.X = width
gtx.Constraints.Max.X = width
return card(gtx, u.syncDialogContent)
})
}),
)
}
func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
return material.List(u.theme, &u.lifecycleList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(20), "Synchronize...")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "Choose another source and whether to pull it into the current vault or push the current vault to it.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showSyncPull, "Pull From Source")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showSyncPush, "Push To Source")
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showSyncLocal, "Local File")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showSyncRemote, "Remote WebDAV")
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.syncSourceMode == syncSourceRemote {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "WebDAV base URL for the other source.", &u.syncRemoteBaseURL, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the other remote .kdbx file.", &u.syncRemotePath, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username for the other WebDAV source.", &u.syncRemoteUsername, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.syncPasswordField(gtx)
}),
)
}
return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.runAdvancedSync, "Synchronize")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.closeAdvancedSync, "Cancel")
}),
)
}),
)
})
}
func (u *ui) syncPasswordField(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "REMOTE PASSWORD")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
field := func(gtx layout.Context) layout.Dimensions {
editor := material.Editor(u.theme, &u.syncRemotePassword, "Remote Password")
editor.Color = u.theme.Palette.Fg
editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return outlinedFieldState(gtx, false, field)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.inlinePasswordToggle(gtx, &u.toggleSyncPassword, u.showSyncPassword)
}),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "Password or app token for the other WebDAV source.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
)
}
func (u *ui) header(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -1578,10 +1866,7 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{}
}
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
return btn.Layout(gtx)
}),
layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
@@ -1590,6 +1875,45 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
)
}
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
btn.CornerRadius = unit.Dp(10)
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.syncMenuToggle(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.syncMenuOpen {
return layout.Dimensions{}
}
return layout.Inset{Left: unit.Dp(6)}.Layout(gtx, u.syncMenu)
}),
)
}
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions")
btn.Background = accentColor
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
btn.Size = unit.Dp(18)
btn.Inset = layout.UniformInset(unit.Dp(8))
return btn.Layout(gtx)
}
func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Synchronize...")
}),
)
})
}
func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
panel := card
spacing := unit.Dp(12)
@@ -1848,10 +2172,7 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{}
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
return btn.Layout(gtx)
}),
layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
@@ -2288,12 +2609,6 @@ func detailLine(th *material.Theme, label, value string) layout.Widget {
func (u *ui) passwordLine(label, value string) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
icon := u.eyeIcon
desc := "Show password"
if u.showPassword {
icon = u.eyeOffIcon
desc = "Hide password"
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(label))
@@ -2307,12 +2622,7 @@ func (u *ui) passwordLine(label, value string) layout.Widget {
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(u.theme, &u.togglePasswordInline, icon, desc)
btn.Background = color.NRGBA{R: 239, G: 236, B: 229, A: 255}
btn.Color = accentColor
btn.Size = unit.Dp(18)
btn.Inset = layout.UniformInset(unit.Dp(8))
return btn.Layout(gtx)
return u.inlinePasswordToggle(gtx, &u.togglePasswordInline, u.showPassword)
}),
)
}),
@@ -2320,6 +2630,21 @@ func (u *ui) passwordLine(label, value string) layout.Widget {
}
}
func (u *ui) inlinePasswordToggle(gtx layout.Context, click *widget.Clickable, showing bool) layout.Dimensions {
icon := u.eyeIcon
desc := "Show password"
if showing {
icon = u.eyeOffIcon
desc = "Hide password"
}
btn := material.IconButton(u.theme, click, icon, desc)
btn.Background = color.NRGBA{R: 239, G: 236, B: 229, A: 255}
btn.Color = accentColor
btn.Size = unit.Dp(18)
btn.Inset = layout.UniformInset(unit.Dp(8))
return btn.Layout(gtx)
}
func (u *ui) detailPasswordValue() string {
item, ok := u.selectedEntry()
if !ok {