Add advanced synchronize dialog and APK tooling
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user