Tighten banner and navigation behavior
This commit is contained in:
+1
-1
@@ -96,7 +96,7 @@ func (s *State) SetSearchQuery(query string) {
|
|||||||
|
|
||||||
func (s *State) BeginNewEntry() {
|
func (s *State) BeginNewEntry() {
|
||||||
s.SelectedEntryID = ""
|
s.SelectedEntryID = ""
|
||||||
s.StatusMessage = "new entry form ready"
|
s.StatusMessage = ""
|
||||||
s.ErrorMessage = ""
|
s.ErrorMessage = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -922,7 +922,7 @@ func TestSetSearchQueryUpdatesControllerSearchState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBeginNewEntryClearsSelectionAndSetsStatus(t *testing.T) {
|
func TestBeginNewEntryClearsSelectionAndStatus(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
state := State{
|
state := State{
|
||||||
@@ -935,8 +935,8 @@ func TestBeginNewEntryClearsSelectionAndSetsStatus(t *testing.T) {
|
|||||||
if state.SelectedEntryID != "" {
|
if state.SelectedEntryID != "" {
|
||||||
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
||||||
}
|
}
|
||||||
if state.StatusMessage != "new entry form ready" {
|
if state.StatusMessage != "" {
|
||||||
t.Fatalf("StatusMessage = %q, want new entry form ready", state.StatusMessage)
|
t.Fatalf("StatusMessage = %q, want empty", state.StatusMessage)
|
||||||
}
|
}
|
||||||
if state.ErrorMessage != "" {
|
if state.ErrorMessage != "" {
|
||||||
t.Fatalf("ErrorMessage = %q, want empty", state.ErrorMessage)
|
t.Fatalf("ErrorMessage = %q, want empty", state.ErrorMessage)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
maxAttachmentBytes = 10 << 20
|
maxAttachmentBytes = 10 << 20
|
||||||
statusBannerDuration = 4 * time.Second
|
statusBannerDuration = 2600 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type bannerKind string
|
type bannerKind string
|
||||||
@@ -79,6 +79,7 @@ type statePaths struct {
|
|||||||
DefaultSaveAsPath string
|
DefaultSaveAsPath string
|
||||||
RecentVaultsPath string
|
RecentVaultsPath string
|
||||||
RecentRemotesPath string
|
RecentRemotesPath string
|
||||||
|
UIPreferencesPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type recentVaultRecord struct {
|
type recentVaultRecord struct {
|
||||||
@@ -94,6 +95,10 @@ type recentRemoteRecord struct {
|
|||||||
LastGroup []string `json:"lastGroup,omitempty"`
|
LastGroup []string `json:"lastGroup,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type uiPreferences struct {
|
||||||
|
GroupControlsHidden bool `json:"groupControlsHidden"`
|
||||||
|
}
|
||||||
|
|
||||||
type ui struct {
|
type ui struct {
|
||||||
mode string
|
mode string
|
||||||
theme *material.Theme
|
theme *material.Theme
|
||||||
@@ -201,6 +206,7 @@ type ui struct {
|
|||||||
keyboardFocus focusID
|
keyboardFocus focusID
|
||||||
defaultSaveAsPath string
|
defaultSaveAsPath string
|
||||||
recentVaultsPath string
|
recentVaultsPath string
|
||||||
|
uiPreferencesPath string
|
||||||
recentRemotesPath string
|
recentRemotesPath string
|
||||||
editingEntry bool
|
editingEntry bool
|
||||||
groupControlsHidden bool
|
groupControlsHidden bool
|
||||||
@@ -293,6 +299,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
lifecycleMode: "local",
|
lifecycleMode: "local",
|
||||||
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
||||||
recentVaultsPath: paths.RecentVaultsPath,
|
recentVaultsPath: paths.RecentVaultsPath,
|
||||||
|
uiPreferencesPath: paths.UIPreferencesPath,
|
||||||
recentRemotesPath: paths.RecentRemotesPath,
|
recentRemotesPath: paths.RecentRemotesPath,
|
||||||
recentVaultGroups: map[string][]string{},
|
recentVaultGroups: map[string][]string{},
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
@@ -309,6 +316,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
u.setCustomFieldRows(nil)
|
u.setCustomFieldRows(nil)
|
||||||
u.loadRecentVaults()
|
u.loadRecentVaults()
|
||||||
u.loadRecentRemotes()
|
u.loadRecentRemotes()
|
||||||
|
u.loadUIPreferences()
|
||||||
u.filter()
|
u.filter()
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
@@ -342,6 +350,7 @@ func defaultStatePaths(stateDir string) statePaths {
|
|||||||
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
||||||
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
||||||
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.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)
|
_ = 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) {
|
func (u *ui) noteRecentRemote(baseURL, path, username, password string, rememberAuth bool) {
|
||||||
baseURL = strings.TrimSpace(baseURL)
|
baseURL = strings.TrimSpace(baseURL)
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
@@ -1398,6 +1438,7 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
for u.toggleGroupControls.Clicked(gtx) {
|
for u.toggleGroupControls.Clicked(gtx) {
|
||||||
u.groupControlsHidden = !u.groupControlsHidden
|
u.groupControlsHidden = !u.groupControlsHidden
|
||||||
|
u.saveUIPreferences()
|
||||||
}
|
}
|
||||||
for u.renameGroup.Clicked(gtx) {
|
for u.renameGroup.Clicked(gtx) {
|
||||||
u.clearDeleteGroupConfirmation()
|
u.clearDeleteGroupConfirmation()
|
||||||
@@ -1644,11 +1685,21 @@ func (u *ui) navigationHeader(gtx layout.Context) layout.Dimensions {
|
|||||||
func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions {
|
func (u *ui) sectionBar(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1737,6 +1737,9 @@ func TestUIStatusBannerExpiresAfterTimeout(t *testing.T) {
|
|||||||
u := newUIWithModel("desktop", vault.Model{})
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
u.now = func() time.Time { return now }
|
u.now = func() time.Time { return now }
|
||||||
u.state.StatusMessage = "synchronize vault complete"
|
u.state.StatusMessage = "synchronize vault complete"
|
||||||
|
if statusBannerDuration != 2600*time.Millisecond {
|
||||||
|
t.Fatalf("statusBannerDuration = %v, want 2.6s", statusBannerDuration)
|
||||||
|
}
|
||||||
u.statusExpiresAt = now.Add(statusBannerDuration)
|
u.statusExpiresAt = now.Add(statusBannerDuration)
|
||||||
|
|
||||||
if got := u.bannerSurface(); got.Kind != bannerStatus || got.Message != "synchronize vault complete" {
|
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) {
|
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -2107,6 +2130,9 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
|||||||
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.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"))
|
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) {
|
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") {
|
if got := paths.RecentRemotesPath; got != filepath.Join(base, "recent-remotes.json") {
|
||||||
t.Fatalf("RecentRemotesPath = %q, want %q", 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) {
|
func TestResolveFlagOrEnvPrefersFlagThenEnvThenFallback(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user