import SwiftUI struct SetupView: View { @State var stepIndex = 0 @Binding var visible: Bool @Binding var setupComplete: Bool var body: some View { GeometryReader { proxy in VStack { StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width) GeometryReader { _ in HStack(spacing: 0) { SecretAgentSetupView(buttonAction: advance) .frame(width: proxy.size.width) SSHAgentSetupView(buttonAction: advance) .frame(width: proxy.size.width) UpdaterExplainerView { visible = false setupComplete = true } .frame(width: proxy.size.width) } .offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0) } } } .frame(idealWidth: 500, idealHeight: 500) } func advance() { withAnimation(.spring()) { stepIndex += 1 } } } struct StepView: View { let numberOfSteps: Int let currentStep: Int // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 let width: CGFloat var body: some View { ZStack(alignment: .leading) { Rectangle() .foregroundColor(.blue) .frame(height: 5) Rectangle() .foregroundColor(.green) .frame(width: max(0, ((width - (Constants.padding * 2)) / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep) - (Constants.circleWidth / 2)), height: 5) .animation(.spring()) HStack { ForEach(0.. index { Circle() .foregroundColor(.green) .frame(width: Constants.circleWidth, height: Constants.circleWidth) Text("✓") .foregroundColor(.white) .bold() } else { Circle() .foregroundColor(.blue) .frame(width: Constants.circleWidth, height: Constants.circleWidth) if currentStep == index { Circle() .strokeBorder(Color.white, lineWidth: 3) .frame(width: Constants.circleWidth, height: Constants.circleWidth) } Text(String(describing: index + 1)) .foregroundColor(.white) .bold() } } if index < numberOfSteps - 1 { Spacer(minLength: 30) } } } }.padding(Constants.padding) } } extension StepView { enum Constants { static let padding: CGFloat = 15 static let circleWidth: CGFloat = 30 } } struct SetupStepView : View where Content : View { let title: String let image: Image let bodyText: String let buttonTitle: String let buttonAction: () -> Void let content: Content init(title: String, image: Image, bodyText: String, buttonTitle: String, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { self.title = title self.image = image self.bodyText = bodyText self.buttonTitle = buttonTitle self.buttonAction = buttonAction self.content = content() } var body: some View { VStack { Text(title) .font(.title) Spacer() image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 64) Spacer() Text(bodyText) .multilineTextAlignment(.center) Spacer() content Spacer() Button(buttonTitle) { buttonAction() } }.padding() } } struct SecretAgentSetupView: View { let buttonAction: () -> Void var body: some View { SetupStepView(title: "Setup Secret Agent", image: Image(nsImage: NSApp.applicationIconImage), bodyText: "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open.", buttonTitle: "Install", buttonAction: install) { (Text("This helper app is called ") + Text("Secret Agent").bold().underline() + Text(" and you may see it in Activity Manager from time to time.")) .multilineTextAlignment(.center) } } func install() { _ = LaunchAgentController().install() buttonAction() } } struct SSHAgentSetupView: View { let buttonAction: () -> Void private static let controller = ShellConfigurationController() @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! var body: some View { SetupStepView(title: "Configure your SSH Agent", image: Image(systemName: "terminal"), bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file.", buttonTitle: "I Added it Manually", buttonAction: buttonAction) { Picker(selection: $selectedShellInstruction, label: EmptyView()) { ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in Text(instruction.shell) .tag(instruction) .padding() } }.pickerStyle(SegmentedPickerStyle()) CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) Button("Add it For Me") { let controller = ShellConfigurationController() if controller.addToShell(shellInstructions: selectedShellInstruction) { buttonAction() } } } } } class Delegate: NSObject, NSOpenSavePanelDelegate { private let name: String init(name: String) { self.name = name } func panel(_ sender: Any, shouldEnable url: URL) -> Bool { return url.lastPathComponent == name } } struct UpdaterExplainerView: View { let buttonAction: () -> Void var body: some View { SetupStepView(title: "Updates", image: Image(systemName: "dot.radiowaves.left.and.right"), bodyText: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.", buttonTitle: "Okay", buttonAction: buttonAction) { Link("Read more about this here.", destination: SetupView.Constants.updaterFAQURL) } } } extension SetupView { enum Constants { static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! } } struct ShellConfigInstruction: Identifiable, Hashable { var shell: String var shellConfigDirectory: String var shellConfigFilename: String var text: String var id: String { shell } var shellConfigPath: String { return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename) } } #if DEBUG struct SetupView_Previews: PreviewProvider { static var previews: some View { Group { SetupView(visible: .constant(true), setupComplete: .constant(false)) } } } struct SecretAgentSetupView_Previews: PreviewProvider { static var previews: some View { Group { SecretAgentSetupView(buttonAction: {}) } } } struct SSHAgentSetupView_Previews: PreviewProvider { static var previews: some View { Group { SSHAgentSetupView(buttonAction: {}) } } } struct UpdaterExplainerView_Previews: PreviewProvider { static var previews: some View { Group { UpdaterExplainerView(buttonAction: {}) } } } #endif