769 lines
40 KiB
Swift
769 lines
40 KiB
Swift
import SwiftUI
|
|
|
|
struct ConfigurationView: View {
|
|
let mode: ConfigurationMode
|
|
let powerModeManager: PowerModeManager
|
|
@EnvironmentObject var enhancementService: AIEnhancementService
|
|
@EnvironmentObject var aiService: AIService
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(\.presentationMode) private var presentationMode
|
|
@FocusState private var isNameFieldFocused: Bool
|
|
|
|
// State for configuration
|
|
@State private var configName: String = "New Power Mode"
|
|
@State private var selectedEmoji: String = "💼"
|
|
@State private var isShowingEmojiPicker = false
|
|
@State private var isShowingAppPicker = false
|
|
@State private var isAIEnhancementEnabled: Bool
|
|
@State private var selectedPromptId: UUID?
|
|
@State private var selectedTranscriptionModelName: String?
|
|
@State private var selectedLanguage: String?
|
|
@State private var installedApps: [(url: URL, name: String, bundleId: String, icon: NSImage)] = []
|
|
@State private var searchText = ""
|
|
|
|
// Validation state
|
|
@State private var validationErrors: [PowerModeValidationError] = []
|
|
@State private var showValidationAlert = false
|
|
|
|
// New state for AI provider and model
|
|
@State private var selectedAIProvider: String?
|
|
@State private var selectedAIModel: String?
|
|
|
|
// App and Website configurations
|
|
@State private var selectedAppConfigs: [AppConfig] = []
|
|
@State private var websiteConfigs: [URLConfig] = []
|
|
@State private var newWebsiteURL: String = ""
|
|
|
|
// New state for screen capture toggle
|
|
@State private var useScreenCapture = false
|
|
|
|
// State for prompt editing (similar to EnhancementSettingsView)
|
|
@State private var isEditingPrompt = false
|
|
@State private var selectedPromptForEdit: CustomPrompt?
|
|
|
|
// Whisper state for model selection
|
|
@EnvironmentObject private var whisperState: WhisperState
|
|
|
|
private var filteredApps: [(url: URL, name: String, bundleId: String, icon: NSImage)] {
|
|
if searchText.isEmpty {
|
|
return installedApps
|
|
}
|
|
return installedApps.filter { app in
|
|
app.name.localizedCaseInsensitiveContains(searchText) ||
|
|
app.bundleId.localizedCaseInsensitiveContains(searchText)
|
|
}
|
|
}
|
|
|
|
// Simplified computed property for effective model name
|
|
private var effectiveModelName: String? {
|
|
if let model = selectedTranscriptionModelName {
|
|
return model
|
|
}
|
|
return whisperState.currentTranscriptionModel?.name
|
|
}
|
|
|
|
init(mode: ConfigurationMode, powerModeManager: PowerModeManager) {
|
|
self.mode = mode
|
|
self.powerModeManager = powerModeManager
|
|
|
|
// Always fetch the most current configuration data
|
|
switch mode {
|
|
case .add:
|
|
_isAIEnhancementEnabled = State(initialValue: true)
|
|
_selectedPromptId = State(initialValue: nil)
|
|
_selectedTranscriptionModelName = State(initialValue: nil)
|
|
_selectedLanguage = State(initialValue: nil)
|
|
_configName = State(initialValue: "")
|
|
_selectedEmoji = State(initialValue: "✏️")
|
|
_useScreenCapture = State(initialValue: false)
|
|
// Default to current global AI provider/model for new configurations - use UserDefaults only
|
|
_selectedAIProvider = State(initialValue: UserDefaults.standard.string(forKey: "selectedAIProvider"))
|
|
_selectedAIModel = State(initialValue: nil) // Initialize to nil and set it after view appears
|
|
case .edit(let config):
|
|
// Get the latest version of this config from PowerModeManager
|
|
let latestConfig = powerModeManager.getConfiguration(with: config.id) ?? config
|
|
_isAIEnhancementEnabled = State(initialValue: latestConfig.isAIEnhancementEnabled)
|
|
_selectedPromptId = State(initialValue: latestConfig.selectedPrompt.flatMap { UUID(uuidString: $0) })
|
|
_selectedTranscriptionModelName = State(initialValue: latestConfig.selectedTranscriptionModelName)
|
|
_selectedLanguage = State(initialValue: latestConfig.selectedLanguage)
|
|
_configName = State(initialValue: latestConfig.name)
|
|
_selectedEmoji = State(initialValue: latestConfig.emoji)
|
|
_selectedAppConfigs = State(initialValue: latestConfig.appConfigs ?? [])
|
|
_websiteConfigs = State(initialValue: latestConfig.urlConfigs ?? [])
|
|
_useScreenCapture = State(initialValue: latestConfig.useScreenCapture)
|
|
_selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider)
|
|
_selectedAIModel = State(initialValue: latestConfig.selectedAIModel)
|
|
case .editDefault(let config):
|
|
// Always use the latest default config
|
|
let latestConfig = powerModeManager.defaultConfig
|
|
_isAIEnhancementEnabled = State(initialValue: latestConfig.isAIEnhancementEnabled)
|
|
_selectedPromptId = State(initialValue: latestConfig.selectedPrompt.flatMap { UUID(uuidString: $0) })
|
|
_selectedTranscriptionModelName = State(initialValue: latestConfig.selectedTranscriptionModelName)
|
|
_selectedLanguage = State(initialValue: latestConfig.selectedLanguage)
|
|
_configName = State(initialValue: latestConfig.name)
|
|
_selectedEmoji = State(initialValue: latestConfig.emoji)
|
|
_useScreenCapture = State(initialValue: latestConfig.useScreenCapture)
|
|
_selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider)
|
|
_selectedAIModel = State(initialValue: latestConfig.selectedAIModel)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Header with Title and Cancel button
|
|
HStack {
|
|
Text(mode.title)
|
|
.font(.largeTitle)
|
|
.fontWeight(.bold)
|
|
|
|
Spacer()
|
|
|
|
if case .edit(let config) = mode {
|
|
Button("Delete") {
|
|
powerModeManager.removeConfiguration(with: config.id)
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
.foregroundColor(.red)
|
|
.padding(.trailing, 8)
|
|
}
|
|
|
|
Button("Cancel") {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
.keyboardShortcut(.escape, modifiers: [])
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.top)
|
|
.padding(.bottom, 10)
|
|
|
|
Divider()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
// Main Input Section
|
|
HStack(spacing: 16) {
|
|
Button(action: {
|
|
isShowingEmojiPicker.toggle()
|
|
}) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.accentColor.opacity(0.15))
|
|
.frame(width: 48, height: 48)
|
|
|
|
Text(selectedEmoji)
|
|
.font(.system(size: 24))
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(mode.isEditingDefault)
|
|
.opacity(mode.isEditingDefault ? 0.5 : 1)
|
|
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
|
|
EmojiPickerView(
|
|
selectedEmoji: $selectedEmoji,
|
|
isPresented: $isShowingEmojiPicker
|
|
)
|
|
}
|
|
|
|
TextField("Name your power mode", text: $configName)
|
|
.font(.system(size: 18, weight: .bold))
|
|
.textFieldStyle(.plain)
|
|
.foregroundColor(.primary)
|
|
.tint(.accentColor)
|
|
.disabled(mode.isEditingDefault)
|
|
.focused($isNameFieldFocused)
|
|
.onAppear {
|
|
if !mode.isEditingDefault {
|
|
isNameFieldFocused = true
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 16)
|
|
.background(CardBackground(isSelected: false))
|
|
.padding(.horizontal)
|
|
|
|
// Enhanced Emoji Picker with Custom Emoji Support
|
|
// if isShowingEmojiPicker { // <<< This conditional block will be removed
|
|
// EmojiPickerView(
|
|
// selectedEmoji: $selectedEmoji,
|
|
// isPresented: $isShowingEmojiPicker
|
|
// )
|
|
// .padding(.horizontal)
|
|
// }
|
|
|
|
// SECTION 1: TRIGGERS
|
|
if !mode.isEditingDefault {
|
|
VStack(spacing: 16) {
|
|
// Section Header
|
|
SectionHeader(title: "When to Trigger")
|
|
|
|
// Applications Subsection
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Applications")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
Button(action: {
|
|
loadInstalledApps()
|
|
isShowingAppPicker = true
|
|
}) {
|
|
Label("Add App", systemImage: "plus.circle.fill")
|
|
.font(.subheadline)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
if selectedAppConfigs.isEmpty {
|
|
HStack {
|
|
Spacer()
|
|
Text("No applications added")
|
|
.foregroundColor(.secondary)
|
|
.font(.subheadline)
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.background(Color(.windowBackgroundColor).opacity(0.2))
|
|
.cornerRadius(8)
|
|
} else {
|
|
// Grid of selected apps that wraps to next line
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 50, maximum: 55), spacing: 10)], spacing: 10) {
|
|
ForEach(selectedAppConfigs) { appConfig in
|
|
VStack {
|
|
ZStack(alignment: .topTrailing) {
|
|
// App icon - completely filling the container
|
|
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) {
|
|
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
.frame(width: 50, height: 50)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
} else {
|
|
Image(systemName: "app.fill")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
.frame(width: 50, height: 50)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
|
|
// Remove button
|
|
Button(action: {
|
|
selectedAppConfigs.removeAll(where: { $0.id == appConfig.id })
|
|
}) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.white)
|
|
.background(Circle().fill(Color.black.opacity(0.6)))
|
|
}
|
|
.buttonStyle(.plain)
|
|
.offset(x: 6, y: -6)
|
|
}
|
|
}
|
|
.frame(width: 50, height: 50)
|
|
.background(CardBackground(isSelected: false, cornerRadius: 10))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Websites Subsection
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Websites")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
// Add URL Field
|
|
HStack {
|
|
TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onSubmit {
|
|
addWebsite()
|
|
}
|
|
|
|
Button(action: addWebsite) {
|
|
Image(systemName: "plus.circle.fill")
|
|
.foregroundColor(.accentColor)
|
|
.font(.system(size: 18))
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(newWebsiteURL.isEmpty)
|
|
}
|
|
|
|
if websiteConfigs.isEmpty {
|
|
HStack {
|
|
Spacer()
|
|
Text("No websites added")
|
|
.foregroundColor(.secondary)
|
|
.font(.subheadline)
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.background(Color(.windowBackgroundColor).opacity(0.2))
|
|
.cornerRadius(8)
|
|
} else {
|
|
// Grid of website tags that wraps to next line
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 160), spacing: 10)], spacing: 10) {
|
|
ForEach(websiteConfigs) { urlConfig in
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "globe")
|
|
.font(.system(size: 11))
|
|
.foregroundColor(.accentColor)
|
|
|
|
Text(urlConfig.url)
|
|
.font(.system(size: 11))
|
|
.lineLimit(1)
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
Button(action: {
|
|
websiteConfigs.removeAll(where: { $0.id == urlConfig.id })
|
|
}) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 9))
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 6)
|
|
.frame(height: 28)
|
|
.background(CardBackground(isSelected: false, cornerRadius: 10))
|
|
}
|
|
}
|
|
.padding(8)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color(.windowBackgroundColor).opacity(0.4))
|
|
)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// SECTION 2: TRANSCRIPTION
|
|
VStack(spacing: 16) {
|
|
// Section Header
|
|
SectionHeader(title: "Transcription")
|
|
|
|
// Whisper Model Selection Subsection
|
|
if whisperState.availableModels.isEmpty {
|
|
Text("No Whisper models available. Download models in the AI Models tab.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.padding()
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.background(Color(.windowBackgroundColor).opacity(0.2))
|
|
.cornerRadius(8)
|
|
} else {
|
|
// Create a simple binding that uses current model if nil
|
|
let modelBinding = Binding<String?>(
|
|
get: {
|
|
selectedTranscriptionModelName ?? whisperState.currentTranscriptionModel?.name ?? whisperState.availableModels.first?.name
|
|
},
|
|
set: { selectedTranscriptionModelName = $0 }
|
|
)
|
|
|
|
HStack {
|
|
Text("Model")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Picker("", selection: modelBinding) {
|
|
Text("Default (\(whisperState.currentTranscriptionModel?.displayName ?? "None"))")
|
|
.tag(nil as String?)
|
|
|
|
ForEach(whisperState.usableModels, id: \.name) { model in
|
|
Text(model.displayName).tag(model.name as String?)
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
// Language Selection Subsection
|
|
if let selectedModel = effectiveModelName,
|
|
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
|
modelInfo.isMultilingualModel {
|
|
|
|
// Create a simple binding that uses UserDefaults language if nil
|
|
let languageBinding = Binding<String?>(
|
|
get: {
|
|
selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "auto"
|
|
},
|
|
set: { selectedLanguage = $0 }
|
|
)
|
|
|
|
HStack {
|
|
Text("Language")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Picker("", selection: languageBinding) {
|
|
ForEach(modelInfo.supportedLanguages.sorted(by: {
|
|
if $0.key == "auto" { return true }
|
|
if $1.key == "auto" { return false }
|
|
return $0.value < $1.value
|
|
}), id: \.key) { key, value in
|
|
Text(value).tag(key as String?)
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
} else if let selectedModel = effectiveModelName,
|
|
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
|
!modelInfo.isMultilingualModel {
|
|
// Silently set to English without showing UI
|
|
EmptyView()
|
|
.onAppear {
|
|
selectedLanguage = "en"
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color(.windowBackgroundColor).opacity(0.4))
|
|
)
|
|
.padding(.horizontal)
|
|
|
|
// SECTION 3: AI ENHANCEMENT
|
|
VStack(spacing: 16) {
|
|
// Section Header
|
|
SectionHeader(title: "AI Enhancement")
|
|
|
|
Toggle("Enable AI Enhancement", isOn: $isAIEnhancementEnabled)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.onChange(of: isAIEnhancementEnabled) { oldValue, newValue in
|
|
if newValue {
|
|
// When enabling AI enhancement, set default values if none are selected
|
|
if selectedAIProvider == nil {
|
|
selectedAIProvider = aiService.selectedProvider.rawValue
|
|
}
|
|
if selectedAIModel == nil {
|
|
selectedAIModel = aiService.currentModel
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
// AI Provider Selection - Match style with Whisper model selection
|
|
// Create a binding for the provider selection that falls back to global settings
|
|
let providerBinding = Binding<AIProvider>(
|
|
get: {
|
|
if let providerName = selectedAIProvider,
|
|
let provider = AIProvider(rawValue: providerName) {
|
|
return provider
|
|
}
|
|
// Just return the global provider without modifying state
|
|
return aiService.selectedProvider
|
|
},
|
|
set: { newValue in
|
|
selectedAIProvider = newValue.rawValue // Update local state for UI responsiveness
|
|
aiService.selectedProvider = newValue // Update global AI service state
|
|
selectedAIModel = nil // Reset selected model when provider changes
|
|
}
|
|
)
|
|
|
|
|
|
|
|
|
|
if isAIEnhancementEnabled {
|
|
|
|
HStack {
|
|
Text("AI Provider")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
if aiService.connectedProviders.isEmpty {
|
|
Text("No providers connected")
|
|
.foregroundColor(.secondary)
|
|
.italic()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
Picker("", selection: providerBinding) {
|
|
ForEach(aiService.connectedProviders.filter { $0 != .elevenLabs }, id: \.self) { provider in
|
|
Text(provider.rawValue).tag(provider)
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.frame(maxWidth: .infinity)
|
|
.onChange(of: selectedAIProvider) { oldValue, newValue in
|
|
// When provider changes, ensure we have a valid model for that provider
|
|
if let provider = newValue.flatMap({ AIProvider(rawValue: $0) }) {
|
|
// Set default model for this provider
|
|
selectedAIModel = provider.defaultModel
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AI Model Selection - Match style with whisper language selection
|
|
let providerName = selectedAIProvider ?? aiService.selectedProvider.rawValue
|
|
if let provider = AIProvider(rawValue: providerName),
|
|
provider != .custom {
|
|
|
|
HStack {
|
|
Text("AI Model")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
if provider == .ollama && aiService.availableModels.isEmpty {
|
|
Text("No models available")
|
|
.foregroundColor(.secondary)
|
|
.italic()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
// Create binding that falls back to current model for the selected provider
|
|
let modelBinding = Binding<String>(
|
|
get: {
|
|
if let model = selectedAIModel, !model.isEmpty {
|
|
return model
|
|
}
|
|
// Just return the current model without modifying state
|
|
return aiService.currentModel
|
|
},
|
|
set: { newModelValue in
|
|
selectedAIModel = newModelValue // Update local state
|
|
// Update the model in AIService for the current provider
|
|
aiService.selectModel(newModelValue)
|
|
}
|
|
)
|
|
|
|
let models = provider == .ollama ? aiService.availableModels : provider.availableModels
|
|
|
|
Picker("", selection: modelBinding) {
|
|
ForEach(models, id: \.self) { model in
|
|
Text(model).tag(model)
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Enhancement Prompts Section (reused from EnhancementSettingsView)
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Enhancement Prompt")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
|
|
PromptSelectionGrid(
|
|
prompts: enhancementService.allPrompts,
|
|
selectedPromptId: selectedPromptId,
|
|
onPromptSelected: { prompt in
|
|
selectedPromptId = prompt.id
|
|
},
|
|
onEditPrompt: { prompt in
|
|
selectedPromptForEdit = prompt
|
|
},
|
|
onDeletePrompt: { prompt in
|
|
enhancementService.deletePrompt(prompt)
|
|
},
|
|
onAddNewPrompt: {
|
|
isEditingPrompt = true
|
|
}
|
|
)
|
|
}
|
|
|
|
Divider()
|
|
|
|
|
|
Toggle("Context Awareness", isOn: $useScreenCapture)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color(.windowBackgroundColor).opacity(0.4))
|
|
)
|
|
.padding(.horizontal)
|
|
|
|
// Save Button
|
|
VoiceInkButton(
|
|
title: mode.isAdding ? "Add New Power Mode" : "Save Changes",
|
|
action: saveConfiguration,
|
|
isDisabled: !canSave
|
|
)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.horizontal)
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
}
|
|
.sheet(isPresented: $isShowingAppPicker) {
|
|
AppPickerSheet(
|
|
installedApps: filteredApps,
|
|
selectedAppConfigs: $selectedAppConfigs,
|
|
searchText: $searchText,
|
|
onDismiss: { isShowingAppPicker = false }
|
|
)
|
|
}
|
|
.sheet(isPresented: $isEditingPrompt) {
|
|
PromptEditorView(mode: .add)
|
|
}
|
|
.sheet(item: $selectedPromptForEdit) { prompt in
|
|
PromptEditorView(mode: .edit(prompt))
|
|
}
|
|
.powerModeValidationAlert(errors: validationErrors, isPresented: $showValidationAlert)
|
|
.navigationTitle("") // Explicitly set an empty title for this view
|
|
.toolbar(.hidden) // Attempt to hide the navigation bar area
|
|
.onAppear {
|
|
// Set AI provider and model for new power modes after environment objects are available
|
|
if case .add = mode {
|
|
if selectedAIProvider == nil {
|
|
selectedAIProvider = aiService.selectedProvider.rawValue
|
|
}
|
|
if selectedAIModel == nil || selectedAIModel?.isEmpty == true {
|
|
selectedAIModel = aiService.currentModel
|
|
}
|
|
}
|
|
|
|
// Select first prompt if AI enhancement is enabled and no prompt is selected
|
|
if isAIEnhancementEnabled && selectedPromptId == nil {
|
|
selectedPromptId = enhancementService.allPrompts.first?.id
|
|
}
|
|
}
|
|
}
|
|
|
|
private var canSave: Bool {
|
|
return !configName.isEmpty
|
|
}
|
|
|
|
private func addWebsite() {
|
|
guard !newWebsiteURL.isEmpty else { return }
|
|
|
|
let cleanedURL = powerModeManager.cleanURL(newWebsiteURL)
|
|
let urlConfig = URLConfig(url: cleanedURL)
|
|
websiteConfigs.append(urlConfig)
|
|
newWebsiteURL = ""
|
|
}
|
|
|
|
private func toggleAppSelection(_ app: (url: URL, name: String, bundleId: String, icon: NSImage)) {
|
|
if let index = selectedAppConfigs.firstIndex(where: { $0.bundleIdentifier == app.bundleId }) {
|
|
selectedAppConfigs.remove(at: index)
|
|
} else {
|
|
let appConfig = AppConfig(bundleIdentifier: app.bundleId, appName: app.name)
|
|
selectedAppConfigs.append(appConfig)
|
|
}
|
|
}
|
|
|
|
private func getConfigForForm() -> PowerModeConfig {
|
|
switch mode {
|
|
case .add:
|
|
return PowerModeConfig(
|
|
name: configName,
|
|
emoji: selectedEmoji,
|
|
appConfigs: selectedAppConfigs.isEmpty ? nil : selectedAppConfigs,
|
|
urlConfigs: websiteConfigs.isEmpty ? nil : websiteConfigs,
|
|
isAIEnhancementEnabled: isAIEnhancementEnabled,
|
|
selectedPrompt: selectedPromptId?.uuidString,
|
|
selectedTranscriptionModelName: selectedTranscriptionModelName,
|
|
selectedLanguage: selectedLanguage,
|
|
useScreenCapture: useScreenCapture,
|
|
selectedAIProvider: selectedAIProvider,
|
|
selectedAIModel: selectedAIModel
|
|
)
|
|
case .edit(let config):
|
|
var updatedConfig = config
|
|
updatedConfig.name = configName
|
|
updatedConfig.emoji = selectedEmoji
|
|
updatedConfig.isAIEnhancementEnabled = isAIEnhancementEnabled
|
|
updatedConfig.selectedPrompt = selectedPromptId?.uuidString
|
|
updatedConfig.selectedTranscriptionModelName = selectedTranscriptionModelName
|
|
updatedConfig.selectedLanguage = selectedLanguage
|
|
updatedConfig.appConfigs = selectedAppConfigs.isEmpty ? nil : selectedAppConfigs
|
|
updatedConfig.urlConfigs = websiteConfigs.isEmpty ? nil : websiteConfigs
|
|
updatedConfig.useScreenCapture = useScreenCapture
|
|
updatedConfig.selectedAIProvider = selectedAIProvider
|
|
updatedConfig.selectedAIModel = selectedAIModel
|
|
return updatedConfig
|
|
|
|
case .editDefault(let config):
|
|
var updatedConfig = config
|
|
updatedConfig.name = configName
|
|
updatedConfig.emoji = selectedEmoji
|
|
updatedConfig.isAIEnhancementEnabled = isAIEnhancementEnabled
|
|
updatedConfig.selectedPrompt = selectedPromptId?.uuidString
|
|
updatedConfig.selectedTranscriptionModelName = selectedTranscriptionModelName
|
|
updatedConfig.selectedLanguage = selectedLanguage
|
|
updatedConfig.useScreenCapture = useScreenCapture
|
|
updatedConfig.selectedAIProvider = selectedAIProvider
|
|
updatedConfig.selectedAIModel = selectedAIModel
|
|
return updatedConfig
|
|
}
|
|
}
|
|
|
|
private func loadInstalledApps() {
|
|
// Get both user-installed and system applications
|
|
let userAppURLs = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask)
|
|
let systemAppURLs = FileManager.default.urls(for: .applicationDirectory, in: .systemDomainMask)
|
|
let allAppURLs = userAppURLs + systemAppURLs
|
|
|
|
let apps = allAppURLs.flatMap { baseURL -> [URL] in
|
|
let enumerator = FileManager.default.enumerator(
|
|
at: baseURL,
|
|
includingPropertiesForKeys: [.isApplicationKey],
|
|
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
|
|
guard let bundle = Bundle(url: url),
|
|
let bundleId = bundle.bundleIdentifier,
|
|
let name = (bundle.infoDictionary?["CFBundleName"] as? String) ??
|
|
(bundle.infoDictionary?["CFBundleDisplayName"] as? String) else {
|
|
return nil
|
|
}
|
|
|
|
let icon = NSWorkspace.shared.icon(forFile: url.path)
|
|
return (url: url, name: name, bundleId: bundleId, icon: icon)
|
|
}
|
|
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
|
}
|
|
|
|
private func saveConfiguration() {
|
|
|
|
|
|
let config = getConfigForForm()
|
|
|
|
// Only validate when the user explicitly tries to save
|
|
let validator = PowerModeValidator(powerModeManager: powerModeManager)
|
|
validationErrors = validator.validateForSave(config: config, mode: mode)
|
|
|
|
if !validationErrors.isEmpty {
|
|
showValidationAlert = true
|
|
return
|
|
}
|
|
|
|
// If validation passes, save the configuration
|
|
switch mode {
|
|
case .add:
|
|
powerModeManager.addConfiguration(config)
|
|
case .edit, .editDefault:
|
|
powerModeManager.updateConfiguration(config)
|
|
}
|
|
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
}
|