import SwiftUI import SecretKit import SecureEnclaveSecretKit import SmartCardSecretKit import Brief struct ContentView: View { @Binding var showingCreation: Bool @Binding var runningSetup: Bool @Binding var hasRunSetup: Bool @State var showingAgentInfo = false @State var activeSecret: AnySecret.ID? @Environment(\.colorScheme) var colorScheme @EnvironmentObject private var storeList: SecretStoreList @EnvironmentObject private var updater: UpdaterType @EnvironmentObject private var agentStatusChecker: AgentStatusCheckerType @State private var selectedUpdate: Release? @State private var showingAppPathNotice = false var body: some View { VStack { if storeList.anyAvailable { StoreListView(activeSecret: $activeSecret) } else { NoStoresView() } } .frame(minWidth: 640, minHeight: 320) .toolbar { toolbarItem(updateNoticeView, id: "update") toolbarItem(runningOrRunSetupView, id: "setup") toolbarItem(appPathNoticeView, id: "appPath") toolbarItem(newItemView, id: "new") } .sheet(isPresented: $runningSetup) { SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) } } } extension ContentView { func toolbarItem(_ view: some View, id: String) -> ToolbarItem { ToolbarItem(id: id) { view } } var needsSetup: Bool { (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild } /// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message /// These two are mutually exclusive @ViewBuilder var runningOrRunSetupView: some View { if needsSetup { setupNoticeView } else { runningNoticeView } } var updateNoticeContent: (String, Color)? { guard let update = updater.update else { return nil } if update.critical { return ("Critical Security Update Required", .red) } else { if updater.testBuild { return ("Test Build", .blue) } else { return ("Update Available", .orange) } } } @ViewBuilder var updateNoticeView: some View { if let update = updater.update, let (text, color) = updateNoticeContent { Button(action: { selectedUpdate = update }, label: { Text(text) .font(.headline) .foregroundColor(.white) }) .buttonStyle(ToolbarButtonStyle(color: color)) .popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in UpdateDetailView(update: update) } } } @ViewBuilder var newItemView: some View { if storeList.modifiableStore?.isAvailable ?? false { Button(action: { showingCreation = true }, label: { Image(systemName: "plus") }) .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { CreateSecretView(store: modifiable, showing: $showingCreation) .onDisappear { guard let newest = modifiable.secrets.last?.id else { return } activeSecret = newest } } } } } @ViewBuilder var setupNoticeView: some View { Button(action: { runningSetup = true }, label: { Group { if hasRunSetup && !agentStatusChecker.running { Text("Secret Agent Is Not Running") } else { Text("Setup Secretive") } } .font(.headline) .foregroundColor(.white) }) .buttonStyle(ToolbarButtonStyle(color: .orange)) } @ViewBuilder var runningNoticeView: some View { Button(action: { showingAgentInfo = true }, label: { HStack { Text("Agent is Running") .font(.headline) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) Circle() .frame(width: 10, height: 10) .foregroundColor(Color.green) } }) .buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { VStack { Text("SecretAgent is Running") .font(.title) .padding(5) Text("SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**You can close Secretive, and everything will still keep working.**") .frame(width: 300) } .padding() } } @ViewBuilder var appPathNoticeView: some View { if !ApplicationDirectoryController().isInApplicationsDirectory { Button(action: { showingAppPathNotice = true }, label: { Group { Text("Secretive Is Not in Applications Folder") } .font(.headline) .foregroundColor(.white) }) .buttonStyle(ToolbarButtonStyle(color: .orange)) .popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { VStack { Image(systemName: "exclamationmark.triangle") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 64) Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.") .frame(maxWidth: 300) } .padding() } } } var attachmentAnchor: PopoverAttachmentAnchor { // Ideally .point(.bottom), but broken on Sonoma (FB12726503) .rect(.bounds) } } #if DEBUG struct ContentView_Previews: PreviewProvider { private static let storeList: SecretStoreList = { let list = SecretStoreList() list.add(store: SecureEnclave.Store()) list.add(store: SmartCard.Store()) return list }() private static let agentStatusChecker = AgentStatusChecker() private static let justUpdatedChecker = JustUpdatedChecker() @State var hasRunSetup = false @State private var showingSetup = false @State private var showingCreation = false static var previews: some View { Group { // Empty on modifiable and nonmodifiable ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) .environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) .environmentObject(PreviewUpdater()) .environmentObject(agentStatusChecker) // 5 items on modifiable and nonmodifiable ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) .environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) .environmentObject(PreviewUpdater()) .environmentObject(agentStatusChecker) } .environmentObject(agentStatusChecker) } } #endif