Remember recent remote vault connections
This commit is contained in:
@@ -78,6 +78,7 @@ type attachmentItem struct {
|
||||
type statePaths struct {
|
||||
DefaultSaveAsPath string
|
||||
RecentVaultsPath string
|
||||
RecentRemotesPath string
|
||||
}
|
||||
|
||||
type recentVaultRecord struct {
|
||||
@@ -85,6 +86,13 @@ type recentVaultRecord struct {
|
||||
LastGroup []string `json:"lastGroup,omitempty"`
|
||||
}
|
||||
|
||||
type recentRemoteRecord struct {
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Path string `json:"path"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type ui struct {
|
||||
mode string
|
||||
theme *material.Theme
|
||||
@@ -159,12 +167,14 @@ type ui struct {
|
||||
showRecycle widget.Clickable
|
||||
showLocalLifecycle widget.Clickable
|
||||
showRemoteLifecycle widget.Clickable
|
||||
rememberRemoteAuth widget.Bool
|
||||
entryClicks []widget.Clickable
|
||||
historyClicks []widget.Clickable
|
||||
attachmentClicks []widget.Clickable
|
||||
breadcrumbs []widget.Clickable
|
||||
groupClicks []widget.Clickable
|
||||
recentVaultClicks []widget.Clickable
|
||||
recentRemoteClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
@@ -189,9 +199,11 @@ type ui struct {
|
||||
keyboardFocus focusID
|
||||
defaultSaveAsPath string
|
||||
recentVaultsPath string
|
||||
recentRemotesPath string
|
||||
editingEntry bool
|
||||
groupControlsHidden bool
|
||||
recentVaults []string
|
||||
recentRemotes []recentRemoteRecord
|
||||
recentVaultGroups map[string][]string
|
||||
deleteGroupPath []string
|
||||
statusExpiresAt time.Time
|
||||
@@ -276,6 +288,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
lifecycleMode: "local",
|
||||
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
||||
recentVaultsPath: paths.RecentVaultsPath,
|
||||
recentRemotesPath: paths.RecentRemotesPath,
|
||||
recentVaultGroups: map[string][]string{},
|
||||
now: time.Now,
|
||||
}
|
||||
@@ -290,6 +303,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
u.keyboardFocus = focusSearch
|
||||
u.setCustomFieldRows(nil)
|
||||
u.loadRecentVaults()
|
||||
u.loadRecentRemotes()
|
||||
u.filter()
|
||||
return u
|
||||
}
|
||||
@@ -322,6 +336,7 @@ func defaultStatePaths(stateDir string) statePaths {
|
||||
return statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +566,13 @@ func (u *ui) openRemoteAction() error {
|
||||
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
||||
return err
|
||||
}
|
||||
u.noteRecentRemote(
|
||||
strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
strings.TrimSpace(u.remotePath.Text()),
|
||||
strings.TrimSpace(u.remoteUsername.Text()),
|
||||
u.remotePassword.Text(),
|
||||
u.rememberRemoteAuth.Value,
|
||||
)
|
||||
u.enterHiddenVaultRoot()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
@@ -695,6 +717,42 @@ func (u *ui) applyRecentVaultRecords(records []recentVaultRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) loadRecentRemotes() {
|
||||
if strings.TrimSpace(u.recentRemotesPath) == "" {
|
||||
return
|
||||
}
|
||||
content, err := os.ReadFile(u.recentRemotesPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var records []recentRemoteRecord
|
||||
if err := json.Unmarshal(content, &records); err != nil {
|
||||
return
|
||||
}
|
||||
filtered := make([]recentRemoteRecord, 0, len(records))
|
||||
seen := map[string]bool{}
|
||||
for _, record := range records {
|
||||
record.BaseURL = strings.TrimSpace(record.BaseURL)
|
||||
record.Path = strings.TrimSpace(record.Path)
|
||||
if record.BaseURL == "" || record.Path == "" {
|
||||
continue
|
||||
}
|
||||
key := record.BaseURL + "|" + record.Path
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
filtered = append(filtered, record)
|
||||
if len(filtered) == 6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
u.recentRemotes = filtered
|
||||
if len(u.recentRemoteClicks) < len(u.recentRemotes) {
|
||||
u.recentRemoteClicks = make([]widget.Clickable, len(u.recentRemotes))
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) saveRecentVaults() {
|
||||
if strings.TrimSpace(u.recentVaultsPath) == "" {
|
||||
return
|
||||
@@ -716,6 +774,51 @@ func (u *ui) saveRecentVaults() {
|
||||
_ = os.WriteFile(u.recentVaultsPath, content, 0o600)
|
||||
}
|
||||
|
||||
func (u *ui) saveRecentRemotes() {
|
||||
if strings.TrimSpace(u.recentRemotesPath) == "" {
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(u.recentRemotesPath), 0o700); err != nil {
|
||||
return
|
||||
}
|
||||
content, err := json.MarshalIndent(u.recentRemotes, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(u.recentRemotesPath, content, 0o600)
|
||||
}
|
||||
|
||||
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
|
||||
baseURL = strings.TrimSpace(baseURL)
|
||||
path = strings.TrimSpace(path)
|
||||
if baseURL == "" || path == "" {
|
||||
return
|
||||
}
|
||||
record := recentRemoteRecord{
|
||||
BaseURL: baseURL,
|
||||
Path: path,
|
||||
}
|
||||
if rememberAuth {
|
||||
record.Username = strings.TrimSpace(username)
|
||||
record.Password = password
|
||||
}
|
||||
next := []recentRemoteRecord{record}
|
||||
for _, existing := range u.recentRemotes {
|
||||
if existing.BaseURL == baseURL && existing.Path == path {
|
||||
continue
|
||||
}
|
||||
next = append(next, existing)
|
||||
if len(next) == 6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
u.recentRemotes = next
|
||||
if len(u.recentRemoteClicks) < len(u.recentRemotes) {
|
||||
u.recentRemoteClicks = make([]widget.Clickable, len(u.recentRemotes))
|
||||
}
|
||||
u.saveRecentRemotes()
|
||||
}
|
||||
|
||||
func (u *ui) recentVaultGroup(path string) []string {
|
||||
if u.recentVaultGroups == nil {
|
||||
return nil
|
||||
@@ -1128,6 +1231,18 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range u.recentRemoteClicks {
|
||||
for u.recentRemoteClicks[i].Clicked(gtx) {
|
||||
if i < len(u.recentRemotes) {
|
||||
record := u.recentRemotes[i]
|
||||
u.remoteBaseURL.SetText(record.BaseURL)
|
||||
u.remotePath.SetText(record.Path)
|
||||
u.remoteUsername.SetText(record.Username)
|
||||
u.remotePassword.SetText(record.Password)
|
||||
u.rememberRemoteAuth.Value = strings.TrimSpace(record.Username) != "" || record.Password != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for u.addEntry.Clicked(gtx) {
|
||||
u.state.BeginNewEntry()
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
|
||||
@@ -1964,6 +1964,34 @@ func TestUIOpenVaultRestoresLastOpenedGroupForThatVault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIRecentRemoteConnectionsPersistAndReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configPath := filepath.Join(t.TempDir(), "recent-remotes.json")
|
||||
|
||||
first := newUIWithSession("desktop", &session.Manager{})
|
||||
first.recentRemotesPath = configPath
|
||||
first.recentRemotes = nil
|
||||
first.noteRecentRemote("https://dav.example.com", "vaults/home.kdbx", "alice", "secret-1", true)
|
||||
first.noteRecentRemote("https://dav.example.com", "vaults/team.kdbx", "bob", "secret-2", false)
|
||||
first.noteRecentRemote("https://dav.example.com", "vaults/home.kdbx", "alice", "secret-3", true)
|
||||
|
||||
second := newUIWithSession("desktop", &session.Manager{})
|
||||
second.recentRemotesPath = configPath
|
||||
second.recentRemotes = nil
|
||||
second.loadRecentRemotes()
|
||||
|
||||
if got := len(second.recentRemotes); got != 2 {
|
||||
t.Fatalf("len(recentRemotes) = %d, want 2", got)
|
||||
}
|
||||
if got := second.recentRemotes[0]; got.BaseURL != "https://dav.example.com" || got.Path != "vaults/home.kdbx" || got.Username != "alice" || got.Password != "secret-3" {
|
||||
t.Fatalf("recentRemotes[0] = %#v, want updated remembered credentials", got)
|
||||
}
|
||||
if got := second.recentRemotes[1]; got.Username != "" || got.Password != "" {
|
||||
t.Fatalf("recentRemotes[1] = %#v, want credentials omitted when remember disabled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1976,6 +2004,9 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
||||
if got := paths.RecentVaultsPath; got != filepath.Join(base, "recent-vaults.json") {
|
||||
t.Fatalf("RecentVaultsPath = %q, want %q", got, filepath.Join(base, "recent-vaults.json"))
|
||||
}
|
||||
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.json") {
|
||||
t.Fatalf("RecentRemotesPath = %q, want %q", got, filepath.Join(base, "recent-remotes.json"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) {
|
||||
@@ -1990,6 +2021,9 @@ func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) {
|
||||
if got := paths.RecentVaultsPath; got != filepath.Join(base, "recent-vaults.json") {
|
||||
t.Fatalf("RecentVaultsPath = %q, want %q", got, filepath.Join(base, "recent-vaults.json"))
|
||||
}
|
||||
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.json") {
|
||||
t.Fatalf("RecentRemotesPath = %q, want %q", got, filepath.Join(base, "recent-remotes.json"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveFlagOrEnvPrefersFlagThenEnvThenFallback(t *testing.T) {
|
||||
|
||||
+41
@@ -40,6 +40,14 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
box := material.CheckBox(u.theme, &u.rememberRemoteAuth, "Remember username and password")
|
||||
box.Color = accentColor
|
||||
return box.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(u.recentRemoteList),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
@@ -99,6 +107,39 @@ func (u *ui) recentVaultList(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) recentRemoteList(gtx layout.Context) layout.Dimensions {
|
||||
if len(u.recentRemotes) == 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if len(u.recentRemoteClicks) < len(u.recentRemotes) {
|
||||
u.recentRemoteClicks = make([]widget.Clickable, len(u.recentRemotes))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(12), "RECENT CONNECTIONS")
|
||||
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 layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild {
|
||||
children := make([]layout.FlexChild, 0, len(u.recentRemotes)*2)
|
||||
for i, record := range u.recentRemotes {
|
||||
index := i
|
||||
label := record.BaseURL + " / " + record.Path
|
||||
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.recentRemoteClicks[index], label)
|
||||
}))
|
||||
if i < len(u.recentRemotes)-1 {
|
||||
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||
}
|
||||
}
|
||||
return children
|
||||
}()...)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *ui) attachmentList(gtx layout.Context) layout.Dimensions {
|
||||
items := u.selectedAttachmentItems()
|
||||
if len(items) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user