Compare commits

...

5 Commits

Author SHA1 Message Date
Joe Julian a867ac4664 Codify workflow parity requirements 2026-04-07 07:12:29 -07:00
Joe Julian 1aab5367a8 Prioritize phone vault actions 2026-04-07 06:53:13 -07:00
Joe Julian 5d435f1f1f Move remote sync actions into sync menu 2026-04-06 22:30:05 -07:00
Joe Julian 7868a77c8a Reset remote sync dialog scroll state 2026-04-06 22:28:14 -07:00
Joe Julian 43ef58936b Match remote sync credentials by host 2026-04-06 22:22:18 -07:00
5 changed files with 371 additions and 133 deletions
+19
View File
@@ -95,6 +95,17 @@ These features are product requirements, not “nice to have” ideas.
- Phone should optimize for low tap count, not purity of mobile patterns.
- The stacked phone layout is the current preferred phone direction.
- Do not reintroduce the abandoned phone flow mode unless explicitly requested.
- Keep the product feeling like the same application on desktop, Android phone,
and Android tablet.
- Platform adaptation is allowed for layout and spacing, not for changing the
user's mental model of the workflow.
- Use the same action names, the same primary next steps, and the same workflow
order across platforms unless there is a hard platform constraint.
- Treat workflow prominence and reachability as product behavior, not visual
polish. A feature is not parity-complete if it technically exists but is
harder to discover or reach on one platform.
- Prefer shared workflow decisions with platform-specific presentation, rather
than platform-specific workflow branches.
- Make all test strings `Heist Movie` themed. Use characters, crews, casinos,
vaults, and locations from heist movies so test fixtures stay obviously fake
and consistent with the product theme.
@@ -117,6 +128,14 @@ These features are product requirements, not “nice to have” ideas.
implement the minimum code to satisfy them,
verify with `go test ./...` and relevant lint checks,
and commit each completed behavior.
- For cross-platform UI work, behavior tests must cover workflow parity, not
just feature or label parity.
- For lifecycle, open, unlock, sync, and other primary flows, tests should
assert the same conceptual next step across desktop, phone, and tablet
layouts.
- When Android or phone UX is part of the slice, verify real reachability on an
emulator or device for the exact flow being changed. Do not count “the same
buttons exist somewhere on screen” as sufficient validation.
- Only stop before the requirements are satisfied if the work is genuinely blocked by a missing decision, missing external dependency, or a hard technical constraint that cannot be resolved within the repo.
- If blocked, state the blocker concretely and stop only at that point.
+30
View File
@@ -11,6 +11,36 @@ The product is not complete until the global exit criteria at the end of this fi
These items came from a hands-on emulator and desktop walkthrough.
They should be treated as usability work, not just polish.
### Cross-Platform Workflow Parity
These items are required to keep desktop, Android phone, and Android tablet
feeling like the same application rather than three related UIs.
- Workflow parity:
define canonical workflows for open, unlock, set up remote sync, use remote
sync, browse entries, and edit entries.
- Workflow parity:
ensure desktop, phone, and tablet use the same action names and the same
primary next steps for those workflows.
- Workflow parity:
remove or reduce platform-specific workflow exceptions where the same user
intent currently takes a different route on different form factors.
- Testing:
add cross-mode behavior tests that assert workflow order and action
prominence, not just label presence.
- Testing:
add explicit lifecycle/open-screen tests for reachability of the primary
action on desktop, phone, and tablet layouts.
- Testing:
add explicit remote-sync workflow tests that prove setup, settings, use, and
removal are reachable from the same primary affordance family across modes.
- Android verification:
validate changed lifecycle/open/sync workflows on the emulator or a device,
including with the on-screen keyboard visible.
- Android verification:
treat “present but below the fold or behind an unexpected branch” as a parity
failure, not as acceptable platform variation.
### Primary Workflow Changes
These should remain in the main user flow rather than being hidden behind a settings gear.
+88 -40
View File
@@ -10,6 +10,7 @@ import (
"image"
"image/color"
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -281,6 +282,7 @@ type ui struct {
detailList widget.List
apiPolicyList widget.List
lifecycleList widget.List
syncDialogList widget.List
phonePanelList widget.List
securityDialogList widget.List
remotePrefsDialogList widget.List
@@ -628,6 +630,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
lifecycleList: widget.List{
List: layout.List{Axis: layout.Vertical},
},
syncDialogList: widget.List{
List: layout.List{Axis: layout.Vertical},
},
phonePanelList: widget.List{
List: layout.List{Axis: layout.Vertical},
},
@@ -1421,6 +1426,7 @@ func (u *ui) openAdvancedSyncDialog() {
u.syncDialogOpen = true
u.syncMenuOpen = false
u.showSyncPassword = false
u.syncDialogList.Position = layout.Position{}
u.syncDialogPurpose = syncDialogPurposeAdvanced
u.syncSourceMode = u.syncDefaultSourceMode
u.syncDirection = u.syncDefaultDirection
@@ -1435,6 +1441,7 @@ func (u *ui) openRemoteSyncSetupDialog() {
u.syncDialogOpen = true
u.syncMenuOpen = false
u.showSyncPassword = false
u.syncDialogList.Position = layout.Position{}
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
u.syncSourceMode = syncSourceRemote
u.syncDirection = syncDirectionPush
@@ -2319,6 +2326,34 @@ func normalizeRemoteCredentialURL(raw string) string {
return raw
}
func remoteCredentialURLMatches(candidate, target string) bool {
candidate = normalizeRemoteCredentialURL(candidate)
target = normalizeRemoteCredentialURL(target)
if candidate == "" || target == "" {
return false
}
if candidate == target {
return true
}
candidateURL, err := url.Parse(candidate)
if err != nil {
return false
}
targetURL, err := url.Parse(target)
if err != nil {
return false
}
if !strings.EqualFold(candidateURL.Hostname(), targetURL.Hostname()) {
return false
}
candidatePath := strings.TrimRight(candidateURL.EscapedPath(), "/")
targetPath := strings.TrimRight(targetURL.EscapedPath(), "/")
if candidatePath == "" || candidatePath == "/" || targetPath == "" || targetPath == "/" {
return true
}
return strings.HasPrefix(targetPath, candidatePath) || strings.HasPrefix(candidatePath, targetPath)
}
func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
if sanitizeSyncSourceMode(u.syncSourceMode) != syncSourceRemote {
return nil
@@ -2346,7 +2381,7 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
matches = append(matches, entry)
}
for _, entry := range entries {
if normalizeRemoteCredentialURL(entry.URL) != baseURL {
if !remoteCredentialURLMatches(entry.URL, baseURL) {
continue
}
appendMatch(entry)
@@ -2364,7 +2399,7 @@ func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
if !ok {
continue
}
if normalizeRemoteCredentialURL(profile.BaseURL) != baseURL {
if !remoteCredentialURLMatches(profile.BaseURL, baseURL) {
continue
}
if remotePath != "" && strings.TrimSpace(profile.Path) != remotePath && strings.TrimSpace(record.Path) != remotePath {
@@ -2729,6 +2764,23 @@ func (u *ui) remoteSyncSetupShortcutLabel() string {
return "Set Up Remote Sync"
}
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
}
func remoteBindingSuffix(baseURL, path, username string) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
return hex.EncodeToString(sum[:8])
@@ -5152,7 +5204,7 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
if len(u.syncRemoteCredentialClicks) < len(matchingCredentials) {
u.syncRemoteCredentialClicks = make([]widget.Clickable, len(matchingCredentials))
}
return material.List(u.theme, &u.lifecycleList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
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())
@@ -5628,6 +5680,38 @@ func (u *ui) syncMenu(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 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 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 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 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),
@@ -6986,43 +7070,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
return children
}()...)
}
if !u.shouldShowDirectRemoteSyncShortcut() && !u.shouldShowRemoteSyncSetupShortcut() && !u.shouldShowRemoteSyncSettingsShortcut() && !u.shouldShowRemoveRemoteSyncShortcut() {
return crumbBar(gtx)
}
children := []layout.FlexChild{
layout.Rigid(crumbBar),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
}
if u.shouldShowDirectRemoteSyncShortcut() {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
}))
}
if u.shouldShowRemoteSyncSetupShortcut() {
if u.shouldShowDirectRemoteSyncShortcut() {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel())
}))
}
if u.shouldShowRemoteSyncSettingsShortcut() {
if u.shouldShowDirectRemoteSyncShortcut() || u.shouldShowRemoteSyncSetupShortcut() {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSettingsShortcutLabel())
}))
}
if u.shouldShowRemoveRemoteSyncShortcut() {
if u.shouldShowDirectRemoteSyncShortcut() || u.shouldShowRemoteSyncSetupShortcut() || u.shouldShowRemoteSyncSettingsShortcut() {
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
}
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.removeSelectedRemoteBinding, u.removeRemoteSyncShortcutLabel())
}))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
return crumbBar(gtx)
}
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
+132 -1
View File
@@ -746,6 +746,20 @@ func TestUILifecycleControlsWithSelectedRecentVaultDoesNotPanic(t *testing.T) {
_ = u.lifecycleControls(gtx)
}
func TestUIShouldPrioritizeLifecyclePrimaryActionsOnPhone(t *testing.T) {
t.Parallel()
phone := newUIWithSession("phone", &session.Manager{})
if !phone.shouldPrioritizeLifecyclePrimaryActions() {
t.Fatal("phone.shouldPrioritizeLifecyclePrimaryActions() = false, want true")
}
desktop := newUIWithSession("desktop", &session.Manager{})
if desktop.shouldPrioritizeLifecyclePrimaryActions() {
t.Fatal("desktop.shouldPrioritizeLifecyclePrimaryActions() = true, want false")
}
}
func TestUIRecentVaultListWithSelectedRecentVaultDoesNotPanic(t *testing.T) {
t.Parallel()
@@ -5953,6 +5967,55 @@ func TestUIRemoteSetupShortcutHasParityAcrossModes(t *testing.T) {
}
}
func TestUISyncMenuActionLabelsIncludeRemoteSetupForUnboundVault(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{{
ID: "entry-1",
Title: "Mint Console",
Path: []string{"Crew", "Signals"},
}},
})
u.state.Section = appstate.SectionEntries
got := u.syncMenuActionLabels()
if !slices.Contains(got, "Set Up Remote Sync") {
t.Fatalf("syncMenuActionLabels() = %v, want Set Up Remote Sync", got)
}
if slices.Contains(got, "Use Remote Sync") {
t.Fatalf("syncMenuActionLabels() = %v, want no Use Remote Sync without saved binding", got)
}
}
func TestUISyncMenuActionLabelsIncludeSavedRemoteActionsForBoundVault(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{{
ID: "remote-creds-1",
Title: "Bellagio WebDAV Sign-In",
Username: "linuscaldwell",
Path: []string{"Crew", "Signals"},
}},
RemoteProfiles: []vault.RemoteProfile{{
ID: "bellagio-webdav",
Name: "Bellagio Vault",
Backend: vault.RemoteBackendWebDAV,
BaseURL: "https://dav.example.invalid/remote.php/dav",
Path: "files/bellagio/keepass.kdbx",
}},
})
u.state.Section = appstate.SectionEntries
got := u.syncMenuActionLabels()
for _, want := range []string{"Use Remote Sync", "Remote Sync Settings", "Stop Using Remote Sync"} {
if !slices.Contains(got, want) {
t.Fatalf("syncMenuActionLabels() = %v, want %q", got, want)
}
}
}
func TestUIShouldHideRemoteSyncSetupShortcutWhenSavedBindingExists(t *testing.T) {
t.Parallel()
@@ -5991,7 +6054,13 @@ func TestUIRemoteSyncSetupShortcutLabelUsesClearLanguage(t *testing.T) {
func TestUILifecycleRemoteSyncActionLabelUsesSetupLanguageWithoutSavedBinding(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
dir := t.TempDir()
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
})
u.vaultPath.SetText("/vaults/bellagio.kdbx")
if !u.shouldShowLifecycleRemoteSyncAction() {
@@ -6732,6 +6801,68 @@ func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesBaseURL(t *testing.T)
}
}
func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesMatchingHost(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Mint WebDAV", Username: "charliecroker", URL: "https://dav.example.invalid", Path: []string{"Crew", "Signals"}},
{ID: "entry-2", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", URL: "https://dav.example.invalid/remote.php/dav", Path: []string{"Crew", "Signals"}},
{ID: "entry-3", Title: "Bank Console", Username: "stevefrezelli", URL: "https://insidejob.example.invalid", Path: []string{"Crew", "Signals"}},
},
})
u.syncSourceMode = syncSourceRemote
u.syncRemoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
got := u.matchingAdvancedSyncRemoteCredentialEntries()
if len(got) != 2 {
t.Fatalf("len(matchingAdvancedSyncRemoteCredentialEntries()) = %d, want 2", len(got))
}
gotIDs := []string{got[0].ID, got[1].ID}
slices.Sort(gotIDs)
if !slices.Equal(gotIDs, []string{"entry-1", "entry-2"}) {
t.Fatalf("matchingAdvancedSyncRemoteCredentialEntries() ids = %v, want [entry-1 entry-2]", gotIDs)
}
}
func TestUIRemoteSyncSetupMatchingCredentialsUsesMatchingHost(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Mint WebDAV", Username: "charliecroker", URL: "https://dav.example.invalid", Path: []string{"Crew", "Signals"}},
{ID: "entry-2", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", URL: "https://dav.example.invalid/remote.php/dav", Path: []string{"Crew", "Signals"}},
},
})
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
u.syncSourceMode = syncSourceRemote
u.syncDirection = syncDirectionPush
u.syncRemoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
got := u.matchingAdvancedSyncRemoteCredentialEntries()
if len(got) != 2 {
t.Fatalf("len(matchingAdvancedSyncRemoteCredentialEntries()) = %d, want 2 in remote setup flow", len(got))
}
gotIDs := []string{got[0].ID, got[1].ID}
slices.Sort(gotIDs)
if !slices.Equal(gotIDs, []string{"entry-1", "entry-2"}) {
t.Fatalf("matchingAdvancedSyncRemoteCredentialEntries() ids = %v, want [entry-1 entry-2] in remote setup flow", gotIDs)
}
}
func TestUIOpenRemoteSyncSetupDialogResetsDialogScrollPosition(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{})
u.syncDialogList.Position = layout.Position{First: 2, Offset: 48, BeforeEnd: true}
u.openRemoteSyncSetupDialog()
if got := u.syncDialogList.Position; got != (layout.Position{}) {
t.Fatalf("syncDialogList.Position = %#v, want zero position after opening setup dialog", got)
}
}
func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesSavedBindingForCurrentVault(t *testing.T) {
t.Parallel()
+102 -92
View File
@@ -22,6 +22,92 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
busy := u.lifecycleBusy()
showLocalChooser := u.showLocalVaultChooser()
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
advancedSection := func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(u.lifecycleAdvancedDisclosure),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleAdvancedHidden {
return layout.Dimensions{}
}
if u.lifecycleMode == "remote" {
return 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 {
lbl := material.Label(u.theme, unit.Sp(13), "Vault settings")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
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), u.lifecycleSecuritySettingsSummary())
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
}),
)
})
}),
)
}
primaryActionsSection := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := "Open Vault"
if busy {
label = "Opening Vault..."
}
if busy {
return passiveTonedButton(gtx, u.theme, label)
}
return tonedButton(gtx, u.theme, &u.openVault, label)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
return layout.Dimensions{}
}
return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel())
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return passiveSectionTab(gtx, u.theme, "Create New Vault", false)
}
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
}),
)
}
selectedVaultSection := func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
}),
)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(12), "OPEN A VAULT")
@@ -119,106 +205,30 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}
return keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return u.lifecycleAdvancedDisclosure(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || u.lifecycleAdvancedHidden {
return layout.Dimensions{}
}
if u.lifecycleMode == "remote" {
return 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 {
lbl := material.Label(u.theme, unit.Sp(13), "Vault settings")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
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), u.lifecycleSecuritySettingsSummary())
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.shouldPrioritizeLifecyclePrimaryActions() {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(primaryActionsSection),
layout.Rigid(selectedVaultSection),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(advancedSection),
)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := "Open Vault"
if busy {
label = "Opening Vault..."
}
if busy {
return passiveTonedButton(gtx, u.theme, label)
}
return tonedButton(gtx, u.theme, &u.openVault, label)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || !u.shouldShowLifecycleRemoteSyncAction() {
return layout.Dimensions{}
}
return tonedButton(gtx, u.theme, &u.lifecycleRemoteSyncAction, u.lifecycleRemoteSyncActionLabel())
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(11), "Need a fresh database instead?")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return passiveSectionTab(gtx, u.theme, "Create New Vault", false)
}
return sectionTabButton(gtx, u.theme, &u.createVault, "Create New Vault", false)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy || selectedLocalPath == "" {
return layout.Dimensions{}
}
return u.selectedLocalVaultCard(gtx, selectedLocalPath)
}),
layout.Rigid(advancedSection),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(primaryActionsSection),
layout.Rigid(selectedVaultSection),
)
}),
)
}
func (u *ui) shouldPrioritizeLifecyclePrimaryActions() bool {
return u.mode == "phone"
}
func (u *ui) selectedRemoteConnectionCard(gtx layout.Context) layout.Dimensions {
heading := u.selectedRemoteCardHeading()
primary := u.selectedRemoteCardPrimaryText()