Compare commits
5 Commits
168927713c
...
74d10535a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d10535a1 | |||
| 16f603ccba | |||
| 9660369851 | |||
| 0a9201e0d1 | |||
| 74a2bbdc92 |
@@ -2740,98 +2740,61 @@ func (u *ui) shouldShowSavedRemoteBindingSelectors() bool {
|
||||
}
|
||||
|
||||
func (u *ui) savedRemoteBindingSummary() (profileLabel, credentialLabel, syncLabel string, ok bool) {
|
||||
profile, ok := u.selectedVaultRemoteProfile()
|
||||
if !ok {
|
||||
return "", "", "", false
|
||||
}
|
||||
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
||||
if !ok {
|
||||
return "", "", "", false
|
||||
}
|
||||
credentialLabel = entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
credentialLabel += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
syncLabel = "Sync manually when you choose Use Remote Sync."
|
||||
if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave {
|
||||
syncLabel = "Syncs automatically on open and save."
|
||||
}
|
||||
return profile.Name, credentialLabel, syncLabel, true
|
||||
summary := u.computeSavedRemoteBindingSummary()
|
||||
return summary.profileLabel, summary.credentialLabel, summary.syncLabel, summary.ok
|
||||
}
|
||||
|
||||
func (u *ui) savedRemoteBindingHeading() string {
|
||||
if !u.shouldShowSavedRemoteBindingSelectors() {
|
||||
return "Use this vault's saved remote sync target"
|
||||
}
|
||||
return "Use a saved remote profile from this vault"
|
||||
return u.buildSyncMenuModel().savedBindingHeading()
|
||||
}
|
||||
|
||||
func (u *ui) openSelectedVaultRemoteButtonLabel() string {
|
||||
if !u.shouldShowSavedRemoteBindingSelectors() {
|
||||
return "Use Remote Sync"
|
||||
}
|
||||
return "Open Saved Remote"
|
||||
return u.buildSyncMenuModel().openSelectedButtonLabel()
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowDirectRemoteSyncShortcut() bool {
|
||||
if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
|
||||
return false
|
||||
}
|
||||
_, ok := u.selectedVaultRemoteBinding()
|
||||
return ok
|
||||
return u.buildSyncMenuModel().showDirectRemoteSyncShortcut()
|
||||
}
|
||||
|
||||
func (u *ui) directRemoteSyncShortcutLabel() string {
|
||||
return "Use Remote Sync"
|
||||
return u.buildSyncMenuModel().directRemoteSyncShortcutLabel()
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowRemoteSyncSettingsShortcut() bool {
|
||||
if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
|
||||
return false
|
||||
}
|
||||
_, ok := u.selectedVaultRemoteBinding()
|
||||
return ok
|
||||
return u.buildSyncMenuModel().showRemoteSyncSettingsShortcut()
|
||||
}
|
||||
|
||||
func (u *ui) remoteSyncSettingsShortcutLabel() string {
|
||||
return "Remote Sync Settings"
|
||||
return u.buildSyncMenuModel().remoteSyncSettingsShortcutLabel()
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowRemoveRemoteSyncShortcut() bool {
|
||||
return u.shouldShowRemoteSyncSettingsShortcut()
|
||||
return u.buildSyncMenuModel().showRemoveRemoteSyncShortcut()
|
||||
}
|
||||
|
||||
func (u *ui) removeRemoteSyncShortcutLabel() string {
|
||||
return "Stop Using Remote Sync"
|
||||
return u.buildSyncMenuModel().removeRemoteSyncShortcutLabel()
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowRemoteSyncSetupShortcut() bool {
|
||||
if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
|
||||
return false
|
||||
}
|
||||
_, ok := u.selectedVaultRemoteBinding()
|
||||
return !ok
|
||||
return u.buildSyncMenuModel().showRemoteSyncSetupShortcut()
|
||||
}
|
||||
|
||||
func (u *ui) remoteSyncSetupShortcutLabel() string {
|
||||
return "Set Up Remote Sync"
|
||||
return u.buildSyncMenuModel().remoteSyncSetupShortcutLabel()
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuActionLabels() []string {
|
||||
labels := []string{"Open Advanced Sync"}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
labels = append(labels, u.remoteSyncSetupShortcutLabel())
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
labels = append(labels, u.directRemoteSyncShortcutLabel())
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
labels = append(labels, u.remoteSyncSettingsShortcutLabel())
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
labels = append(labels, u.removeRemoteSyncShortcutLabel())
|
||||
}
|
||||
return labels
|
||||
return u.buildSyncMenuModel().actionLabels()
|
||||
}
|
||||
|
||||
func remoteBindingSuffix(baseURL, path, username string) string {
|
||||
@@ -2939,11 +2902,11 @@ func (u *ui) removeSelectedRemoteBindingAction() error {
|
||||
}
|
||||
|
||||
func (u *ui) saveCurrentRemoteBindingHeading() string {
|
||||
return "Bind this local vault to the current remote target"
|
||||
return u.buildSyncMenuModel().saveCurrentRemoteBindingHeading()
|
||||
}
|
||||
|
||||
func (u *ui) saveCurrentRemoteBindingButtonLabel() string {
|
||||
return "Save Remote In Vault"
|
||||
return u.buildSyncMenuModel().saveCurrentRemoteBindingButtonLabel()
|
||||
}
|
||||
|
||||
func (u *ui) materializeCurrentRemoteCache() error {
|
||||
@@ -4801,46 +4764,6 @@ func (u *ui) syncHostedAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
|
||||
panel := card
|
||||
if u.mode == "phone" {
|
||||
panel = compactCard
|
||||
}
|
||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
u.lifecycleBranding,
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
u.lifecycleControls,
|
||||
}
|
||||
return material.List(u.theme, &u.lifecycleList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return rows[i](gtx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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) securityDialog(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -5263,160 +5186,6 @@ func (u *ui) approvalDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
matchingCredentials := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(u.syncRemoteCredentialClicks) < len(matchingCredentials) {
|
||||
u.syncRemoteCredentialClicks = make([]widget.Clickable, len(matchingCredentials))
|
||||
}
|
||||
return material.List(u.theme, &u.syncDialogList).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), u.syncDialogTitle())
|
||||
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), u.syncDialogDescription())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncDirectionChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Direction")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.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 syncChoiceButton(gtx, u.theme, &u.showSyncPull, "Pull Into Current Vault", u.syncDirection == syncDirectionPull)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSyncPush, "Push Current Vault Out", u.syncDirection == syncDirectionPush)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncDirectionChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(12)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncSourceChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Other Source")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.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 syncChoiceButton(gtx, u.theme, &u.showSyncLocal, "Local File", u.syncSourceMode == syncSourceLocal)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSyncRemote, "Remote WebDAV", u.syncSourceMode == syncSourceRemote)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncSourceChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(12)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncDialogSummaryCard(gtx, u.theme, u.syncDialogPurpose, u.syncSourceMode, u.syncDirection)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.syncSourceMode == syncSourceRemote {
|
||||
children := []layout.FlexChild{
|
||||
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)
|
||||
}),
|
||||
}
|
||||
if u.syncDialogPurpose == syncDialogPurposeRemoteSetup {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
check := material.CheckBox(u.theme, &u.syncSetupAutomatic, "Sync automatically on open and save")
|
||||
check.Color = accentColor
|
||||
return check.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
if len(matchingCredentials) > 0 {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Matching vault credentials")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
for i, entry := range matchingCredentials {
|
||||
i := i
|
||||
entry := entry
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
label += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
selected := strings.TrimSpace(u.selectedSyncRemoteCredentialEntryID) == entry.ID
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), label)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
children...,
|
||||
)
|
||||
}
|
||||
if supportsDesktopFilePicker(runtime.GOOS) {
|
||||
return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx)
|
||||
}
|
||||
return labeledEditorHelp(u.theme, "Local Vault Path", "Enter the shared-storage path to the other local .kdbx file to synchronize with.", &u.syncLocalPath, 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, u.syncDialogConfirmButtonLabel())
|
||||
}),
|
||||
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) pendingApproval() (apiapproval.Request, bool) {
|
||||
pending := u.state.PendingApprovals()
|
||||
if len(pending) == 0 {
|
||||
@@ -5473,184 +5242,6 @@ func approvalFact(theme *material.Theme, title, primary, secondary string) layou
|
||||
}
|
||||
}
|
||||
|
||||
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 u.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" {
|
||||
if u.shouldShowLifecycleSetup() || u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.headerActions(gtx)
|
||||
}
|
||||
if u.shouldShowDesktopWorkingHeader() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.brandMark(gtx, 196, 56)
|
||||
}),
|
||||
layout.Rigid(u.headerActions),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.shouldShowDesktopWorkingHeader() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
spacing := gtx.Dp(unit.Dp(8))
|
||||
rowOriginX := 0
|
||||
var syncDims, lockDims, mainDims layout.Dimensions
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
syncDims = u.syncButtonGroup(gtx)
|
||||
return syncDims
|
||||
}),
|
||||
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")
|
||||
lockDims = btn.Layout(gtx)
|
||||
return lockDims
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
mainDims = u.mainMenuButtonGroup(gtx)
|
||||
return mainDims
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
rowOps := op.Record(gtx.Ops)
|
||||
rowDims := row(gtx)
|
||||
rowCall := rowOps.Stop()
|
||||
|
||||
if u.mode == "phone" {
|
||||
rowOriginX = max(0, gtx.Constraints.Max.X-rowDims.Size.X)
|
||||
}
|
||||
|
||||
drawMenu := func(menu layout.Widget, triggerRightX, triggerBottomY int) {
|
||||
menuGTX := gtx
|
||||
menuGTX.Constraints.Min = image.Point{}
|
||||
menuGTX.Constraints.Max.X = gtx.Constraints.Max.X
|
||||
menuOps := op.Record(gtx.Ops)
|
||||
menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu)
|
||||
menuCall := menuOps.Stop()
|
||||
menuX := anchoredMenuOriginX(gtx.Constraints.Max.X, rowOriginX, triggerRightX, menuDims.Size.X)
|
||||
stack := op.Offset(image.Pt(menuX, triggerBottomY)).Push(gtx.Ops)
|
||||
menuCall.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
}
|
||||
|
||||
rowStack := op.Offset(image.Pt(rowOriginX, 0)).Push(gtx.Ops)
|
||||
rowCall.Add(gtx.Ops)
|
||||
rowStack.Pop()
|
||||
|
||||
if u.mode == "phone" {
|
||||
if u.syncMenuOpen {
|
||||
u.phoneSyncMenuVisible = true
|
||||
u.phoneSyncMenuAnchor = image.Pt(rowOriginX+syncDims.Size.X, rowDims.Size.Y)
|
||||
}
|
||||
if u.mainMenuOpen {
|
||||
triggerRightX := syncDims.Size.X + spacing + lockDims.Size.X + spacing + mainDims.Size.X
|
||||
u.phoneMainMenuVisible = true
|
||||
u.phoneMainMenuAnchor = image.Pt(rowOriginX+triggerRightX, rowDims.Size.Y)
|
||||
}
|
||||
width := gtx.Constraints.Max.X
|
||||
return layout.Dimensions{Size: image.Pt(width, rowDims.Size.Y)}
|
||||
}
|
||||
|
||||
if u.syncMenuOpen {
|
||||
drawMenu(u.syncMenu, syncDims.Size.X, rowDims.Size.Y)
|
||||
}
|
||||
if u.mainMenuOpen {
|
||||
triggerRightX := syncDims.Size.X + spacing + lockDims.Size.X + spacing + mainDims.Size.X
|
||||
drawMenu(u.mainMenu, triggerRightX, rowDims.Size.Y)
|
||||
}
|
||||
|
||||
width := rowDims.Size.X
|
||||
return layout.Dimensions{Size: image.Pt(width, rowDims.Size.Y)}
|
||||
}
|
||||
|
||||
func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showEntries, "Entries") },
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin") },
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens") },
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit") },
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") },
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings") },
|
||||
}
|
||||
rowWidth := menuActionWidth(gtx, rows)
|
||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[0])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[1])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[2])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[3])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[4])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[5])
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -5715,288 +5306,6 @@ func aboutFact(theme *material.Theme, title, primary, secondary string) layout.W
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||
label := "Sync"
|
||||
spacing := unit.Dp(4)
|
||||
if u.mode == "phone" {
|
||||
spacing = unit.Dp(3)
|
||||
}
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.mode == "phone")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncMenuToggle(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return row(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions")
|
||||
if u.syncMenuOpen {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
} else {
|
||||
btn.Background = color.NRGBA{R: 231, G: 236, B: 232, A: 255}
|
||||
btn.Color = accentColor
|
||||
}
|
||||
btn.Size = unit.Dp(18)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(8))
|
||||
if u.mode == "phone" {
|
||||
btn.Size = unit.Dp(16)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(7))
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
||||
profiles := u.availableRemoteProfiles()
|
||||
credentials := u.availableRemoteCredentialEntries()
|
||||
if len(u.vaultRemoteProfileClicks) < len(profiles) {
|
||||
u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles))
|
||||
}
|
||||
if len(u.vaultRemoteCredentialClicks) < len(credentials) {
|
||||
u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials))
|
||||
}
|
||||
actionRows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync") },
|
||||
}
|
||||
if supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "" {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
||||
})
|
||||
}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
|
||||
})
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
|
||||
})
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel())
|
||||
})
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel())
|
||||
})
|
||||
}
|
||||
actionWidth := menuActionWidth(gtx, actionRows)
|
||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.FlexChild{
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !supportsVaultShare(runtime.GOOS) || u.vaultSharer == nil || strings.TrimSpace(u.currentShareableVaultPath()) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
})
|
||||
}),
|
||||
}
|
||||
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowRemoteSyncSettingsShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.shouldShowRemoveRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), u.savedRemoteBindingHeading())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
if !u.shouldShowSavedRemoteBindingSelectors() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
profileLabel, credentialLabel, syncLabel, ok := u.savedRemoteBindingSummary()
|
||||
if !ok {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(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(13), profileLabel)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+credentialLabel)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), syncLabel)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
for i, profile := range profiles {
|
||||
i := i
|
||||
profile := profile
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID
|
||||
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), profile.Name)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
for i, entry := range credentials {
|
||||
i := i
|
||||
entry := entry
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID
|
||||
label := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
label += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), label)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
if _, ok := u.selectedVaultRemoteProfile(); ok {
|
||||
if _, ok := u.selectedVaultRemoteCredentialEntry(); ok {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if u.hasOpenVault() {
|
||||
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
||||
remotePath := strings.TrimSpace(u.remotePath.Text())
|
||||
username := strings.TrimSpace(u.remoteUsername.Text())
|
||||
password := u.remotePassword.Text()
|
||||
if baseURL != "" && remotePath != "" && username != "" && password != "" {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), u.saveCurrentRemoteBindingHeading())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, u.saveCurrentRemoteBindingButtonLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
||||
})
|
||||
}
|
||||
|
||||
func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
measureGTX := gtx
|
||||
measureGTX.Constraints.Min = image.Point{}
|
||||
measureGTX.Constraints.Max.X = gtx.Constraints.Max.X
|
||||
macro := op.Record(gtx.Ops)
|
||||
contentDims := w(measureGTX)
|
||||
_ = macro.Stop()
|
||||
width := contentDims.Size.X + gtx.Dp(unit.Dp(20))
|
||||
maxWidth := gtx.Constraints.Max.X
|
||||
if maxWidth > 0 && width > maxWidth {
|
||||
width = maxWidth
|
||||
}
|
||||
if width > 0 {
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
}
|
||||
return compactCard(gtx, w)
|
||||
}
|
||||
|
||||
func (u *ui) sectionSpacing() unit.Dp {
|
||||
if u.mode == "phone" {
|
||||
if u.denseLayout {
|
||||
@@ -7284,112 +6593,6 @@ func (u *ui) groupBarShowsExplicitNavigationButtons() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *ui) topRightActionOrder() []string {
|
||||
if u.isVaultLocked() {
|
||||
return nil
|
||||
}
|
||||
return []string{"Sync", "Lock", "Menu"}
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||
button := func(gtx layout.Context) layout.Dimensions {
|
||||
icon := u.menuIcon
|
||||
if icon == nil {
|
||||
icon = u.settingsIcon
|
||||
}
|
||||
btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu")
|
||||
if u.mainMenuOpen {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
} else {
|
||||
btn.Background = selectedColor
|
||||
btn.Color = accentColor
|
||||
}
|
||||
btn.Size = unit.Dp(18)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(8))
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
return button(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode != "phone" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
contentInsetPx := gtx.Dp(unit.Dp(16))
|
||||
contentWidth := max(0, gtx.Constraints.Max.X-(contentInsetPx*2))
|
||||
|
||||
drawMenu := func(anchor image.Point, menu layout.Widget) layout.Dimensions {
|
||||
menuGTX := gtx
|
||||
menuGTX.Constraints.Min = image.Point{}
|
||||
menuGTX.Constraints.Max.X = contentWidth
|
||||
menuOps := op.Record(gtx.Ops)
|
||||
menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu)
|
||||
menuCall := menuOps.Stop()
|
||||
menuX := contentInsetPx + anchoredMenuOriginX(contentWidth, 0, anchor.X, menuDims.Size.X)
|
||||
menuY := contentInsetPx + anchor.Y
|
||||
stack := op.Offset(image.Pt(menuX, menuY)).Push(gtx.Ops)
|
||||
menuCall.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
if u.syncMenuVisibleOnPhone() {
|
||||
_ = drawMenu(u.phoneSyncMenuAnchor, u.syncMenu)
|
||||
}
|
||||
if u.mainMenuVisibleOnPhone() {
|
||||
_ = drawMenu(u.phoneMainMenuAnchor, u.mainMenu)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuVisibleOnPhone() bool {
|
||||
return u.mode == "phone" && u.phoneSyncMenuVisible && u.syncMenuOpen
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuVisibleOnPhone() bool {
|
||||
return u.mode == "phone" && u.phoneMainMenuVisible && u.mainMenuOpen
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuDropsBelowTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuRightAlignsToTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) headerMenusUseOverlayModel() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuDropsBelowTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuRightAlignsToTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func anchoredMenuX(triggerWidth, menuWidth int) int {
|
||||
return triggerWidth - menuWidth
|
||||
}
|
||||
|
||||
func anchoredMenuOriginX(containerWidth, rowOriginX, triggerRightX, menuWidth int) int {
|
||||
x := rowOriginX + triggerRightX - menuWidth
|
||||
if x < 0 {
|
||||
return 0
|
||||
}
|
||||
if x+menuWidth > containerWidth {
|
||||
return max(0, containerWidth-menuWidth)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func detailLine(th *material.Theme, label, value string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
valueSize := unit.Sp(16)
|
||||
@@ -7592,30 +6795,6 @@ func syncChoiceButton(gtx layout.Context, th *material.Theme, click *widget.Clic
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
|
||||
func menuActionWidth(gtx layout.Context, rows []layout.Widget) int {
|
||||
width := 0
|
||||
for _, row := range rows {
|
||||
measureGTX := gtx
|
||||
measureGTX.Constraints.Min = image.Point{}
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := row(measureGTX)
|
||||
_ = macro.Stop()
|
||||
if dims.Size.X > width {
|
||||
width = dims.Size.X
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions {
|
||||
if width <= 0 {
|
||||
return child(gtx)
|
||||
}
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
return layout.E.Layout(gtx, child)
|
||||
}
|
||||
|
||||
func syncDialogSectionLabel(th *material.Theme, text string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(th, unit.Sp(12), strings.ToUpper(text))
|
||||
|
||||
+144
@@ -391,6 +391,150 @@ func TestAnchoredMenuOriginXClampsToVisibleContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderActionMetricsComputeTriggerAnchors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
metrics := headerActionMetrics{
|
||||
RowOriginX: 24,
|
||||
Spacing: 8,
|
||||
RowDims: layout.Dimensions{Size: image.Pt(180, 40)},
|
||||
SyncDims: layout.Dimensions{Size: image.Pt(52, 40)},
|
||||
LockDims: layout.Dimensions{Size: image.Pt(44, 40)},
|
||||
MainDims: layout.Dimensions{Size: image.Pt(36, 40)},
|
||||
}
|
||||
|
||||
if got := metrics.syncAnchor(); got != (dropdownAnchor{TriggerRightX: 76, TriggerBottomY: 40}) {
|
||||
t.Fatalf("metrics.syncAnchor() = %+v, want right=76 bottom=40", got)
|
||||
}
|
||||
if got := metrics.mainAnchor(); got != (dropdownAnchor{TriggerRightX: 172, TriggerBottomY: 40}) {
|
||||
t.Fatalf("metrics.mainAnchor() = %+v, want right=172 bottom=40", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropdownSurfaceOriginKeepsMenusWithinVisibleArea(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
surface := dropdownSurface{ContainerWidth: 320, LeftInset: 16, TopInset: 16}
|
||||
anchor := dropdownAnchor{TriggerRightX: 300, TriggerBottomY: 42}
|
||||
|
||||
if got := surface.origin(anchor, 140); got != image.Pt(176, 58) {
|
||||
t.Fatalf("surface.origin(anchor, 140) = %v, want (176,58)", got)
|
||||
}
|
||||
|
||||
leftAnchor := dropdownAnchor{TriggerRightX: 36, TriggerBottomY: 42}
|
||||
if got := surface.origin(leftAnchor, 120); got != image.Pt(16, 58) {
|
||||
t.Fatalf("surface.origin(leftAnchor, 120) = %v, want (16,58)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSyncMenuModelForUnboundVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "mint-ledger", Title: "Mint Ledger"},
|
||||
},
|
||||
})
|
||||
u.state.Section = appstate.SectionEntries
|
||||
|
||||
model := u.buildSyncMenuModel()
|
||||
if !model.showRemoteSyncSetupShortcut() {
|
||||
t.Fatal("model.showRemoteSyncSetupShortcut() = false, want true for an unbound open vault")
|
||||
}
|
||||
if model.showDirectRemoteSyncShortcut() {
|
||||
t.Fatal("model.showDirectRemoteSyncShortcut() = true, want false without a saved binding")
|
||||
}
|
||||
if got := model.actionLabels(); !slices.Equal(got, []string{"Open Advanced Sync", "Set Up Remote Sync"}) {
|
||||
t.Fatalf("model.actionLabels() = %v, want [Open Advanced Sync Set Up Remote Sync]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSyncMenuModelForBoundVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "mint-credential",
|
||||
Title: "Mint Credentials",
|
||||
Username: "verbal-kint",
|
||||
},
|
||||
},
|
||||
RemoteProfiles: []vault.RemoteProfile{
|
||||
{
|
||||
ID: "mint-profile",
|
||||
Name: "Downtown Mint",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://mint.example.invalid/remote.php/dav",
|
||||
Path: "/files/kint/mint.kdbx",
|
||||
},
|
||||
},
|
||||
})
|
||||
u.state.Section = appstate.SectionEntries
|
||||
u.selectedVaultRemoteProfileID = "mint-profile"
|
||||
u.selectedVaultRemoteCredentialEntryID = "mint-credential"
|
||||
u.selectedVaultRemoteSyncMode = appstate.SyncModeAutomaticOnOpenSave
|
||||
|
||||
model := u.buildSyncMenuModel()
|
||||
if model.showRemoteSyncSetupShortcut() {
|
||||
t.Fatal("model.showRemoteSyncSetupShortcut() = true, want false for a bound vault")
|
||||
}
|
||||
if !model.showDirectRemoteSyncShortcut() {
|
||||
t.Fatal("model.showDirectRemoteSyncShortcut() = false, want true for a bound vault")
|
||||
}
|
||||
if !model.showRemoteSyncSettingsShortcut() {
|
||||
t.Fatal("model.showRemoteSyncSettingsShortcut() = false, want true for a bound vault")
|
||||
}
|
||||
if !model.showRemoveRemoteSyncShortcut() {
|
||||
t.Fatal("model.showRemoveRemoteSyncShortcut() = false, want true for a bound vault")
|
||||
}
|
||||
summary := model.savedBindingSummary
|
||||
if !summary.ok {
|
||||
t.Fatal("model.savedBindingSummary.ok = false, want true")
|
||||
}
|
||||
if summary.profileLabel != "Downtown Mint" {
|
||||
t.Fatalf("model.savedBindingSummary.profileLabel = %q, want Downtown Mint", summary.profileLabel)
|
||||
}
|
||||
if summary.credentialLabel != "Mint Credentials · verbal-kint" {
|
||||
t.Fatalf("model.savedBindingSummary.credentialLabel = %q, want Mint Credentials · verbal-kint", summary.credentialLabel)
|
||||
}
|
||||
if summary.syncLabel != "Syncs automatically on open and save." {
|
||||
t.Fatalf("model.savedBindingSummary.syncLabel = %q, want automatic-sync summary", summary.syncLabel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSyncMenuModelShowsSaveCurrentBindingOnlyWithCompleteRemoteInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "mint-ledger", Title: "Mint Ledger"},
|
||||
},
|
||||
})
|
||||
u.state.Section = appstate.SectionEntries
|
||||
|
||||
model := u.buildSyncMenuModel()
|
||||
if model.showSaveCurrentBinding {
|
||||
t.Fatal("model.showSaveCurrentBinding = true, want false without remote input")
|
||||
}
|
||||
|
||||
u.remoteBaseURL.SetText("https://mint.example.invalid/remote.php/dav")
|
||||
u.remotePath.SetText("/files/kint/mint.kdbx")
|
||||
u.remoteUsername.SetText("verbal-kint")
|
||||
u.remotePassword.SetText("kobayashi")
|
||||
|
||||
model = u.buildSyncMenuModel()
|
||||
if !model.showSaveCurrentBinding {
|
||||
t.Fatal("model.showSaveCurrentBinding = false, want true with complete remote input")
|
||||
}
|
||||
if got := model.saveCurrentRemoteBindingHeading(); got != "Bind this local vault to the current remote target" {
|
||||
t.Fatalf("model.saveCurrentRemoteBindingHeading() = %q, want vault binding guidance", got)
|
||||
}
|
||||
if got := model.saveCurrentRemoteBindingButtonLabel(); got != "Save Remote In Vault" {
|
||||
t.Fatalf("model.saveCurrentRemoteBindingButtonLabel() = %q, want Save Remote In Vault", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUICurrentVaultSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type dropdownAnchor struct {
|
||||
TriggerRightX int
|
||||
TriggerBottomY int
|
||||
}
|
||||
|
||||
func (a dropdownAnchor) point() image.Point {
|
||||
return image.Pt(a.TriggerRightX, a.TriggerBottomY)
|
||||
}
|
||||
|
||||
type dropdownSurface struct {
|
||||
ContainerWidth int
|
||||
LeftInset int
|
||||
TopInset int
|
||||
}
|
||||
|
||||
func (s dropdownSurface) menuConstraints(gtx layout.Context) layout.Context {
|
||||
menuGTX := gtx
|
||||
menuGTX.Constraints.Min = image.Point{}
|
||||
menuGTX.Constraints.Max.X = max(0, s.ContainerWidth)
|
||||
return menuGTX
|
||||
}
|
||||
|
||||
func (s dropdownSurface) origin(anchor dropdownAnchor, menuWidth int) image.Point {
|
||||
x := s.LeftInset + anchoredMenuOriginX(s.ContainerWidth, 0, anchor.TriggerRightX, menuWidth)
|
||||
y := s.TopInset + anchor.TriggerBottomY
|
||||
return image.Pt(x, y)
|
||||
}
|
||||
|
||||
func (s dropdownSurface) draw(gtx layout.Context, anchor dropdownAnchor, menu layout.Widget) layout.Dimensions {
|
||||
menuGTX := s.menuConstraints(gtx)
|
||||
menuOps := op.Record(gtx.Ops)
|
||||
menuDims := layout.Inset{Top: unit.Dp(6)}.Layout(menuGTX, menu)
|
||||
menuCall := menuOps.Stop()
|
||||
menuOrigin := s.origin(anchor, menuDims.Size.X)
|
||||
stack := op.Offset(menuOrigin).Push(gtx.Ops)
|
||||
menuCall.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
type headerActionMetrics struct {
|
||||
RowOriginX int
|
||||
Spacing int
|
||||
RowDims layout.Dimensions
|
||||
SyncDims layout.Dimensions
|
||||
LockDims layout.Dimensions
|
||||
MainDims layout.Dimensions
|
||||
}
|
||||
|
||||
func (m headerActionMetrics) syncAnchor() dropdownAnchor {
|
||||
return dropdownAnchor{
|
||||
TriggerRightX: m.RowOriginX + m.SyncDims.Size.X,
|
||||
TriggerBottomY: m.RowDims.Size.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (m headerActionMetrics) mainAnchor() dropdownAnchor {
|
||||
triggerRightX := m.SyncDims.Size.X + m.Spacing + m.LockDims.Size.X + m.Spacing + m.MainDims.Size.X
|
||||
return dropdownAnchor{
|
||||
TriggerRightX: m.RowOriginX + triggerRightX,
|
||||
TriggerBottomY: m.RowDims.Size.Y,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
func (u *ui) header(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode == "phone" {
|
||||
if u.shouldShowLifecycleSetup() || u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return u.headerActions(gtx)
|
||||
}
|
||||
if u.shouldShowDesktopWorkingHeader() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.brandMark(gtx, 196, 56)
|
||||
}),
|
||||
layout.Rigid(u.headerActions),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
|
||||
if u.shouldShowLifecycleSetup() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.isVaultLocked() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if u.shouldShowDesktopWorkingHeader() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
spacing := gtx.Dp(unit.Dp(8))
|
||||
metrics := headerActionMetrics{Spacing: spacing}
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
metrics.SyncDims = u.syncButtonGroup(gtx)
|
||||
return metrics.SyncDims
|
||||
}),
|
||||
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")
|
||||
metrics.LockDims = btn.Layout(gtx)
|
||||
return metrics.LockDims
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
metrics.MainDims = u.mainMenuButtonGroup(gtx)
|
||||
return metrics.MainDims
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
rowOps := op.Record(gtx.Ops)
|
||||
metrics.RowDims = row(gtx)
|
||||
rowCall := rowOps.Stop()
|
||||
|
||||
if u.mode == "phone" {
|
||||
metrics.RowOriginX = max(0, gtx.Constraints.Max.X-metrics.RowDims.Size.X)
|
||||
}
|
||||
|
||||
surface := dropdownSurface{ContainerWidth: gtx.Constraints.Max.X, LeftInset: 0, TopInset: 0}
|
||||
|
||||
rowStack := op.Offset(image.Pt(metrics.RowOriginX, 0)).Push(gtx.Ops)
|
||||
rowCall.Add(gtx.Ops)
|
||||
rowStack.Pop()
|
||||
|
||||
if u.mode == "phone" {
|
||||
if u.syncMenuOpen {
|
||||
u.phoneSyncMenuVisible = true
|
||||
u.phoneSyncMenuAnchor = metrics.syncAnchor().point()
|
||||
}
|
||||
if u.mainMenuOpen {
|
||||
u.phoneMainMenuVisible = true
|
||||
u.phoneMainMenuAnchor = metrics.mainAnchor().point()
|
||||
}
|
||||
width := gtx.Constraints.Max.X
|
||||
return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)}
|
||||
}
|
||||
|
||||
if u.syncMenuOpen {
|
||||
surface.draw(gtx, metrics.syncAnchor(), u.syncMenu)
|
||||
}
|
||||
if u.mainMenuOpen {
|
||||
surface.draw(gtx, metrics.mainAnchor(), u.mainMenu)
|
||||
}
|
||||
|
||||
width := metrics.RowDims.Size.X
|
||||
return layout.Dimensions{Size: image.Pt(width, metrics.RowDims.Size.Y)}
|
||||
}
|
||||
|
||||
func (u *ui) mainMenu(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showEntries, "Entries")
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin")
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showAPITokens, "API Tokens")
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.showAPIAudit, "API Audit")
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.showAbout, "About") },
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Settings")
|
||||
},
|
||||
}
|
||||
rowWidth := menuActionWidth(gtx, rows)
|
||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[0])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[1])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[2])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[3])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[4])
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, rowWidth, rows[5])
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) syncButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||
label := "Sync"
|
||||
spacing := unit.Dp(4)
|
||||
if u.mode == "phone" {
|
||||
spacing = unit.Dp(3)
|
||||
}
|
||||
row := func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncPrimaryButton(gtx, u.theme, &u.synchronizeVault, label, u.mode == "phone")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: spacing}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncMenuToggle(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return row(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuToggle(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.IconButton(u.theme, &u.toggleSyncMenu, u.chevronDownIcon, "More synchronize actions")
|
||||
if u.syncMenuOpen {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
} else {
|
||||
btn.Background = color.NRGBA{R: 231, G: 236, B: 232, A: 255}
|
||||
btn.Color = accentColor
|
||||
}
|
||||
btn.Size = unit.Dp(18)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(8))
|
||||
if u.mode == "phone" {
|
||||
btn.Size = unit.Dp(16)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(7))
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
||||
model := u.buildSyncMenuModel()
|
||||
profiles := u.availableRemoteProfiles()
|
||||
credentials := u.availableRemoteCredentialEntries()
|
||||
if len(u.vaultRemoteProfileClicks) < len(profiles) {
|
||||
u.vaultRemoteProfileClicks = make([]widget.Clickable, len(profiles))
|
||||
}
|
||||
if len(u.vaultRemoteCredentialClicks) < len(credentials) {
|
||||
u.vaultRemoteCredentialClicks = make([]widget.Clickable, len(credentials))
|
||||
}
|
||||
actionRows := []layout.Widget{
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
},
|
||||
}
|
||||
if model.showShare {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
||||
})
|
||||
}
|
||||
if model.showRemoteSyncSetupShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSetupShortcutLabel())
|
||||
})
|
||||
}
|
||||
if model.showDirectRemoteSyncShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.directRemoteSyncShortcutLabel())
|
||||
})
|
||||
}
|
||||
if model.showRemoteSyncSettingsShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSettingsShortcutLabel())
|
||||
})
|
||||
}
|
||||
if model.showRemoveRemoteSyncShortcut() {
|
||||
actionRows = append(actionRows, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.removeRemoteSyncShortcutLabel())
|
||||
})
|
||||
}
|
||||
actionWidth := menuActionWidth(gtx, actionRows)
|
||||
return intrinsicCompactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.FlexChild{
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Need another source or direction?")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !model.showShare {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
})
|
||||
}),
|
||||
}
|
||||
if model.showRemoteSyncSetupShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSetupShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if model.showDirectRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, model.directRemoteSyncShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if model.showRemoteSyncSettingsShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, model.remoteSyncSettingsShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if model.showRemoveRemoteSyncShortcut() {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return rightAlignedMenuAction(gtx, actionWidth, func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, model.removeRemoteSyncShortcutLabel())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if u.hasOpenVault() && len(profiles) > 0 && len(credentials) > 0 {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), model.savedBindingHeading())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
if !model.showSelectors {
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
summary := model.savedBindingSummary
|
||||
if !summary.ok {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Background{}.Layout(gtx, fill(color.NRGBA{R: 242, G: 245, B: 240, A: 255}), func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(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(13), summary.profileLabel)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "Credential: "+summary.credentialLabel)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), summary.syncLabel)
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
for i, profile := range profiles {
|
||||
i := i
|
||||
profile := profile
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
selected := strings.TrimSpace(u.selectedVaultRemoteProfileID) == profile.ID
|
||||
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.vaultRemoteProfileClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), profile.Name)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
rows = append(rows, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
for i, entry := range credentials {
|
||||
i := i
|
||||
entry := entry
|
||||
rows = append(rows,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
selected := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == entry.ID
|
||||
label := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
label += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
return layout.Inset{Bottom: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.vaultRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), label)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
if _, ok := u.selectedVaultRemoteProfile(); ok {
|
||||
if _, ok := u.selectedVaultRemoteCredentialEntry(); ok {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.openSelectedVaultRemoteButtonLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if model.showSaveCurrentBinding {
|
||||
rows = append(rows,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), model.saveCurrentRemoteBindingHeading())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, model.saveCurrentRemoteBindingButtonLabel())
|
||||
}),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
||||
})
|
||||
}
|
||||
|
||||
func intrinsicCompactCard(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
measureGTX := gtx
|
||||
measureGTX.Constraints.Min = image.Point{}
|
||||
measureGTX.Constraints.Max.X = gtx.Constraints.Max.X
|
||||
macro := op.Record(gtx.Ops)
|
||||
contentDims := w(measureGTX)
|
||||
_ = macro.Stop()
|
||||
width := contentDims.Size.X + gtx.Dp(unit.Dp(20))
|
||||
maxWidth := gtx.Constraints.Max.X
|
||||
if maxWidth > 0 && width > maxWidth {
|
||||
width = maxWidth
|
||||
}
|
||||
if width > 0 {
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
}
|
||||
return compactCard(gtx, w)
|
||||
}
|
||||
|
||||
func (u *ui) topRightActionOrder() []string {
|
||||
if u.isVaultLocked() {
|
||||
return nil
|
||||
}
|
||||
return []string{"Sync", "Lock", "Menu"}
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuButtonGroup(gtx layout.Context) layout.Dimensions {
|
||||
button := func(gtx layout.Context) layout.Dimensions {
|
||||
icon := u.menuIcon
|
||||
if icon == nil {
|
||||
icon = u.settingsIcon
|
||||
}
|
||||
btn := material.IconButton(u.theme, &u.toggleMainMenu, icon, "Menu")
|
||||
if u.mainMenuOpen {
|
||||
btn.Background = accentColor
|
||||
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
} else {
|
||||
btn.Background = selectedColor
|
||||
btn.Color = accentColor
|
||||
}
|
||||
btn.Size = unit.Dp(18)
|
||||
btn.Inset = layout.UniformInset(unit.Dp(8))
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
return button(gtx)
|
||||
}
|
||||
|
||||
func (u *ui) phoneHeaderMenus(gtx layout.Context) layout.Dimensions {
|
||||
if u.mode != "phone" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if !u.syncMenuVisibleOnPhone() && !u.mainMenuVisibleOnPhone() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
contentInsetPx := gtx.Dp(unit.Dp(16))
|
||||
surface := dropdownSurface{
|
||||
ContainerWidth: max(0, gtx.Constraints.Max.X-(contentInsetPx*2)),
|
||||
LeftInset: contentInsetPx,
|
||||
TopInset: contentInsetPx,
|
||||
}
|
||||
|
||||
if u.syncMenuVisibleOnPhone() {
|
||||
surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneSyncMenuAnchor.X, TriggerBottomY: u.phoneSyncMenuAnchor.Y}, u.syncMenu)
|
||||
}
|
||||
if u.mainMenuVisibleOnPhone() {
|
||||
surface.draw(gtx, dropdownAnchor{TriggerRightX: u.phoneMainMenuAnchor.X, TriggerBottomY: u.phoneMainMenuAnchor.Y}, u.mainMenu)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuVisibleOnPhone() bool {
|
||||
return u.mode == "phone" && u.phoneSyncMenuVisible && u.syncMenuOpen
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuVisibleOnPhone() bool {
|
||||
return u.mode == "phone" && u.phoneMainMenuVisible && u.mainMenuOpen
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuDropsBelowTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) syncMenuRightAlignsToTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) headerMenusUseOverlayModel() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuDropsBelowTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *ui) mainMenuRightAlignsToTrigger() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func anchoredMenuX(triggerWidth, menuWidth int) int {
|
||||
return triggerWidth - menuWidth
|
||||
}
|
||||
|
||||
func anchoredMenuOriginX(containerWidth, rowOriginX, triggerRightX, menuWidth int) int {
|
||||
x := rowOriginX + triggerRightX - menuWidth
|
||||
if x < 0 {
|
||||
return 0
|
||||
}
|
||||
if x+menuWidth > containerWidth {
|
||||
return max(0, containerWidth-menuWidth)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func menuActionWidth(gtx layout.Context, rows []layout.Widget) int {
|
||||
width := 0
|
||||
for _, row := range rows {
|
||||
measureGTX := gtx
|
||||
measureGTX.Constraints.Min = image.Point{}
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := row(measureGTX)
|
||||
_ = macro.Stop()
|
||||
if dims.Size.X > width {
|
||||
width = dims.Size.X
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func rightAlignedMenuAction(gtx layout.Context, width int, child layout.Widget) layout.Dimensions {
|
||||
if width <= 0 {
|
||||
return child(gtx)
|
||||
}
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
return layout.E.Layout(gtx, child)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions {
|
||||
panel := card
|
||||
if u.mode == "phone" {
|
||||
panel = compactCard
|
||||
}
|
||||
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
rows := []layout.Widget{
|
||||
u.lifecycleBranding,
|
||||
layout.Spacer{Height: unit.Dp(8)}.Layout,
|
||||
u.lifecycleControls,
|
||||
}
|
||||
return material.List(u.theme, &u.lifecycleList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return rows[i](gtx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
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 {
|
||||
matchingCredentials := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(u.syncRemoteCredentialClicks) < len(matchingCredentials) {
|
||||
u.syncRemoteCredentialClicks = make([]widget.Clickable, len(matchingCredentials))
|
||||
}
|
||||
return material.List(u.theme, &u.syncDialogList).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), u.syncDialogTitle())
|
||||
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), u.syncDialogDescription())
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncDirectionChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Direction")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.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 syncChoiceButton(gtx, u.theme, &u.showSyncPull, "Pull Into Current Vault", u.syncDirection == syncDirectionPull)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSyncPush, "Push Current Vault Out", u.syncDirection == syncDirectionPush)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncDirectionChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(12)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncSourceChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Other Source")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.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 syncChoiceButton(gtx, u.theme, &u.showSyncLocal, "Local File", u.syncSourceMode == syncSourceLocal)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncChoiceButton(gtx, u.theme, &u.showSyncRemote, "Remote WebDAV", u.syncSourceMode == syncSourceRemote)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !u.shouldShowSyncSourceChoices() {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Spacer{Height: unit.Dp(12)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return syncDialogSummaryCard(gtx, u.theme, u.syncDialogPurpose, u.syncSourceMode, u.syncDirection)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.syncSourceMode == syncSourceRemote {
|
||||
children := []layout.FlexChild{
|
||||
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)
|
||||
}),
|
||||
}
|
||||
if u.syncDialogPurpose == syncDialogPurposeRemoteSetup {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
check := material.CheckBox(u.theme, &u.syncSetupAutomatic, "Sync automatically on open and save")
|
||||
check.Color = accentColor
|
||||
return check.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
if len(matchingCredentials) > 0 {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Matching vault credentials")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
for i, entry := range matchingCredentials {
|
||||
i := i
|
||||
entry := entry
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
label += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
selected := strings.TrimSpace(u.selectedSyncRemoteCredentialEntryID) == entry.ID
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), label)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||
}
|
||||
if supportsDesktopFilePicker(runtime.GOOS) {
|
||||
return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx)
|
||||
}
|
||||
return labeledEditorHelp(u.theme, "Local Vault Path", "Enter the shared-storage path to the other local .kdbx file to synchronize with.", &u.syncLocalPath, 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, u.syncDialogConfirmButtonLabel())
|
||||
}),
|
||||
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 u.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)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"git.julianfamily.org/keepassgo/appstate"
|
||||
)
|
||||
|
||||
type syncMenuModel struct {
|
||||
hasOpenVault bool
|
||||
hasSelectedBinding bool
|
||||
showSelectors bool
|
||||
showShare bool
|
||||
showSaveCurrentBinding bool
|
||||
savedBindingSummary syncMenuBindingSummary
|
||||
remoteBaseURL string
|
||||
remotePath string
|
||||
remoteUsername string
|
||||
remotePassword string
|
||||
selectedVaultSyncMode appstate.SyncMode
|
||||
}
|
||||
|
||||
type syncMenuBindingSummary struct {
|
||||
profileLabel string
|
||||
credentialLabel string
|
||||
syncLabel string
|
||||
ok bool
|
||||
}
|
||||
|
||||
func (u *ui) buildSyncMenuModel() syncMenuModel {
|
||||
model := syncMenuModel{
|
||||
hasOpenVault: u.hasOpenVault(),
|
||||
showSelectors: u.shouldShowSavedRemoteBindingSelectors(),
|
||||
showShare: supportsVaultShare(runtime.GOOS) && u.vaultSharer != nil && strings.TrimSpace(u.currentShareableVaultPath()) != "",
|
||||
remoteBaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
remotePath: strings.TrimSpace(u.remotePath.Text()),
|
||||
remoteUsername: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
remotePassword: u.remotePassword.Text(),
|
||||
selectedVaultSyncMode: normalizeUISyncMode(u.selectedVaultRemoteSyncMode),
|
||||
}
|
||||
_, model.hasSelectedBinding = u.selectedVaultRemoteBinding()
|
||||
model.savedBindingSummary = u.computeSavedRemoteBindingSummary()
|
||||
model.showSaveCurrentBinding = model.hasOpenVault && model.remoteBaseURL != "" && model.remotePath != "" && model.remoteUsername != "" && model.remotePassword != ""
|
||||
return model
|
||||
}
|
||||
|
||||
func (u *ui) computeSavedRemoteBindingSummary() syncMenuBindingSummary {
|
||||
profile, ok := u.selectedVaultRemoteProfile()
|
||||
if !ok {
|
||||
return syncMenuBindingSummary{}
|
||||
}
|
||||
entry, ok := u.selectedVaultRemoteCredentialEntry()
|
||||
if !ok {
|
||||
return syncMenuBindingSummary{}
|
||||
}
|
||||
credentialLabel := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
credentialLabel += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
syncLabel := "Sync manually when you choose Use Remote Sync."
|
||||
if normalizeUISyncMode(u.selectedVaultRemoteSyncMode) == appstate.SyncModeAutomaticOnOpenSave {
|
||||
syncLabel = "Syncs automatically on open and save."
|
||||
}
|
||||
return syncMenuBindingSummary{
|
||||
profileLabel: profile.Name,
|
||||
credentialLabel: credentialLabel,
|
||||
syncLabel: syncLabel,
|
||||
ok: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (m syncMenuModel) savedBindingHeading() string {
|
||||
if !m.showSelectors {
|
||||
return "Use this vault's saved remote sync target"
|
||||
}
|
||||
return "Use a saved remote profile from this vault"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) openSelectedButtonLabel() string {
|
||||
if !m.showSelectors {
|
||||
return "Use Remote Sync"
|
||||
}
|
||||
return "Open Saved Remote"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) showDirectRemoteSyncShortcut() bool {
|
||||
return m.hasOpenVault && m.hasSelectedBinding
|
||||
}
|
||||
|
||||
func (m syncMenuModel) directRemoteSyncShortcutLabel() string {
|
||||
return "Use Remote Sync"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) showRemoteSyncSettingsShortcut() bool {
|
||||
return m.hasOpenVault && m.hasSelectedBinding
|
||||
}
|
||||
|
||||
func (m syncMenuModel) remoteSyncSettingsShortcutLabel() string {
|
||||
return "Remote Sync Settings"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) showRemoveRemoteSyncShortcut() bool {
|
||||
return m.showRemoteSyncSettingsShortcut()
|
||||
}
|
||||
|
||||
func (m syncMenuModel) removeRemoteSyncShortcutLabel() string {
|
||||
return "Stop Using Remote Sync"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) showRemoteSyncSetupShortcut() bool {
|
||||
return m.hasOpenVault && !m.hasSelectedBinding
|
||||
}
|
||||
|
||||
func (m syncMenuModel) remoteSyncSetupShortcutLabel() string {
|
||||
return "Set Up Remote Sync"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) actionLabels() []string {
|
||||
labels := []string{"Open Advanced Sync"}
|
||||
if m.showRemoteSyncSetupShortcut() {
|
||||
labels = append(labels, m.remoteSyncSetupShortcutLabel())
|
||||
}
|
||||
if m.showDirectRemoteSyncShortcut() {
|
||||
labels = append(labels, m.directRemoteSyncShortcutLabel())
|
||||
}
|
||||
if m.showRemoteSyncSettingsShortcut() {
|
||||
labels = append(labels, m.remoteSyncSettingsShortcutLabel())
|
||||
}
|
||||
if m.showRemoveRemoteSyncShortcut() {
|
||||
labels = append(labels, m.removeRemoteSyncShortcutLabel())
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func (m syncMenuModel) saveCurrentRemoteBindingHeading() string {
|
||||
return "Bind this local vault to the current remote target"
|
||||
}
|
||||
|
||||
func (m syncMenuModel) saveCurrentRemoteBindingButtonLabel() string {
|
||||
return "Save Remote In Vault"
|
||||
}
|
||||
Reference in New Issue
Block a user