From 41a33906b360a2d47488e7d69788612f08c6e9a3 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Thu, 6 Mar 2025 15:54:11 +0545 Subject: [PATCH] fix: enhance app scanning to include applications in subfolders using FileManager.enumerator for recursive directory traversal --- VoiceInk.xcodeproj/project.pbxproj | 8 +- VoiceInk/Views/PowerModeView.swift | 326 +------------------ VoiceInk/Views/PowerModeViewComponents.swift | 310 ++++++++++++++++++ 3 files changed, 325 insertions(+), 319 deletions(-) create mode 100644 VoiceInk/Views/PowerModeViewComponents.swift diff --git a/VoiceInk.xcodeproj/project.pbxproj b/VoiceInk.xcodeproj/project.pbxproj index 114cc77..dca757a 100644 --- a/VoiceInk.xcodeproj/project.pbxproj +++ b/VoiceInk.xcodeproj/project.pbxproj @@ -468,7 +468,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 96; + CURRENT_PROJECT_VERSION = 99; DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\""; DEVELOPMENT_TEAM = V6J6A3VWY2; ENABLE_HARDENED_RUNTIME = YES; @@ -483,7 +483,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.97; + MARKETING_VERSION = 0.99; PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -501,7 +501,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 96; + CURRENT_PROJECT_VERSION = 99; DEVELOPMENT_ASSET_PATHS = "\"VoiceInk/Preview Content\""; DEVELOPMENT_TEAM = V6J6A3VWY2; ENABLE_HARDENED_RUNTIME = YES; @@ -516,7 +516,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 0.97; + MARKETING_VERSION = 0.99; PRODUCT_BUNDLE_IDENTIFIER = com.prakashjoshipax.VoiceInk; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/VoiceInk/Views/PowerModeView.swift b/VoiceInk/Views/PowerModeView.swift index 4646a91..646f674 100644 --- a/VoiceInk/Views/PowerModeView.swift +++ b/VoiceInk/Views/PowerModeView.swift @@ -260,12 +260,18 @@ struct ConfigurationSheet: View { let systemAppURLs = FileManager.default.urls(for: .applicationDirectory, in: .systemDomainMask) let allAppURLs = userAppURLs + systemAppURLs - let apps = allAppURLs.flatMap { url -> [URL] in - return (try? FileManager.default.contentsOfDirectory( - at: url, + let apps = allAppURLs.flatMap { baseURL -> [URL] in + let enumerator = FileManager.default.enumerator( + at: baseURL, includingPropertiesForKeys: [.isApplicationKey], - options: [.skipsHiddenFiles] - )) ?? [] + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) + + return enumerator?.compactMap { item -> URL? in + guard let url = item as? URL, + url.pathExtension == "app" else { return nil } + return url + } ?? [] } installedApps = apps.compactMap { url in @@ -449,313 +455,3 @@ struct PowerModeView: View { } } } - -// Supporting Views -struct PowerModeEmptyStateView: View { - @Binding var showAddModal: Bool - @Binding var configMode: ConfigurationMode? - - var body: some View { - VStack(spacing: 16) { - Image(systemName: "bolt.circle.fill") - .font(.system(size: 48)) - .foregroundColor(.secondary) - - Text("No Applications Configured") - .font(.title2) - .fontWeight(.semibold) - - Text("Add applications to customize their AI enhancement settings.") - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - Button(action: { - print("🔍 Empty state Add Application button clicked") - configMode = .add - print("🔍 Configuration mode set to: \(String(describing: configMode))") - showAddModal = true - print("🔍 Empty state showAddModal set to: \(showAddModal)") - }) { - Label("Add Application", systemImage: "plus.circle.fill") - .font(.headline) - } - .buttonStyle(.borderedProminent) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - } -} - -struct ConfiguredAppsGrid: View { - @ObservedObject var powerModeManager: PowerModeManager - @EnvironmentObject var enhancementService: AIEnhancementService - @State private var editingConfig: PowerModeConfig? - @State private var showingConfigSheet = false - - var body: some View { - ScrollView { - VStack(spacing: 8) { - ForEach(powerModeManager.configurations.sorted(by: { $0.appName.localizedCaseInsensitiveCompare($1.appName) == .orderedAscending })) { config in - ConfiguredAppRow( - config: config, - isEditing: editingConfig?.id == config.id, - action: { - editingConfig = config - showingConfigSheet = true - } - ) - .contextMenu { - Button(action: { - editingConfig = config - showingConfigSheet = true - }) { - Label("Edit", systemImage: "pencil") - } - Button(role: .destructive, action: { - powerModeManager.removeConfiguration(for: config.bundleIdentifier) - }) { - Label("Remove", systemImage: "trash") - } - } - } - } - .padding() - } - .sheet(isPresented: $showingConfigSheet, onDismiss: { editingConfig = nil }) { - if let config = editingConfig { - ConfigurationSheet( - mode: .edit(config), - isPresented: $showingConfigSheet, - powerModeManager: powerModeManager - ) - .environmentObject(enhancementService) - } - } - } -} - -struct ConfiguredAppRow: View { - let config: PowerModeConfig - let isEditing: Bool - let action: () -> Void - @EnvironmentObject var enhancementService: AIEnhancementService - - private var selectedPrompt: CustomPrompt? { - guard let promptId = config.selectedPrompt, - let uuid = UUID(uuidString: promptId) else { return nil } - return enhancementService.allPrompts.first { $0.id == uuid } - } - - private var isWebsiteConfig: Bool { - return config.urlConfigs != nil && !config.urlConfigs!.isEmpty - } - - var body: some View { - Button(action: action) { - HStack(spacing: 12) { - // Icon - if isWebsiteConfig { - Image(systemName: "globe") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 32, height: 32) - .foregroundColor(.accentColor) - } else if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: config.bundleIdentifier) { - Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path)) - .resizable() - .frame(width: 32, height: 32) - } - - // Info - VStack(alignment: .leading, spacing: 2) { - Text(config.appName) - .font(.headline) - if isWebsiteConfig { - if let urlConfig = config.urlConfigs?.first { - Text(urlConfig.url) - .font(.caption) - .foregroundColor(.secondary) - } - } else { - Text(config.bundleIdentifier) - .font(.caption) - .foregroundColor(.secondary) - } - } - - Spacer() - - HStack(spacing: 8) { - HStack(spacing: 4) { - Image(systemName: config.isAIEnhancementEnabled ? "checkmark.circle.fill" : "circle") - .foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary) - .font(.system(size: 14)) - Text("AI Enhancement") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(RoundedRectangle(cornerRadius: 6) - .fill(config.isAIEnhancementEnabled ? Color.accentColor.opacity(0.1) : Color.secondary.opacity(0.1))) - - if config.isAIEnhancementEnabled { - if let prompt = selectedPrompt { - HStack(spacing: 4) { - Image(systemName: prompt.icon.rawValue) - .foregroundColor(.accentColor) - .font(.system(size: 14)) - Text(prompt.title) - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(RoundedRectangle(cornerRadius: 6) - .fill(Color.accentColor.opacity(0.1))) - } else { - Text("No Prompt") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(RoundedRectangle(cornerRadius: 6) - .fill(Color.secondary.opacity(0.1))) - } - } - } - } - .contentShape(Rectangle()) - .padding(12) - .background(RoundedRectangle(cornerRadius: 8) - .fill(isEditing ? Color.accentColor.opacity(0.1) : Color(.windowBackgroundColor).opacity(0.4))) - .overlay(RoundedRectangle(cornerRadius: 8) - .stroke(isEditing ? Color.accentColor : Color.clear, lineWidth: 1)) - } - .buttonStyle(.plain) - } -} - -struct AppConfigurationFormView: View { - let appName: String? - let appIcon: NSImage? - let isDefaultConfig: Bool - @Binding var isAIEnhancementEnabled: Bool - @Binding var selectedPromptId: UUID? - @EnvironmentObject var enhancementService: AIEnhancementService - - var body: some View { - VStack(spacing: 20) { - VStack(spacing: 16) { - if !isDefaultConfig { - if let appIcon = appIcon { - HStack { - Image(nsImage: appIcon) - .resizable() - .frame(width: 32, height: 32) - Text(appName ?? "") - .font(.headline) - Spacer() - } - } - } else { - HStack { - Image(systemName: "gearshape.fill") - .font(.system(size: 24)) - .foregroundColor(.accentColor) - Text("Default Settings") - .font(.headline) - Spacer() - } - - Text("These settings will be applied to all applications that don't have specific configurations.") - .font(.subheadline) - .foregroundColor(.secondary) - .padding(.bottom, 8) - } - - Toggle("AI Enhancement", isOn: $isAIEnhancementEnabled) - } - .padding(.horizontal) - - if isAIEnhancementEnabled { - Divider() - - VStack(alignment: .leading) { - Text("Select Prompt") - .font(.headline) - .padding(.horizontal) - - let columns = [ - GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36) - ] - - LazyVGrid(columns: columns, spacing: 24) { - ForEach(enhancementService.allPrompts) { prompt in - prompt.promptIcon( - isSelected: selectedPromptId == prompt.id, - onTap: { selectedPromptId = prompt.id } - ) - } - } - .padding(.vertical, 12) - .padding(.horizontal, 16) - } - } - } - .padding(.vertical) - } -} - -struct AppGridItem: View { - let app: (url: URL, name: String, bundleId: String, icon: NSImage) - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - VStack(spacing: 8) { - Image(nsImage: app.icon) - .resizable() - .frame(width: 48, height: 48) - Text(app.name) - .font(.system(size: 12)) - .lineLimit(2) - .multilineTextAlignment(.center) - .frame(height: 32) - } - .frame(width: 100) - .padding(8) - .background(RoundedRectangle(cornerRadius: 8) - .fill(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)) - .overlay(RoundedRectangle(cornerRadius: 8) - .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 1)) - } - .buttonStyle(.plain) - } -} - -// New component for feature highlights -struct FeatureHighlight: View { - let icon: String - let title: String - let description: String - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 6) { - Image(systemName: icon) - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(.blue) - Text(title) - .font(.system(size: 13, weight: .semibold)) - } - - Text(description) - .font(.system(size: 12)) - .foregroundColor(.secondary) - .lineLimit(2) - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} diff --git a/VoiceInk/Views/PowerModeViewComponents.swift b/VoiceInk/Views/PowerModeViewComponents.swift new file mode 100644 index 0000000..9539d1c --- /dev/null +++ b/VoiceInk/Views/PowerModeViewComponents.swift @@ -0,0 +1,310 @@ +import SwiftUI +// Supporting Views +struct PowerModeEmptyStateView: View { + @Binding var showAddModal: Bool + @Binding var configMode: ConfigurationMode? + + var body: some View { + VStack(spacing: 16) { + Image(systemName: "bolt.circle.fill") + .font(.system(size: 48)) + .foregroundColor(.secondary) + + Text("No Applications Configured") + .font(.title2) + .fontWeight(.semibold) + + Text("Add applications to customize their AI enhancement settings.") + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button(action: { + print("🔍 Empty state Add Application button clicked") + configMode = .add + print("🔍 Configuration mode set to: \(String(describing: configMode))") + showAddModal = true + print("🔍 Empty state showAddModal set to: \(showAddModal)") + }) { + Label("Add Application", systemImage: "plus.circle.fill") + .font(.headline) + } + .buttonStyle(.borderedProminent) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + } +} + +struct ConfiguredAppsGrid: View { + @ObservedObject var powerModeManager: PowerModeManager + @EnvironmentObject var enhancementService: AIEnhancementService + @State private var editingConfig: PowerModeConfig? + @State private var showingConfigSheet = false + + var body: some View { + ScrollView { + VStack(spacing: 8) { + ForEach(powerModeManager.configurations.sorted(by: { $0.appName.localizedCaseInsensitiveCompare($1.appName) == .orderedAscending })) { config in + ConfiguredAppRow( + config: config, + isEditing: editingConfig?.id == config.id, + action: { + editingConfig = config + showingConfigSheet = true + } + ) + .contextMenu { + Button(action: { + editingConfig = config + showingConfigSheet = true + }) { + Label("Edit", systemImage: "pencil") + } + Button(role: .destructive, action: { + powerModeManager.removeConfiguration(for: config.bundleIdentifier) + }) { + Label("Remove", systemImage: "trash") + } + } + } + } + .padding() + } + .sheet(isPresented: $showingConfigSheet, onDismiss: { editingConfig = nil }) { + if let config = editingConfig { + ConfigurationSheet( + mode: .edit(config), + isPresented: $showingConfigSheet, + powerModeManager: powerModeManager + ) + .environmentObject(enhancementService) + } + } + } +} + +struct ConfiguredAppRow: View { + let config: PowerModeConfig + let isEditing: Bool + let action: () -> Void + @EnvironmentObject var enhancementService: AIEnhancementService + + private var selectedPrompt: CustomPrompt? { + guard let promptId = config.selectedPrompt, + let uuid = UUID(uuidString: promptId) else { return nil } + return enhancementService.allPrompts.first { $0.id == uuid } + } + + private var isWebsiteConfig: Bool { + return config.urlConfigs != nil && !config.urlConfigs!.isEmpty + } + + var body: some View { + Button(action: action) { + HStack(spacing: 12) { + // Icon + if isWebsiteConfig { + Image(systemName: "globe") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + .foregroundColor(.accentColor) + } else if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: config.bundleIdentifier) { + Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path)) + .resizable() + .frame(width: 32, height: 32) + } + + // Info + VStack(alignment: .leading, spacing: 2) { + Text(config.appName) + .font(.headline) + if isWebsiteConfig { + if let urlConfig = config.urlConfigs?.first { + Text(urlConfig.url) + .font(.caption) + .foregroundColor(.secondary) + } + } else { + Text(config.bundleIdentifier) + .font(.caption) + .foregroundColor(.secondary) + } + } + + Spacer() + + HStack(spacing: 8) { + HStack(spacing: 4) { + Image(systemName: config.isAIEnhancementEnabled ? "checkmark.circle.fill" : "circle") + .foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary) + .font(.system(size: 14)) + Text("AI Enhancement") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(config.isAIEnhancementEnabled ? .accentColor : .secondary) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 6) + .fill(config.isAIEnhancementEnabled ? Color.accentColor.opacity(0.1) : Color.secondary.opacity(0.1))) + + if config.isAIEnhancementEnabled { + if let prompt = selectedPrompt { + HStack(spacing: 4) { + Image(systemName: prompt.icon.rawValue) + .foregroundColor(.accentColor) + .font(.system(size: 14)) + Text(prompt.title) + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 6) + .fill(Color.accentColor.opacity(0.1))) + } else { + Text("No Prompt") + .font(.system(size: 12)) + .foregroundColor(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 6) + .fill(Color.secondary.opacity(0.1))) + } + } + } + } + .contentShape(Rectangle()) + .padding(12) + .background(RoundedRectangle(cornerRadius: 8) + .fill(isEditing ? Color.accentColor.opacity(0.1) : Color(.windowBackgroundColor).opacity(0.4))) + .overlay(RoundedRectangle(cornerRadius: 8) + .stroke(isEditing ? Color.accentColor : Color.clear, lineWidth: 1)) + } + .buttonStyle(.plain) + } +} + +struct AppConfigurationFormView: View { + let appName: String? + let appIcon: NSImage? + let isDefaultConfig: Bool + @Binding var isAIEnhancementEnabled: Bool + @Binding var selectedPromptId: UUID? + @EnvironmentObject var enhancementService: AIEnhancementService + + var body: some View { + VStack(spacing: 20) { + VStack(spacing: 16) { + if !isDefaultConfig { + if let appIcon = appIcon { + HStack { + Image(nsImage: appIcon) + .resizable() + .frame(width: 32, height: 32) + Text(appName ?? "") + .font(.headline) + Spacer() + } + } + } else { + HStack { + Image(systemName: "gearshape.fill") + .font(.system(size: 24)) + .foregroundColor(.accentColor) + Text("Default Settings") + .font(.headline) + Spacer() + } + + Text("These settings will be applied to all applications that don't have specific configurations.") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.bottom, 8) + } + + Toggle("AI Enhancement", isOn: $isAIEnhancementEnabled) + } + .padding(.horizontal) + + if isAIEnhancementEnabled { + Divider() + + VStack(alignment: .leading) { + Text("Select Prompt") + .font(.headline) + .padding(.horizontal) + + let columns = [ + GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36) + ] + + LazyVGrid(columns: columns, spacing: 24) { + ForEach(enhancementService.allPrompts) { prompt in + prompt.promptIcon( + isSelected: selectedPromptId == prompt.id, + onTap: { selectedPromptId = prompt.id } + ) + } + } + .padding(.vertical, 12) + .padding(.horizontal, 16) + } + } + } + .padding(.vertical) + } +} + +struct AppGridItem: View { + let app: (url: URL, name: String, bundleId: String, icon: NSImage) + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + Image(nsImage: app.icon) + .resizable() + .frame(width: 48, height: 48) + Text(app.name) + .font(.system(size: 12)) + .lineLimit(2) + .multilineTextAlignment(.center) + .frame(height: 32) + } + .frame(width: 100) + .padding(8) + .background(RoundedRectangle(cornerRadius: 8) + .fill(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)) + .overlay(RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 1)) + } + .buttonStyle(.plain) + } +} + +// New component for feature highlights +struct FeatureHighlight: View { + let icon: String + let title: String + let description: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.blue) + Text(title) + .font(.system(size: 13, weight: .semibold)) + } + + Text(description) + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(2) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +}