import SwiftUI import SecretKit import Brief struct ContentView: View { @ObservedObject var storeList: SecretStoreList @ObservedObject var updater: UpdaterType @ObservedObject var agentStatusChecker: AgentStatusCheckerType var runSetupBlock: (() -> Void)? @State fileprivate var active: AnySecret.ID? @State fileprivate var showingDeletion = false @State fileprivate var deletingSecret: AnySecret? var body: some View { VStack { if updater.update != nil { updateNotice() } if !agentStatusChecker.running { agentNotice() } if storeList.anyAvailable { NavigationView { List(selection: $active) { ForEach(storeList.stores) { store in if store.isAvailable { Section(header: Text(store.name)) { if store.secrets.isEmpty { if store is AnySecretStoreModifiable { NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) { Text("No Secrets") } } else { NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) { Text("No Secrets") } } } else { ForEach(store.secrets) { secret in NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) { Text(secret.name) }.contextMenu { if store is AnySecretStoreModifiable { Button(action: { self.delete(secret: secret) }) { Text("Delete") } } } } } } } } }.onAppear { self.active = self.nextDefaultSecret } .listStyle(SidebarListStyle()) .frame(minWidth: 100, idealWidth: 240) } .navigationViewStyle(DoubleColumnNavigationViewStyle()) .sheet(isPresented: $showingDeletion) { if self.storeList.modifiableStore != nil { DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { deleted in self.showingDeletion = false if deleted { self.active = self.nextDefaultSecret } } } } } else { NoStoresView() } }.frame(minWidth: 640, minHeight: 320) } func updateNotice() -> some View { guard let update = updater.update else { return AnyView(Spacer()) } let severity: NoticeView.Severity let text: String if update.critical { severity = .critical text = "Critical Security Update Required" } else { severity = .advisory text = "Update Available" } let action = { _ = NSWorkspace.shared.open(update.html_url) } let ignoreAction = { updater.ignore(release: update) } return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update", action: action, secondaryActionTitle: "Ignore", secondaryAction: ignoreAction)) } func agentNotice() -> some View { NoticeView(text: "Secret Agent isn't running. Run setup again to fix.", severity: .advisory, actionTitle: "Run Setup") { self.runSetupBlock?() } } func delete(secret: SecretType) { deletingSecret = AnySecret(secret) self.showingDeletion = true } var nextDefaultSecret: AnyHashable? { let fallback: AnyHashable if self.storeList.modifiableStore?.isAvailable ?? false { fallback = Constants.emptyStoreModifiableTag } else { fallback = Constants.emptyStoreTag } return self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback } } fileprivate enum Constants { static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag" } #if DEBUG struct ContentView_Previews: PreviewProvider { static var previews: some View { Group { ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater(), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical), agentStatusChecker: PreviewAgentStatusChecker()) ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical), agentStatusChecker: PreviewAgentStatusChecker(running: false)) } } } #endif