import AppKit import SwiftUI import ServiceManagement // MARK: - Environment Key for Window Access private struct WindowKey: EnvironmentKey { static let defaultValue: FirstLaunchWizard? = nil } extension EnvironmentValues { var window: FirstLaunchWizard? { get { self[WindowKey.self] } set { self[WindowKey.self] = newValue } } } // MARK: - PreferenceKey for Button Width Sync struct ButtonWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } } // MARK: - First Launch Wizard Window class FirstLaunchWizard: NSWindow { private var hostingView: NSHostingView? private var isClosing = false // Perfect fit window size - made smaller static let windowSize = NSSize(width: 420, height: 520) init() { let windowRect = NSRect(origin: .zero, size: FirstLaunchWizard.windowSize) super.init( contentRect: windowRect, styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false ) setupWindow() setupContent() } private func setupWindow() { title = "Welcome to ShotScreen" isOpaque = false backgroundColor = .clear hasShadow = true titleVisibility = .hidden titlebarAppearsTransparent = true isMovable = true isMovableByWindowBackground = true level = .normal isReleasedWhenClosed = true // Center the window center() } private func setupContent() { let wizardView = SinglePageWizardView { [unowned self] in // Completion handler - mark first launch as completed SettingsManager.shared.hasCompletedFirstLaunch = true print("✅ First launch wizard completed - closing window") print("🔍 DEBUG: About to call safeClose(), self exists: \(self)") self.safeClose() } hostingView = NSHostingView(rootView: AnyView(wizardView.environment(\.window, self))) contentView = hostingView } private func safeClose() { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.safeClose() } return } guard !isClosing else { print("🔍 DEBUG: Already closing, skipping") return } isClosing = true print("🎯 Safely closing wizard...") // Force close immediately without async orderOut(nil) close() } override func close() { print("🎉 FirstLaunchWizard: Window closing") guard !isClosing else { print("🔍 DEBUG: Close() called but already closing") return } isClosing = true // First hide the window immediately orderOut(nil) // Clean up hosting view if let hosting = hostingView { hosting.rootView = AnyView(EmptyView()) hosting.removeFromSuperview() hostingView = nil } contentView = nil // Force the superclass close super.close() print("🎉 FirstLaunchWizard: Window actually closed") } deinit { print("🎉 FirstLaunchWizard: Deallocated") } } // MARK: - Single Page Wizard View struct SinglePageWizardView: View { let onComplete: () -> Void @Environment(\.window) private var window // State for permission checks @State private var hasScreenRecordingPermission = false @State private var isLaunchAtStartupEnabled = false @State private var hasCompletedScreenshotSettings = false var body: some View { VStack(spacing: 0) { // Header with logo and title CompactHeaderView() .padding(.top, 20) .padding(.bottom, 20) // All content perfectly fitted without scrolling VStack(spacing: 16) { // Setup section SetupSection( hasScreenRecordingPermission: hasScreenRecordingPermission, hasCompletedScreenshotSettings: hasCompletedScreenshotSettings, onScreenshotSettingsCompleted: { hasCompletedScreenshotSettings = true } ) // Configuration section ConfigurationSection( isLaunchAtStartupEnabled: isLaunchAtStartupEnabled ) } .padding(.horizontal, 20) Spacer() // Bottom action buttons HStack(spacing: 10) { Button("Skip Setup") { print("🔴 Skip Setup button pressed") onComplete() } .buttonStyle(.bordered) .foregroundColor(.secondary) .controlSize(.small) Spacer() Button("Finished!") { print("🟢 Finished! button pressed") onComplete() } .buttonStyle(.borderedProminent) .controlSize(.small) } .padding(.top, 20) .padding(.bottom, 20) .padding(.horizontal, 20) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( VisualEffectBackground(material: .hudWindow, blending: .behindWindow) .cornerRadius(16) ) .padding(14) .onAppear(perform: checkPermissions) .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in print("App became active, re-checking permissions...") checkPermissions() } } private func checkPermissions() { // Check Screen Recording Permission let hasPermission = CGPreflightScreenCaptureAccess() if !hasPermission { CGRequestScreenCaptureAccess() } self.hasScreenRecordingPermission = hasPermission // Check Launch at Startup Status self.isLaunchAtStartupEnabled = SMAppService.mainApp.status == .enabled print("✅ Permissions check: ScreenRecording=\(self.hasScreenRecordingPermission), LaunchAtStartup=\(self.isLaunchAtStartupEnabled)") } } // MARK: - Compact Header View struct CompactHeaderView: View { var body: some View { VStack(spacing: 8) { // App logo - now using AppIcon instead of Slothhead Group { if let nsImage = NSImage(named: "AppIcon") { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) } else if let bundle = Bundle.main.url(forResource: "AppIcon", withExtension: "icns"), let nsImage = NSImage(contentsOf: bundle) { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) } else { Image(systemName: "camera.viewfinder") .font(.system(size: 40)) .foregroundColor(.blue) } } VStack(spacing: 2) { Text("Welcome to ShotScreen") .font(.title3) .fontWeight(.bold) .foregroundColor(.primary) Text("Set up your screenshot tool") .font(.caption) .foregroundColor(.secondary) } } } } // MARK: - Setup Section struct SetupSection: View { var hasScreenRecordingPermission: Bool var hasCompletedScreenshotSettings: Bool var onScreenshotSettingsCompleted: () -> Void var body: some View { VStack(spacing: 16) { // macOS Screenshot Settings with Instructions SimpleScreenshotCardWithInstructions( action: openMacOSScreenshotSettings, isCompleted: hasCompletedScreenshotSettings, onCompleted: onScreenshotSettingsCompleted ) // Privacy Permissions SinglePageCard( icon: "shield.checkerboard", title: "Grant Screen Recording Permission", subtitle: "Required for screenshot capture", actionTitle: "Open Privacy", action: openSystemPreferences, isCompleted: hasScreenRecordingPermission ) } } private func openMacOSScreenshotSettings() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.keyboard?Shortcuts") { NSWorkspace.shared.open(url) } else { print("❌ Could not open macOS Screenshot Settings") } } private func openSystemPreferences() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") { NSWorkspace.shared.open(url) } else { print("❌ Could not open System Preferences") } } } // MARK: - Simple Screenshot Card with Instructions struct SimpleScreenshotCardWithInstructions: View { let action: () -> Void var isCompleted: Bool var onCompleted: () -> Void @State private var showingInstructions = false var body: some View { HStack(alignment: .center, spacing: 12) { Image(systemName: "gear") .font(.system(size: 20)) .foregroundColor(.blue) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text("Disable macOS Screenshot Shortcut") .fontWeight(.medium) if isCompleted { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.subheadline) } } Text("Click Instructions for step-by-step guide") .font(.caption) .foregroundColor(.secondary) } Spacer() Button("Instructions") { print("📖 Instructions button pressed") showingInstructions = true } .buttonStyle(.borderedProminent) .controlSize(.small) } .padding(12) .background( RoundedRectangle(cornerRadius: 8) .fill(Color(NSColor.controlBackgroundColor)) .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) ) .sheet(isPresented: $showingInstructions) { InstructionsPopupView(openSettingsAction: action, isCompleted: isCompleted, onCompleted: onCompleted) } } } // MARK: - Instructions Popup View struct InstructionsPopupView: View { @Environment(\.presentationMode) var presentationMode let openSettingsAction: () -> Void var isCompleted: Bool var onCompleted: () -> Void @State private var imageSize: CGSize = CGSize(width: 600, height: 400) var body: some View { VStack(spacing: 20) { // Header HStack { Text("How to Disable macOS Screenshot Shortcut") .font(.title2) .fontWeight(.bold) Spacer() Button("Close") { presentationMode.wrappedValue.dismiss() } .buttonStyle(.bordered) .controlSize(.small) } .padding(.top, 20) .padding(.horizontal, 20) // Instruction image Group { if let bundle = Bundle.main.url(forResource: "images/Wizard_TurnOffSceenShot", withExtension: "png"), let nsImage = NSImage(contentsOf: bundle) { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fit) .onAppear { // Get actual image size and scale it appropriately let maxWidth: CGFloat = 800 let maxHeight: CGFloat = 600 let aspectRatio = nsImage.size.width / nsImage.size.height if nsImage.size.width > maxWidth { imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) } else if nsImage.size.height > maxHeight { imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) } else { imageSize = nsImage.size } } .frame(width: imageSize.width, height: imageSize.height) .background(Color.white) .cornerRadius(8) .shadow(radius: 2) } else if let nsImage = NSImage(named: "Wizard_TurnOffSceenShot") { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fit) .onAppear { // Get actual image size and scale it appropriately let maxWidth: CGFloat = 800 let maxHeight: CGFloat = 600 let aspectRatio = nsImage.size.width / nsImage.size.height if nsImage.size.width > maxWidth { imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) } else if nsImage.size.height > maxHeight { imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) } else { imageSize = nsImage.size } } .frame(width: imageSize.width, height: imageSize.height) .background(Color.white) .cornerRadius(8) .shadow(radius: 2) } else { // Development fallback: try to load from source directory let developmentImagePaths = [ "ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png", "./ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png", "../ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png" ] if let workingImagePath = developmentImagePaths.first(where: { FileManager.default.fileExists(atPath: $0) }), let nsImage = NSImage(contentsOfFile: workingImagePath) { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fit) .onAppear { // Get actual image size and scale it appropriately let maxWidth: CGFloat = 800 let maxHeight: CGFloat = 600 let aspectRatio = nsImage.size.width / nsImage.size.height if nsImage.size.width > maxWidth { imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) } else if nsImage.size.height > maxHeight { imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) } else { imageSize = nsImage.size } } .frame(width: imageSize.width, height: imageSize.height) .background(Color.white) .cornerRadius(8) .shadow(radius: 2) } else { // Final fallback if image is not found anywhere VStack(spacing: 10) { Image(systemName: "photo") .font(.system(size: 50)) .foregroundColor(.gray) Text("Instructions Image") .font(.caption) .foregroundColor(.secondary) Text("Image will be available in release build") .font(.caption2) .foregroundColor(.orange) } .frame(width: imageSize.width, height: imageSize.height) .background(Color.gray.opacity(0.1)) .cornerRadius(8) } } } .padding(.horizontal, 20) // Instructions text VStack(alignment: .leading, spacing: 8) { Text("Follow these steps:") .font(.headline) Text("1. Click 'Open Settings' below to open macOS System Preferences") Text("2. Navigate to Keyboard > Shortcuts > Screenshots") Text("3. Uncheck 'Save picture of selected area as a file' (⌘+⇧+4)") Text("4. This prevents conflicts with ShotScreen") } .font(.body) .padding(.horizontal, 20) .frame(maxWidth: .infinity, alignment: .leading) Spacer() // Bottom buttons HStack(spacing: 12) { Button("Open Settings") { print("🔧 Opening macOS Screenshot Settings from popup...") openSettingsAction() // Don't dismiss popup - let user read instructions while configuring } .buttonStyle(.borderedProminent) Button("Got it!") { print("✅ User completed screenshot settings configuration") onCompleted() presentationMode.wrappedValue.dismiss() } .buttonStyle(.bordered) } .padding(.bottom, 20) } .frame(width: max(imageSize.width + 40, 400), height: imageSize.height + 260) .background(Color(NSColor.windowBackgroundColor)) } } // MARK: - Configuration Section struct ConfigurationSection: View { var isLaunchAtStartupEnabled: Bool var body: some View { // Launch at Startup - now as a button SinglePageCard( icon: "power", title: "Launch at Startup", subtitle: "Start automatically when you log in", actionTitle: "Open Login Items", action: openLoginItemsSettings, isCompleted: isLaunchAtStartupEnabled ) } private func openLoginItemsSettings() { print("🔧 Opening Login Items Settings...") // Voor macOS 13+ - gebruik eenvoudige URL if #available(macOS 13.0, *) { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { NSWorkspace.shared.open(url) return } } // Voor macOS 12 en ouder if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { NSWorkspace.shared.open(url) } else { print("❌ Could not open Login Items Settings") } } } // MARK: - Single Page Card with Action struct SinglePageCard: View { let icon: String let title: String let subtitle: String let actionTitle: String let action: () -> Void var isCompleted: Bool var body: some View { HStack(alignment: .center, spacing: 12) { Image(systemName: icon) .font(.system(size: 20)) .foregroundColor(.blue) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(title) .fontWeight(.medium) if isCompleted { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.subheadline) } } Text(subtitle) .font(.caption) .foregroundColor(.secondary) } Spacer() Button(actionTitle) { print("🔧 Action button pressed: \(actionTitle)") action() } .buttonStyle(.borderedProminent) .controlSize(.small) } .padding(12) .background( RoundedRectangle(cornerRadius: 8) .fill(Color(NSColor.controlBackgroundColor)) .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) ) } } // MARK: - Visual Effect Background Helper // Note: VisualEffectBackground is defined in IntegratedGalleryView.swift