Tighten banner and navigation behavior

This commit is contained in:
Joe Julian
2026-03-29 21:16:38 -07:00
parent 62bb20edb0
commit 59905ccd98
4 changed files with 87 additions and 7 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ func (s *State) SetSearchQuery(query string) {
func (s *State) BeginNewEntry() {
s.SelectedEntryID = ""
s.StatusMessage = "new entry form ready"
s.StatusMessage = ""
s.ErrorMessage = ""
}
+3 -3
View File
@@ -922,7 +922,7 @@ func TestSetSearchQueryUpdatesControllerSearchState(t *testing.T) {
}
}
func TestBeginNewEntryClearsSelectionAndSetsStatus(t *testing.T) {
func TestBeginNewEntryClearsSelectionAndStatus(t *testing.T) {
t.Parallel()
state := State{
@@ -935,8 +935,8 @@ func TestBeginNewEntryClearsSelectionAndSetsStatus(t *testing.T) {
if state.SelectedEntryID != "" {
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
}
if state.StatusMessage != "new entry form ready" {
t.Fatalf("StatusMessage = %q, want new entry form ready", state.StatusMessage)
if state.StatusMessage != "" {
t.Fatalf("StatusMessage = %q, want empty", state.StatusMessage)
}
if state.ErrorMessage != "" {
t.Fatalf("ErrorMessage = %q, want empty", state.ErrorMessage)
+54 -3
View File
@@ -41,7 +41,7 @@ const (
const (
maxAttachmentBytes = 10 << 20
statusBannerDuration = 4 * time.Second
statusBannerDuration = 2600 * time.Millisecond
)
type bannerKind string
@@ -79,6 +79,7 @@ type statePaths struct {
DefaultSaveAsPath string
RecentVaultsPath string
RecentRemotesPath string
UIPreferencesPath string
}
type recentVaultRecord struct {
@@ -94,6 +95,10 @@ type recentRemoteRecord struct {
LastGroup []string `json:"lastGroup,omitempty"`
}
type uiPreferences struct {
GroupControlsHidden bool `json:"groupControlsHidden"`
}
type ui struct {
mode string
theme *material.Theme
@@ -201,6 +206,7 @@ type ui struct {
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
uiPreferencesPath string
recentRemotesPath string
editingEntry bool
groupControlsHidden bool
@@ -293,6 +299,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
lifecycleMode: "local",
defaultSaveAsPath: paths.DefaultSaveAsPath,
recentVaultsPath: paths.RecentVaultsPath,
uiPreferencesPath: paths.UIPreferencesPath,
recentRemotesPath: paths.RecentRemotesPath,
recentVaultGroups: map[string][]string{},
now: time.Now,
@@ -309,6 +316,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.setCustomFieldRows(nil)
u.loadRecentVaults()
u.loadRecentRemotes()
u.loadUIPreferences()
u.filter()
return u
}
@@ -342,6 +350,7 @@ func defaultStatePaths(stateDir string) statePaths {
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"),
}
}
@@ -805,6 +814,37 @@ func (u *ui) saveRecentRemotes() {
_ = os.WriteFile(u.recentRemotesPath, content, 0o600)
}
func (u *ui) loadUIPreferences() {
if strings.TrimSpace(u.uiPreferencesPath) == "" {
return
}
content, err := os.ReadFile(u.uiPreferencesPath)
if err != nil {
return
}
var prefs uiPreferences
if err := json.Unmarshal(content, &prefs); err != nil {
return
}
u.groupControlsHidden = prefs.GroupControlsHidden
}
func (u *ui) saveUIPreferences() {
if strings.TrimSpace(u.uiPreferencesPath) == "" {
return
}
if err := os.MkdirAll(filepath.Dir(u.uiPreferencesPath), 0o700); err != nil {
return
}
content, err := json.MarshalIndent(uiPreferences{
GroupControlsHidden: u.groupControlsHidden,
}, "", " ")
if err != nil {
return
}
_ = os.WriteFile(u.uiPreferencesPath, content, 0o600)
}
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
baseURL = strings.TrimSpace(baseURL)
path = strings.TrimSpace(path)
@@ -1398,6 +1438,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
}
for u.toggleGroupControls.Clicked(gtx) {
u.groupControlsHidden = !u.groupControlsHidden
u.saveUIPreferences()
}
for u.renameGroup.Clicked(gtx) {
u.clearDeleteGroupConfirmation()
@@ -1644,11 +1685,21 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
func (u *ui) sectionBar(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.showEntries, "Entries")
btn := material.Button(u.theme, &u.showEntries, "Entries")
btn.Background = accentColor
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
btn.TextSize = unit.Sp(11)
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.showRecycle, "Recycle Bin")
btn := material.Button(u.theme, &u.showRecycle, "Recycle Bin")
btn.Background = accentColor
btn.Color = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
btn.TextSize = unit.Sp(11)
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
return btn.Layout(gtx)
}),
)
}
+29
View File
@@ -1737,6 +1737,9 @@ func TestUIStatusBannerExpiresAfterTimeout(t *testing.T) {
u := newUIWithModel("desktop", vault.Model{})
u.now = func() time.Time { return now }
u.state.StatusMessage = "synchronize vault complete"
if statusBannerDuration != 2600*time.Millisecond {
t.Fatalf("statusBannerDuration = %v, want 2.6s", statusBannerDuration)
}
u.statusExpiresAt = now.Add(statusBannerDuration)
if got := u.bannerSurface(); got.Kind != bannerStatus || got.Message != "synchronize vault complete" {
@@ -2003,6 +2006,26 @@ func TestUIRecentRemoteConnectionsPersistAndReload(t *testing.T) {
}
}
func TestUIGroupToolsDisclosureStatePersists(t *testing.T) {
t.Parallel()
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
first := newUIWithSession("desktop", &session.Manager{})
first.uiPreferencesPath = configPath
first.groupControlsHidden = true
first.saveUIPreferences()
second := newUIWithSession("desktop", &session.Manager{})
second.uiPreferencesPath = configPath
second.groupControlsHidden = false
second.loadUIPreferences()
if !second.groupControlsHidden {
t.Fatal("groupControlsHidden = false after reload, want true")
}
}
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
t.Parallel()
@@ -2107,6 +2130,9 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.json") {
t.Fatalf("RecentRemotesPath = %q, want %q", got, filepath.Join(base, "recent-remotes.json"))
}
if got := paths.UIPreferencesPath; got != filepath.Join(base, "ui-prefs.json") {
t.Fatalf("UIPreferencesPath = %q, want %q", got, filepath.Join(base, "ui-prefs.json"))
}
}
func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) {
@@ -2124,6 +2150,9 @@ func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) {
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.json") {
t.Fatalf("RecentRemotesPath = %q, want %q", got, filepath.Join(base, "recent-remotes.json"))
}
if got := paths.UIPreferencesPath; got != filepath.Join(base, "ui-prefs.json") {
t.Fatalf("UIPreferencesPath = %q, want %q", got, filepath.Join(base, "ui-prefs.json"))
}
}
func TestResolveFlagOrEnvPrefersFlagThenEnvThenFallback(t *testing.T) {