diff --git a/MyPass/ContentView.swift b/MyPass/ContentView.swift index cabcf8e..ebd9db4 100644 --- a/MyPass/ContentView.swift +++ b/MyPass/ContentView.swift @@ -6,75 +6,128 @@ // import SwiftUI -import SwiftData +import MyPassCore +#if os(macOS) +import AppKit +#endif struct ContentView: View { - @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] + @StateObject private var session = VaultSession() var body: some View { - NavigationViewWrapper { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") - } label: { - Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) - } - } - .onDelete(perform: deleteItems) - } -#if os(macOS) - .navigationSplitViewColumnWidth(min: 180, ideal: 200) -#endif - .toolbar { -#if os(iOS) - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } -#endif - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } + SwiftUI.Group { + if session.isLocked { + UnlockView(vm: UnlockViewModel(session: session)) + } else { + vaultView } } + .onReceive( + NotificationCenter.default.publisher(for: sceneBackgroundNotification) + ) { _ in + session.lock() + } } - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) - modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(items[index]) + @ViewBuilder + private var vaultView: some View { + let vaultVM = VaultViewModel(session: session) + #if os(macOS) + MacVaultView(vm: vaultVM) + #else + NavigationStack { + if let root = session.database?.root { + GroupBrowserView(vm: vaultVM, group: root) } } + #endif + } + + private var sceneBackgroundNotification: Notification.Name { + #if os(iOS) + UIScene.didEnterBackgroundNotification + #else + NSApplication.didResignActiveNotification + #endif } } -fileprivate struct NavigationViewWrapper: View { - let content: () -> Content +// MARK: - macOS 3-column layout + +#if os(macOS) +private struct MacVaultView: View { + @ObservedObject var vm: VaultViewModel + @State private var selectedGroup: MyPassCore.Group? + @State private var selectedEntry: Entry? + @State private var showAddEntry = false + + private var activeGroup: MyPassCore.Group? { selectedGroup ?? vm.session.database?.root } var body: some View { -#if os(macOS) NavigationSplitView { - content() + GroupSidebarView(vm: vm, selectedGroup: $selectedGroup) + } content: { + if vm.isSearching { + SearchView(vm: vm, selectedEntry: $selectedEntry) + } else if let group = activeGroup { + entryList(for: group) + } else { + Text("Select a group").foregroundStyle(.secondary) + } } detail: { - Text("Select an item") + if let entry = selectedEntry { + EntryDetailView(entry: entry, session: vm.session) + } else { + Text("Select an entry").foregroundStyle(.secondary) + } + } + .searchable(text: $vm.searchQuery, prompt: "Search all entries…") + .sheet(isPresented: $showAddEntry) { + if let group = activeGroup { + EntryEditView(vm: EntryEditViewModel(session: vm.session, groupId: group.id)) + } + } + } + + private func entryList(for group: MyPassCore.Group) -> some View { + List(group.entries, selection: $selectedEntry) { entry in + EntryListRow(entry: entry).tag(entry) + } + .navigationTitle(group.name) + .toolbar { + ToolbarItem { + Button { showAddEntry = true } label: { Image(systemName: "plus") } + } } -#else - content() -#endif } } -#Preview { - ContentView() - .modelContainer(for: Item.self, inMemory: true) +private struct GroupSidebarView: View { + @ObservedObject var vm: VaultViewModel + @Binding var selectedGroup: MyPassCore.Group? + + var body: some View { + List(selection: $selectedGroup) { + if let root = vm.session.database?.root { + GroupNode(group: root) + } + } + .navigationTitle("Groups") + } } + +private struct GroupNode: View { + let group: MyPassCore.Group + var body: some View { + if group.subgroups.isEmpty { + Label(group.name, systemImage: "folder").tag(group) + } else { + DisclosureGroup { + ForEach(group.subgroups) { sub in GroupNode(group: sub) } + } label: { + Label(group.name, systemImage: "folder").tag(group) + } + } + } +} +#endif diff --git a/MyPass/MyPass.entitlements b/MyPass/MyPass.entitlements index 2eb7e33..38cff67 100644 --- a/MyPass/MyPass.entitlements +++ b/MyPass/MyPass.entitlements @@ -3,6 +3,12 @@ com.apple.security.application-groups - + + group.org.antiloop222.mypass + + keychain-access-groups + + $(AppIdentifierPrefix)org.antiloop222.mypass + diff --git a/MyPass/ViewModels/EntryEditViewModel.swift b/MyPass/ViewModels/EntryEditViewModel.swift index 0fe178e..e307cc0 100644 --- a/MyPass/ViewModels/EntryEditViewModel.swift +++ b/MyPass/ViewModels/EntryEditViewModel.swift @@ -39,7 +39,7 @@ final class EntryEditViewModel: ObservableObject { var isEditing: Bool { existingEntry != nil } - func save(dismiss: () -> Void) { + func save(dismiss: @escaping () -> Void) { Task { isSaving = true errorMessage = nil diff --git a/MyPass/Views/UnlockView.swift b/MyPass/Views/UnlockView.swift index f252ead..d8a2124 100644 --- a/MyPass/Views/UnlockView.swift +++ b/MyPass/Views/UnlockView.swift @@ -4,6 +4,7 @@ // import SwiftUI +import UniformTypeIdentifiers import MyPassCore struct UnlockView: View {