From 171ce1eea7b9c4bde976cedd18e725d552ecf1d3 Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 19 Sep 2026 17:11:02 +0200 Subject: [PATCH] feat: add EntryListView, remove GroupBrowserView and SearchView Replaces group-drilling navigation with a flat, filterable/searchable entry list backed by VaultViewModel.displayedEntries. SearchView's role is fully absorbed by EntryListView's .searchable() modifier. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf --- MyPass/Views/EntryListView.swift | 92 ++++++++++++++++++++++++++++ MyPass/Views/GroupBrowserView.swift | 93 ----------------------------- MyPass/Views/SearchView.swift | 47 --------------- 3 files changed, 92 insertions(+), 140 deletions(-) create mode 100644 MyPass/Views/EntryListView.swift delete mode 100644 MyPass/Views/GroupBrowserView.swift delete mode 100644 MyPass/Views/SearchView.swift diff --git a/MyPass/Views/EntryListView.swift b/MyPass/Views/EntryListView.swift new file mode 100644 index 0000000..eeba487 --- /dev/null +++ b/MyPass/Views/EntryListView.swift @@ -0,0 +1,92 @@ +// MyPass/Views/EntryListView.swift +import SwiftUI +import MyPassCore + +struct EntryListView: View { + @ObservedObject var vm: VaultViewModel + @State private var isSearching = false + @State private var showAddEntry = false + + var body: some View { + List { + ForEach(vm.displayedEntries) { flatEntry in + NavigationLink( + destination: EntryDetailView( + entry: flatEntry.entry, + groupId: flatEntry.parentGroupId, + session: vm.session + ) + ) { + EntryRow(flatEntry: flatEntry) + } + } + .onDelete { offsets in + offsets.map { vm.displayedEntries[$0] }.forEach(vm.deleteEntry) + } + } + .navigationTitle("MyPass") + .searchable(text: $vm.searchQuery, isPresented: $isSearching, prompt: "Search entries…") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { isSearching.toggle() } label: { + Image(systemName: "magnifyingglass") + } + } + ToolbarItem(placement: .primaryAction) { + Button { showAddEntry = true } label: { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $showAddEntry) { + EntryEditView(vm: EntryEditViewModel(session: vm.session, groupId: defaultGroupId)) + } + .alert("Error", isPresented: Binding( + get: { vm.errorMessage != nil }, + set: { if !$0 { vm.errorMessage = nil } } + )) { + Button("OK", role: .cancel) { vm.errorMessage = nil } + } message: { + Text(vm.errorMessage ?? "") + } + } + + /// The sole selected filter group if exactly one is active, otherwise the vault root. + private var defaultGroupId: UUID { + if vm.selectedGroupIds.count == 1, let only = vm.selectedGroupIds.first { + return only + } + return vm.session.database?.root.id ?? UUID() + } +} + +private struct EntryRow: View { + let flatEntry: FlatEntry + + private var breadcrumbText: String? { + guard !flatEntry.breadcrumb.isEmpty else { return nil } + return flatEntry.breadcrumb.map(\.name).joined(separator: " > ") + } + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: KeePassIconMapper.symbolName(forIconIndex: flatEntry.entry.iconIndex)) + .foregroundStyle(.secondary) + .frame(width: 24) + VStack(alignment: .leading, spacing: 2) { + Text(flatEntry.entry.title).font(.body) + if let breadcrumbText { + Text("\(flatEntry.entry.username) · \(breadcrumbText)") + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } else { + Text(flatEntry.entry.username) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } +} diff --git a/MyPass/Views/GroupBrowserView.swift b/MyPass/Views/GroupBrowserView.swift deleted file mode 100644 index abc1e14..0000000 --- a/MyPass/Views/GroupBrowserView.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// GroupBrowserView.swift -// MyPass -// - -import SwiftUI -import MyPassCore - -struct GroupBrowserView: View { - @ObservedObject var vm: VaultViewModel - let group: MyPassCore.Group - - @State private var selectedEntry: Entry? - @State private var showAddEntry = false - - var body: some View { - List { - if vm.isSearching { - searchResultsSection - } else { - groupTreeSection - } - } - .navigationTitle(group.name) - .searchable(text: $vm.searchQuery, prompt: "Search all entries…") - .toolbar { - #if os(iOS) - ToolbarItem(placement: .navigationBarTrailing) { EditButton() } - #endif - ToolbarItem(placement: .primaryAction) { - Button { showAddEntry = true } label: { - Image(systemName: "plus") - } - } - } - .sheet(isPresented: $showAddEntry) { - let editVM = EntryEditViewModel(session: vm.session, groupId: group.id) - EntryEditView(vm: editVM) - } - .alert("Error", isPresented: Binding( - get: { vm.errorMessage != nil }, - set: { if !$0 { vm.errorMessage = nil } } - )) { - Button("OK", role: .cancel) { vm.errorMessage = nil } - } message: { - Text(vm.errorMessage ?? "") - } - } - - @ViewBuilder - private var searchResultsSection: some View { - ForEach(vm.filteredEntries) { entry in - NavigationLink(destination: EntryDetailView(entry: entry, session: vm.session)) { - EntryRow(entry: entry) - } - } - } - - @ViewBuilder - private var groupTreeSection: some View { - if !group.subgroups.isEmpty { - Section("Groups") { - ForEach(group.subgroups) { sub in - NavigationLink(destination: GroupBrowserView(vm: vm, group: sub)) { - Label(sub.name, systemImage: "folder") - } - } - } - } - if !group.entries.isEmpty { - Section("Entries") { - ForEach(group.entries) { entry in - NavigationLink(destination: EntryDetailView(entry: entry, session: vm.session)) { - EntryRow(entry: entry) - } - } - .onDelete { offsets in - offsets.map { group.entries[$0] }.forEach { vm.deleteEntry($0, fromGroup: group) } - } - } - } - } -} - -private struct EntryRow: View { - let entry: Entry - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(entry.title).font(.body) - Text(entry.username).font(.caption).foregroundStyle(.secondary) - } - } -} diff --git a/MyPass/Views/SearchView.swift b/MyPass/Views/SearchView.swift deleted file mode 100644 index ae9eba5..0000000 --- a/MyPass/Views/SearchView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// SearchView.swift -// MyPass -// -// Search is embedded directly in GroupBrowserView via .searchable. SearchView is a -// standalone view for displaying search results when the query is active -- used in -// the macOS 3-column layout as the middle column when searching. -// - -import SwiftUI -import MyPassCore - -struct SearchView: View { - @ObservedObject var vm: VaultViewModel - @Binding var selectedEntry: Entry? - - var body: some View { - SwiftUI.Group { - if vm.filteredEntries.isEmpty { - ContentUnavailableView.search(text: vm.searchQuery) - } else { - List(vm.filteredEntries, selection: $selectedEntry) { entry in - #if os(macOS) - EntryListRow(entry: entry).tag(entry) - #else - NavigationLink(destination: EntryDetailView(entry: entry, session: vm.session)) { - EntryListRow(entry: entry) - } - #endif - } - } - } - } -} - -struct EntryListRow: View { - let entry: Entry - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(entry.title).font(.body) - Text(entry.username).font(.caption).foregroundStyle(.secondary) - if !entry.url.isEmpty { - Text(entry.url).font(.caption2).foregroundStyle(.tertiary) - } - } - } -}