import SwiftUI import CoreModels import CoreSTT import CoreUtils import CorePermissions class PreferencesWindowController: NSWindowController { private let modelManager: ModelManager private let whisperEngine: WhisperCPPEngine private let permissionManager: PermissionManager private var preferencesView: PreferencesView? init(modelManager: ModelManager, whisperEngine: WhisperCPPEngine, permissionManager: PermissionManager, initialTab: Int = 0) { self.modelManager = modelManager self.whisperEngine = whisperEngine self.permissionManager = permissionManager let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 600, height: 500), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) super.init(window: window) window.title = "MenuWhisper Preferences" window.center() preferencesView = PreferencesView( modelManager: modelManager, whisperEngine: whisperEngine, permissionManager: permissionManager, initialTab: initialTab, onClose: { [weak self] in self?.close() } ) window.contentView = NSHostingView(rootView: preferencesView!) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setSelectedTab(_ tabIndex: Int) { preferencesView?.setSelectedTab(tabIndex) } } struct PreferencesView: View { @ObservedObject var modelManager: ModelManager let whisperEngine: WhisperCPPEngine @ObservedObject var permissionManager: PermissionManager let onClose: () -> Void @State private var selectedTab: Int @State private var isDownloading: [String: Bool] = [:] @State private var downloadProgress: [String: Double] = [:] @State private var showingDeleteAlert = false @State private var modelToDelete: ModelInfo? init(modelManager: ModelManager, whisperEngine: WhisperCPPEngine, permissionManager: PermissionManager, initialTab: Int = 0, onClose: @escaping () -> Void) { self.modelManager = modelManager self.whisperEngine = whisperEngine self.permissionManager = permissionManager self.onClose = onClose self._selectedTab = State(initialValue: initialTab) } func setSelectedTab(_ tabIndex: Int) { selectedTab = tabIndex } var body: some View { TabView(selection: $selectedTab) { ModelsTab( modelManager: modelManager, whisperEngine: whisperEngine, isDownloading: $isDownloading, downloadProgress: $downloadProgress, showingDeleteAlert: $showingDeleteAlert, modelToDelete: $modelToDelete ) .tabItem { Label("Models", systemImage: "brain.head.profile") } .tag(0) PermissionsTab(permissionManager: permissionManager) .tabItem { Label("Permissions", systemImage: "lock.shield") } .tag(1) GeneralTab() .tabItem { Label("General", systemImage: "gearshape") } .tag(2) } .frame(width: 600, height: 500) .alert("Delete Model", isPresented: $showingDeleteAlert) { Button("Cancel", role: .cancel) { modelToDelete = nil } Button("Delete", role: .destructive) { if let model = modelToDelete { deleteModel(model) } modelToDelete = nil } } message: { if let model = modelToDelete { Text("Are you sure you want to delete '\(model.name)'? This action cannot be undone.") } } } private func deleteModel(_ model: ModelInfo) { do { try modelManager.deleteModel(model) } catch { print("Failed to delete model: \(error)") } } } struct ModelsTab: View { @ObservedObject var modelManager: ModelManager let whisperEngine: WhisperCPPEngine @Binding var isDownloading: [String: Bool] @Binding var downloadProgress: [String: Double] @Binding var showingDeleteAlert: Bool @Binding var modelToDelete: ModelInfo? var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Speech Recognition Models") .font(.title2) .fontWeight(.semibold) Text("Download and manage speech recognition models. Larger models provide better accuracy but use more memory and processing time.") .font(.caption) .foregroundColor(.secondary) // Current Model Status VStack(alignment: .leading, spacing: 8) { Text("Current Model") .font(.headline) if let activeModel = modelManager.activeModel { HStack { VStack(alignment: .leading) { Text(activeModel.name) .font(.body) .fontWeight(.medium) Text("\(activeModel.sizeMB) MB • \(activeModel.qualityTier) quality • \(activeModel.estimatedRAM)") .font(.caption) .foregroundColor(.secondary) } Spacer() Circle() .fill(whisperEngine.isModelLoaded() ? Color.green : Color.orange) .frame(width: 8, height: 8) Text(whisperEngine.isModelLoaded() ? "Loaded" : "Loading...") .font(.caption) .foregroundColor(whisperEngine.isModelLoaded() ? .green : .orange) } .padding(12) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(8) } else { Text("No model selected") .foregroundColor(.secondary) .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(8) } } // Available Models VStack(alignment: .leading, spacing: 8) { Text("Available Models") .font(.headline) ScrollView { LazyVStack(spacing: 8) { ForEach(modelManager.availableModels) { model in ModelRow( model: model, modelManager: modelManager, whisperEngine: whisperEngine, isDownloading: isDownloading[model.name] ?? false, downloadProgress: downloadProgress[model.name] ?? 0.0, onDownload: { downloadModel(model) }, onSelect: { selectModel(model) }, onDelete: { modelToDelete = model showingDeleteAlert = true } ) } } } .frame(maxHeight: 200) } Spacer() } .padding(20) } private func downloadModel(_ model: ModelInfo) { isDownloading[model.name] = true downloadProgress[model.name] = 0.0 Task { do { try await modelManager.downloadModel(model) { progress in DispatchQueue.main.async { downloadProgress[model.name] = progress.progress } } DispatchQueue.main.async { isDownloading[model.name] = false downloadProgress[model.name] = 1.0 } } catch { DispatchQueue.main.async { isDownloading[model.name] = false downloadProgress[model.name] = 0.0 } print("Download failed: \(error)") } } } private func selectModel(_ model: ModelInfo) { modelManager.setActiveModel(model) Task { do { if let modelPath = modelManager.getModelPath(for: model) { try await whisperEngine.loadModel(at: modelPath) } } catch { print("Failed to load model: \(error)") } } } } struct ModelRow: View { let model: ModelInfo @ObservedObject var modelManager: ModelManager let whisperEngine: WhisperCPPEngine let isDownloading: Bool let downloadProgress: Double let onDownload: () -> Void let onSelect: () -> Void let onDelete: () -> Void private var isActive: Bool { modelManager.activeModel?.name == model.name } var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { HStack { Text(model.name) .font(.body) .fontWeight(.medium) if isActive { Text("ACTIVE") .font(.caption) .fontWeight(.semibold) .foregroundColor(.white) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Color.blue) .cornerRadius(4) } } Text("\(model.sizeMB) MB • \(model.qualityTier) quality • \(model.estimatedRAM)") .font(.caption) .foregroundColor(.secondary) if !model.notes.isEmpty { Text(model.notes) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } } Spacer() VStack(spacing: 8) { if model.isDownloaded { HStack(spacing: 8) { if !isActive { Button("Select") { onSelect() } .buttonStyle(.bordered) } Button("Delete") { onDelete() } .buttonStyle(.bordered) .foregroundColor(.red) } } else { if isDownloading { VStack { ProgressView(value: downloadProgress) .frame(width: 80) Text("\(Int(downloadProgress * 100))%") .font(.caption) } } else { Button("Download") { onDownload() } .buttonStyle(.bordered) } } } } .padding(12) .background(isActive ? Color.blue.opacity(0.1) : Color(NSColor.controlBackgroundColor)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isActive ? Color.blue : Color.clear, lineWidth: 2) ) } } struct PermissionsTab: View { @ObservedObject var permissionManager: PermissionManager var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Permissions") .font(.title2) .fontWeight(.semibold) Text("MenuWhisper requires certain system permissions to function properly. Click the buttons below to grant permissions in System Settings.") .font(.caption) .foregroundColor(.secondary) VStack(alignment: .leading, spacing: 12) { // Microphone Permission PermissionRow( title: "Microphone", description: "Required to capture speech for transcription", status: permissionManager.microphoneStatus, onOpenSettings: { permissionManager.openSystemSettings(for: .microphone) }, onRefresh: { permissionManager.checkAllPermissions() } ) Divider() // Accessibility Permission PermissionRow( title: "Accessibility", description: "Required to insert transcribed text into other applications", status: permissionManager.accessibilityStatus, onOpenSettings: { permissionManager.openSystemSettings(for: .accessibility) }, onRefresh: { permissionManager.checkAllPermissions() } ) Divider() // Input Monitoring Permission PermissionRow( title: "Input Monitoring", description: "Required to send keyboard events for text insertion", status: permissionManager.inputMonitoringStatus, onOpenSettings: { permissionManager.openSystemSettings(for: .inputMonitoring) }, onRefresh: { permissionManager.checkAllPermissions() } ) } .padding(16) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(12) // Help text VStack(alignment: .leading, spacing: 8) { Text("Need Help?") .font(.headline) Text("After granting permissions in System Settings:") .font(.body) .foregroundColor(.secondary) VStack(alignment: .leading, spacing: 4) { Text("1. Close System Settings") Text("2. Click 'Refresh Status' to update permission status") Text("3. Some permissions may require restarting MenuWhisper") } .font(.caption) .foregroundColor(.secondary) .padding(.leading, 8) } Spacer() } .padding(20) } } struct PermissionRow: View { let title: String let description: String let status: PermissionStatus let onOpenSettings: () -> Void let onRefresh: () -> Void var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { HStack { Text(title) .font(.body) .fontWeight(.medium) Spacer() // Status indicator HStack(spacing: 4) { Circle() .fill(statusColor) .frame(width: 8, height: 8) Text(statusText) .font(.caption) .fontWeight(.medium) .foregroundColor(statusColor) } } Text(description) .font(.caption) .foregroundColor(.secondary) } Spacer() VStack(spacing: 6) { if status != .granted { Button("Open System Settings") { onOpenSettings() } .buttonStyle(.bordered) .controlSize(.small) } Button("Refresh Status") { onRefresh() } .buttonStyle(.borderless) .controlSize(.small) .foregroundColor(.secondary) } } } private var statusColor: Color { switch status { case .granted: return .green case .denied: return .red case .notDetermined, .restricted: return .orange } } private var statusText: String { switch status { case .granted: return "Granted" case .denied: return "Denied" case .notDetermined: return "Not Set" case .restricted: return "Restricted" } } } struct GeneralTab: View { var body: some View { VStack(alignment: .leading, spacing: 16) { Text("General Settings") .font(.title2) .fontWeight(.semibold) Text("Additional settings will be available in Phase 4.") .font(.body) .foregroundColor(.secondary) Spacer() } .padding(20) } }