Unify open flow around local vaults
This commit is contained in:
@@ -217,6 +217,13 @@ const (
|
|||||||
syncDirectionPush syncDirection = "push"
|
syncDirectionPush syncDirection = "push"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type syncDialogPurpose string
|
||||||
|
|
||||||
|
const (
|
||||||
|
syncDialogPurposeAdvanced syncDialogPurpose = "advanced"
|
||||||
|
syncDialogPurposeRemoteSetup syncDialogPurpose = "remote-setup"
|
||||||
|
)
|
||||||
|
|
||||||
type ui struct {
|
type ui struct {
|
||||||
mode string
|
mode string
|
||||||
theme *material.Theme
|
theme *material.Theme
|
||||||
@@ -286,6 +293,7 @@ type ui struct {
|
|||||||
toggleSyncMenu widget.Clickable
|
toggleSyncMenu widget.Clickable
|
||||||
toggleMainMenu widget.Clickable
|
toggleMainMenu widget.Clickable
|
||||||
openAdvancedSync widget.Clickable
|
openAdvancedSync widget.Clickable
|
||||||
|
useSavedAdvancedSyncRemote widget.Clickable
|
||||||
openSelectedVaultRemote widget.Clickable
|
openSelectedVaultRemote widget.Clickable
|
||||||
saveCurrentRemoteBinding widget.Clickable
|
saveCurrentRemoteBinding widget.Clickable
|
||||||
openSecuritySettings widget.Clickable
|
openSecuritySettings widget.Clickable
|
||||||
@@ -446,6 +454,7 @@ type ui struct {
|
|||||||
syncRemoteUsername widget.Editor
|
syncRemoteUsername widget.Editor
|
||||||
syncRemotePassword widget.Editor
|
syncRemotePassword widget.Editor
|
||||||
selectedSyncRemoteCredentialEntryID string
|
selectedSyncRemoteCredentialEntryID string
|
||||||
|
syncDialogPurpose syncDialogPurpose
|
||||||
syncDialogOpen bool
|
syncDialogOpen bool
|
||||||
syncMenuOpen bool
|
syncMenuOpen bool
|
||||||
mainMenuOpen bool
|
mainMenuOpen bool
|
||||||
@@ -1361,11 +1370,28 @@ func (u *ui) openAdvancedSyncDialog() {
|
|||||||
u.syncDialogOpen = true
|
u.syncDialogOpen = true
|
||||||
u.syncMenuOpen = false
|
u.syncMenuOpen = false
|
||||||
u.showSyncPassword = false
|
u.showSyncPassword = false
|
||||||
|
u.syncDialogPurpose = syncDialogPurposeAdvanced
|
||||||
u.syncSourceMode = u.syncDefaultSourceMode
|
u.syncSourceMode = u.syncDefaultSourceMode
|
||||||
u.syncDirection = u.syncDefaultDirection
|
u.syncDirection = u.syncDefaultDirection
|
||||||
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||||
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||||
}
|
}
|
||||||
|
u.syncSavedRemoteBindingSelection()
|
||||||
|
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) openRemoteSyncSetupDialog() {
|
||||||
|
u.syncDialogOpen = true
|
||||||
|
u.syncMenuOpen = false
|
||||||
|
u.showSyncPassword = false
|
||||||
|
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
|
||||||
|
u.syncSourceMode = syncSourceRemote
|
||||||
|
u.syncDirection = syncDirectionPush
|
||||||
|
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||||
|
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||||
|
}
|
||||||
|
u.syncSavedRemoteBindingSelection()
|
||||||
|
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) clearSyncLocalImport() {
|
func (u *ui) clearSyncLocalImport() {
|
||||||
@@ -2012,12 +2038,15 @@ func (u *ui) restoreStartupLifecycleTarget() {
|
|||||||
remoteRecord, hasRemote, remoteUsedAt := u.latestRecentRemote()
|
remoteRecord, hasRemote, remoteUsedAt := u.latestRecentRemote()
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case hasRemote && (localPath == "" || remoteUsedAt.After(localUsedAt)):
|
case hasRemote && strings.TrimSpace(remoteRecord.LocalVaultPath) != "" && (localPath == "" || remoteUsedAt.After(localUsedAt)):
|
||||||
u.lifecycleMode = "remote"
|
u.lifecycleMode = "local"
|
||||||
u.applyRecentRemoteRecord(remoteRecord)
|
u.vaultPath.SetText(strings.TrimSpace(remoteRecord.LocalVaultPath))
|
||||||
case localPath != "":
|
case localPath != "":
|
||||||
u.lifecycleMode = "local"
|
u.lifecycleMode = "local"
|
||||||
u.vaultPath.SetText(localPath)
|
u.vaultPath.SetText(localPath)
|
||||||
|
case hasRemote:
|
||||||
|
u.lifecycleMode = "remote"
|
||||||
|
u.applyRecentRemoteRecord(remoteRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2251,6 +2280,27 @@ func (u *ui) applyAdvancedSyncRemoteCredentialEntry(entry vault.Entry) {
|
|||||||
u.syncRemotePassword.SetText(entry.Password)
|
u.syncRemotePassword.SetText(entry.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) savedAdvancedSyncRemoteBinding() (appstate.ResolvedRemoteBinding, bool) {
|
||||||
|
if !u.hasOpenVault() {
|
||||||
|
return appstate.ResolvedRemoteBinding{}, false
|
||||||
|
}
|
||||||
|
_, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding()
|
||||||
|
if err != nil || !ok {
|
||||||
|
return appstate.ResolvedRemoteBinding{}, false
|
||||||
|
}
|
||||||
|
return resolved, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) prefillAdvancedSyncRemoteFromSavedBinding() {
|
||||||
|
resolved, ok := u.savedAdvancedSyncRemoteBinding()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.syncRemoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||||
|
u.syncRemotePath.SetText(resolved.Profile.Path)
|
||||||
|
u.applyAdvancedSyncRemoteCredentialEntry(resolved.Credentials)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) selectVaultRemoteProfile(id string) {
|
func (u *ui) selectVaultRemoteProfile(id string) {
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
u.selectedVaultRemoteProfileID = id
|
u.selectedVaultRemoteProfileID = id
|
||||||
@@ -2484,6 +2534,18 @@ func (u *ui) directRemoteSyncShortcutLabel() string {
|
|||||||
return "Use Remote Sync"
|
return "Use Remote Sync"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) shouldShowRemoteSyncSetupShortcut() bool {
|
||||||
|
if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := u.selectedVaultRemoteBinding()
|
||||||
|
return !ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) remoteSyncSetupShortcutLabel() string {
|
||||||
|
return "Set Up Remote Sync"
|
||||||
|
}
|
||||||
|
|
||||||
func remoteBindingSuffix(baseURL, path, username string) string {
|
func remoteBindingSuffix(baseURL, path, username string) string {
|
||||||
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
|
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
|
||||||
return hex.EncodeToString(sum[:8])
|
return hex.EncodeToString(sum[:8])
|
||||||
@@ -4105,6 +4167,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for u.useSavedAdvancedSyncRemote.Clicked(gtx) {
|
||||||
|
u.openRemoteSyncSetupDialog()
|
||||||
|
}
|
||||||
for u.openSelectedVaultRemote.Clicked(gtx) {
|
for u.openSelectedVaultRemote.Clicked(gtx) {
|
||||||
if u.lifecycleBusy() {
|
if u.lifecycleBusy() {
|
||||||
continue
|
continue
|
||||||
@@ -6646,16 +6711,27 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
|
|||||||
return children
|
return children
|
||||||
}()...)
|
}()...)
|
||||||
}
|
}
|
||||||
if !u.shouldShowDirectRemoteSyncShortcut() {
|
if !u.shouldShowDirectRemoteSyncShortcut() && !u.shouldShowRemoteSyncSetupShortcut() {
|
||||||
return crumbBar(gtx)
|
return crumbBar(gtx)
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
children := []layout.FlexChild{
|
||||||
layout.Rigid(crumbBar),
|
layout.Rigid(crumbBar),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
}
|
||||||
|
if u.shouldShowDirectRemoteSyncShortcut() {
|
||||||
|
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel())
|
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())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
|
func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) {
|
||||||
|
|||||||
+143
-7
@@ -4934,14 +4934,11 @@ func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) {
|
|||||||
|
|
||||||
second := newUIWithSession("desktop", &session.Manager{}, paths)
|
second := newUIWithSession("desktop", &session.Manager{}, paths)
|
||||||
|
|
||||||
if got := second.lifecycleMode; got != "remote" {
|
if got := second.lifecycleMode; got != "local" {
|
||||||
t.Fatalf("lifecycleMode = %q, want remote", got)
|
t.Fatalf("lifecycleMode = %q, want local", got)
|
||||||
}
|
}
|
||||||
if got := second.remoteBaseURL.Text(); got != "https://dav.example.com" {
|
if got := second.vaultPath.Text(); got != "/tmp/local.kdbx" {
|
||||||
t.Fatalf("remoteBaseURL = %q, want https://dav.example.com", got)
|
t.Fatalf("vaultPath = %q, want /tmp/local.kdbx", got)
|
||||||
}
|
|
||||||
if got := second.remotePath.Text(); got != "vaults/home.kdbx" {
|
|
||||||
t.Fatalf("remotePath = %q, want vaults/home.kdbx", got)
|
|
||||||
}
|
}
|
||||||
if got := second.remoteUsername.Text(); got != "" {
|
if got := second.remoteUsername.Text(); got != "" {
|
||||||
t.Fatalf("remoteUsername = %q, want empty for location-only recent remote", got)
|
t.Fatalf("remoteUsername = %q, want empty for location-only recent remote", got)
|
||||||
@@ -5422,6 +5419,31 @@ func TestRestoreStartupLifecycleTargetSelectsMostRecentLocalVault(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRestoreStartupLifecycleTargetUsesLocalCacheFromRecentRemote(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.lifecycleMode = "remote"
|
||||||
|
u.vaultPath.SetText("")
|
||||||
|
u.recentVaults = []string{"/tmp/older.kdbx"}
|
||||||
|
u.recentVaultUsedAt["/tmp/older.kdbx"] = time.Date(2026, time.April, 5, 1, 2, 3, 0, time.UTC)
|
||||||
|
u.recentRemotes = []recentRemoteRecord{{
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
Path: "files/family/keepass.kdbx",
|
||||||
|
LocalVaultPath: "/tmp/family-cache.kdbx",
|
||||||
|
UsedAt: time.Date(2026, time.April, 5, 2, 2, 3, 0, time.UTC).Format(time.RFC3339Nano),
|
||||||
|
}}
|
||||||
|
|
||||||
|
u.restoreStartupLifecycleTarget()
|
||||||
|
|
||||||
|
if got := u.lifecycleMode; got != "local" {
|
||||||
|
t.Fatalf("lifecycleMode after restore = %q, want local", got)
|
||||||
|
}
|
||||||
|
if got := u.vaultPath.Text(); got != "/tmp/family-cache.kdbx" {
|
||||||
|
t.Fatalf("vaultPath after restore = %q, want /tmp/family-cache.kdbx", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestShowLocalVaultChooser(t *testing.T) {
|
func TestShowLocalVaultChooser(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -5795,6 +5817,120 @@ func TestUIDirectRemoteSyncShortcutLabelUsesSyncLanguage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIShouldShowRemoteSyncSetupShortcutForOpenedLocalVaultWithoutSavedBinding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{
|
||||||
|
Entries: []vault.Entry{{
|
||||||
|
ID: "entry-1",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Path: []string{"Crew", "Internet"},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
u.state.Section = appstate.SectionEntries
|
||||||
|
|
||||||
|
if !u.shouldShowRemoteSyncSetupShortcut() {
|
||||||
|
t.Fatal("shouldShowRemoteSyncSetupShortcut() = false, want true for opened local vault without saved binding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIShouldHideRemoteSyncSetupShortcutWhenSavedBindingExists(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{
|
||||||
|
Entries: []vault.Entry{{
|
||||||
|
ID: "remote-creds-1",
|
||||||
|
Title: "WebDAV Sign-In",
|
||||||
|
Username: "tjulian",
|
||||||
|
Path: []string{"Crew", "Internet"},
|
||||||
|
}},
|
||||||
|
RemoteProfiles: []vault.RemoteProfile{{
|
||||||
|
ID: "family-webdav",
|
||||||
|
Name: "Family Vault",
|
||||||
|
Backend: vault.RemoteBackendWebDAV,
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
Path: "files/family/keepass.kdbx",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
u.state.Section = appstate.SectionEntries
|
||||||
|
|
||||||
|
if u.shouldShowRemoteSyncSetupShortcut() {
|
||||||
|
t.Fatal("shouldShowRemoteSyncSetupShortcut() = true, want false when saved binding already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIRemoteSyncSetupShortcutLabelUsesClearLanguage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
|
||||||
|
if got := u.remoteSyncSetupShortcutLabel(); got != "Set Up Remote Sync" {
|
||||||
|
t.Fatalf("remoteSyncSetupShortcutLabel() = %q, want Set Up Remote Sync", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIOpenRemoteSyncSetupDialogPrefillsCurrentVaultSetupFlow(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{
|
||||||
|
Entries: []vault.Entry{{
|
||||||
|
ID: "remote-creds-1",
|
||||||
|
Title: "WebDAV Sign-In",
|
||||||
|
Username: "tjulian",
|
||||||
|
Password: "token-1",
|
||||||
|
Path: []string{"Crew", "Internet"},
|
||||||
|
}},
|
||||||
|
RemoteProfiles: []vault.RemoteProfile{{
|
||||||
|
ID: "family-webdav",
|
||||||
|
Name: "Family Vault",
|
||||||
|
Backend: vault.RemoteBackendWebDAV,
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
Path: "files/family/keepass.kdbx",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
u.vaultPath.SetText("/vaults/family.kdbx")
|
||||||
|
|
||||||
|
u.openRemoteSyncSetupDialog()
|
||||||
|
|
||||||
|
if !u.syncDialogOpen {
|
||||||
|
t.Fatal("syncDialogOpen = false, want true")
|
||||||
|
}
|
||||||
|
if got := u.syncDialogPurpose; got != syncDialogPurposeRemoteSetup {
|
||||||
|
t.Fatalf("syncDialogPurpose = %q, want remote setup", got)
|
||||||
|
}
|
||||||
|
if got := u.syncSourceMode; got != syncSourceRemote {
|
||||||
|
t.Fatalf("syncSourceMode = %q, want remote", got)
|
||||||
|
}
|
||||||
|
if got := u.syncDirection; got != syncDirectionPush {
|
||||||
|
t.Fatalf("syncDirection = %q, want push", got)
|
||||||
|
}
|
||||||
|
if got := u.syncLocalPath.Text(); got != "/vaults/family.kdbx" {
|
||||||
|
t.Fatalf("syncLocalPath = %q, want current vault path", got)
|
||||||
|
}
|
||||||
|
if got := u.syncRemoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" {
|
||||||
|
t.Fatalf("syncRemoteBaseURL = %q, want saved remote base URL", got)
|
||||||
|
}
|
||||||
|
if got := u.syncRemotePath.Text(); got != "files/family/keepass.kdbx" {
|
||||||
|
t.Fatalf("syncRemotePath = %q, want saved remote path", got)
|
||||||
|
}
|
||||||
|
if got := u.syncRemoteUsername.Text(); got != "tjulian" {
|
||||||
|
t.Fatalf("syncRemoteUsername = %q, want tjulian", got)
|
||||||
|
}
|
||||||
|
if got := u.syncRemotePassword.Text(); got != "token-1" {
|
||||||
|
t.Fatalf("syncRemotePassword = %q, want token-1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUISelectedLocalVaultRemoteSyncSummaryMentionsSetup(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
|
||||||
|
if got := u.selectedLocalVaultRemoteSyncSummary("/vaults/family.kdbx"); got != "Open this vault to set up a WebDAV sync target for it." {
|
||||||
|
t.Fatalf("selectedLocalVaultRemoteSyncSummary() = %q, want setup guidance", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) {
|
func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+13
-184
@@ -21,7 +21,6 @@ import (
|
|||||||
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||||
busy := u.lifecycleBusy()
|
busy := u.lifecycleBusy()
|
||||||
showLocalChooser := u.showLocalVaultChooser()
|
showLocalChooser := u.showLocalVaultChooser()
|
||||||
showRemoteChooser := u.showRemoteConnectionChooser()
|
|
||||||
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
|
selectedLocalPath := strings.TrimSpace(u.vaultPath.Text())
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -31,172 +30,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
message := "Choose a recent vault or enter a .kdbx path, then unlock it."
|
message := "Choose a recent vault or enter a .kdbx path, then unlock it. Remote sync attaches to that local vault after it opens."
|
||||||
if u.lifecycleMode == "remote" {
|
|
||||||
message = u.remoteLifecycleMessage()
|
|
||||||
}
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), message)
|
lbl := material.Label(u.theme, unit.Sp(14), message)
|
||||||
lbl.Color = accentColor
|
lbl.Color = accentColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
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 {
|
|
||||||
if busy {
|
|
||||||
return passiveSectionTab(gtx, u.theme, "Local Vault", u.lifecycleMode == "local")
|
|
||||||
}
|
|
||||||
return sectionTabButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault", u.lifecycleMode == "local")
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if busy {
|
|
||||||
return passiveSectionTab(gtx, u.theme, "Remote Vault", u.lifecycleMode == "remote")
|
|
||||||
}
|
|
||||||
return sectionTabButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault", u.lifecycleMode == "remote")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if u.lifecycleMode == "remote" {
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "LOCATION")
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if showRemoteChooser || !u.hasSelectedRemoteTarget() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if showRemoteChooser && !busy {
|
|
||||||
return u.recentRemoteList(gtx)
|
|
||||||
}
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION")
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help")
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
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), "Local Cache Bootstrap")
|
|
||||||
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.remoteLifecycleSetupSummary())
|
|
||||||
lbl.Color = mutedColor
|
|
||||||
return lbl.Layout(gtx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if !showRemoteChooser {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if !showLocalChooser {
|
if !showLocalChooser {
|
||||||
@@ -326,29 +166,6 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if u.lifecycleMode == "remote" {
|
|
||||||
label := u.remoteOpenButtonLabel()
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if busy {
|
|
||||||
return passiveTonedButton(gtx, u.theme, label)
|
|
||||||
}
|
|
||||||
return tonedButton(gtx, u.theme, &u.openRemote, label)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if busy || !u.hasSelectedRemoteTarget() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
if busy || !u.hasSelectedRemoteTarget() {
|
|
||||||
return layout.Dimensions{}
|
|
||||||
}
|
|
||||||
return u.selectedRemoteConnectionCard(gtx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
label := "Open Vault"
|
label := "Open Vault"
|
||||||
@@ -499,6 +316,11 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime
|
|||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), u.selectedLocalVaultRemoteSyncSummary(path))
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
|
return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault")
|
||||||
@@ -508,6 +330,13 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) selectedLocalVaultRemoteSyncSummary(path string) string {
|
||||||
|
if record, ok := u.boundRecentRemoteForLocalVault(path); ok {
|
||||||
|
return "Saved remote sync target: " + friendlyRecentRemoteLabel(record)
|
||||||
|
}
|
||||||
|
return "Open this vault to set up a WebDAV sync target for it."
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
||||||
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user