diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index d66060b..fe78151 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; 50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; + 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; }; @@ -123,6 +124,7 @@ 50617D9823FCE48E0099B055 /* SecretiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveTests.swift; sourceTree = ""; }; 50617D9A23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = ""; }; + 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; @@ -264,6 +266,7 @@ isa = PBXGroup; children = ( 50617D8423FCE48E0099B055 /* ContentView.swift */, + 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, 50153E21250DECA300525160 /* SecretListItemView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, @@ -475,6 +478,7 @@ 2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, + 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 2699062..59dd3fc 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -9,6 +9,8 @@ struct ContentView { - guard let update = updater.update else { - return ToolbarItem { AnyView(EmptyView()) } + + 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 } - let color: Color - let text: String + } + + var updateNoticeContent: (String, Color)? { + guard let update = updater.update else { return nil } if update.critical { - text = "Critical Security Update Required" - color = .red + return ("Critical Security Update Required", .red) } else { if updater.testBuild { - text = "Test Build" - color = .blue + return ("Test Build", .blue) } else { - text = "Update Available" - color = .orange + return ("Update Available", .orange) } } - return ToolbarItem { - AnyView( - Button(action: { - selectedUpdate = update - }, label: { - Text(text) - .font(.headline) - .foregroundColor(.white) - }) - .background(color) - .cornerRadius(5) - .popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in - UpdateDetailView(update: update) - } - ) - } } - var newItem: ToolbarItem { - guard storeList.modifiableStore?.isAvailable ?? false else { - return ToolbarItem { AnyView(EmptyView()) } - } - return ToolbarItem { - AnyView( - Button(action: { - showingCreation = true - }, label: { - Image(systemName: "plus") - }) - .sheet(isPresented: $showingCreation) { - if let modifiable = storeList.modifiableStore { - CreateSecretView(store: modifiable, showing: $showingCreation) - } - } - - ) - } - } - - var setupNotice: ToolbarItem { - return ToolbarItem { - AnyView( - Group { - if (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild { - Button(action: { - runningSetup = true - }, label: { - Group { - if hasRunSetup && !agentStatusChecker.running { - Text("Secret Agent Is Not Running") - } else { - Text("Setup Secretive") - } - } - .font(.headline) - .foregroundColor(.white) - }) - .background(Color.orange) - .cornerRadius(5) - } else { - EmptyView() - } - } - ) - } - } - - var appPathNotice: ToolbarItem { - let controller = ApplicationDirectoryController() - guard !controller.isInApplicationsDirectory else { - return ToolbarItem { AnyView(EmptyView()) } - } - return ToolbarItem { - AnyView( - Button(action: { - showingAppPathNotice = true - }, label: { - Group { - Text("Secretive Is Not in Applications Folder") - } + @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) - }) - .background(Color.orange) - .cornerRadius(5) - .popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), 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() + }) + .buttonStyle(ToolbarButtonStyle(color: color)) + .popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), 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) } - ) + } + } + } + + @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: .point(.bottom), 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: .point(.bottom), 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() + } } } @@ -198,3 +220,4 @@ struct ContentView_Previews: PreviewProvider { } #endif + diff --git a/Sources/Secretive/Views/ToolbarButtonStyle.swift b/Sources/Secretive/Views/ToolbarButtonStyle.swift new file mode 100644 index 0000000..a80cde4 --- /dev/null +++ b/Sources/Secretive/Views/ToolbarButtonStyle.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct ToolbarButtonStyle: ButtonStyle { + + private let lightColor: Color + private let darkColor: Color + @Environment(\.colorScheme) var colorScheme + @State var hovering = false + + init(color: Color) { + self.lightColor = color + self.darkColor = color + } + + init(lightColor: Color, darkColor: Color) { + self.lightColor = lightColor + self.darkColor = darkColor + } + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8)) + .background(colorScheme == .light ? lightColor : darkColor) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .overlay( + RoundedRectangle(cornerRadius: 5) + .stroke(colorScheme == .light ? .black.opacity(0.15) : .white.opacity(0.15), lineWidth: 1) + .background(hovering ? (colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.05)) : Color.clear) + ) + .onHover { hovering in + withAnimation { + self.hovering = hovering + } + } + } +}