Files
keevault/MyPass/Views/GroupBrowserView.swift
T
krissandClaude Sonnet 5 a48e8baf7a feat: add EntryEditView for add/edit entries
Also fixes real bugs found while making Phase 4 build clean on both
platforms:
- FileBookmarkService used .withSecurityScope unconditionally, but
  that bookmark option is macOS-only -- iOS grants scoped access
  automatically once the user picks a file. Now conditional per platform.
- EntryEditViewModel's groupId was non-optional with no default, but
  EntryDetailView's edit-sheet call site (Task 14) has no group
  reference to pass -- only the "add new entry" path actually needs
  it, so groupId is now optional and validated at save time instead.
- Missing `import Combine` in EntryDetailView (Timer.publish/autoconnect)
  and UnlockViewModel/VaultViewModel/EntryEditViewModel (@Published).
- GroupBrowserView's `group: Group` property collided with SwiftUI's
  own Group view type -- qualified as MyPassCore.Group.
- EntryEditView's .keyboardType(.URL) and .navigationBarTitleDisplayMode
  are iOS-only APIs, guarded with #if os(iOS) for the macOS build.

Verified clean (aside from the known, expected ContentView.swift/Item
breakage) on both iOS Simulator and macOS destinations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 16:00:37 +02:00

94 lines
2.8 KiB
Swift

//
// 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)
}
}
}