diff --git a/VoiceInk/PowerMode/PowerModeConfigView.swift b/VoiceInk/PowerMode/PowerModeConfigView.swift index f2d5928..7a1b6e9 100644 --- a/VoiceInk/PowerMode/PowerModeConfigView.swift +++ b/VoiceInk/PowerMode/PowerModeConfigView.swift @@ -159,14 +159,17 @@ struct ConfigurationView: View { .foregroundColor(.primary) .tint(.accentColor) .focused($isNameFieldFocused) - .onAppear { - isNameFieldFocused = true - } } .padding(.horizontal, 20) .padding(.vertical, 16) .background(CardBackground(isSelected: false)) .padding(.horizontal) + .onAppear { + // Add a small delay to ensure the view is fully loaded + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isNameFieldFocused = true + } + } // Enhanced Emoji Picker with Custom Emoji Support // if isShowingEmojiPicker { // <<< This conditional block will be removed diff --git a/VoiceInk/PowerMode/PowerModeView.swift b/VoiceInk/PowerMode/PowerModeView.swift index 669a068..d8fe164 100644 --- a/VoiceInk/PowerMode/PowerModeView.swift +++ b/VoiceInk/PowerMode/PowerModeView.swift @@ -73,103 +73,122 @@ struct PowerModeView: View { var body: some View { NavigationStack(path: $navigationPath) { - ScrollView { - VStack(spacing: 24) { - // Header Section - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .center) { - Text("Power Mode") - .font(.system(size: 22, weight: .bold)) + VStack(spacing: 0) { + // Header Section with proper macOS styling + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Text("Power Modes") + .font(.system(size: 28, weight: .bold, design: .default)) + .foregroundColor(.primary) + + // InfoTip for Power Mode + InfoTip( + title: "What is Power Mode?", + message: "Automatically apply custom configurations based on the app/website you are using", + learnMoreURL: "https://www.youtube.com/watch?v=-xFLvgNs_Iw" + ) + } - InfoTip( - title: "Power Mode", - message: "Create custom modes that automatically apply when using specific apps/websites.", - learnMoreURL: "https://www.youtube.com/watch?v=cEepexxgf6Y&t=10s" - ) - - Spacer() - } + Text("Automate your workflows with context-aware configurations.") + .font(.system(size: 14)) + .foregroundColor(.secondary) } - Text("Automatically apply custom configurations based on the app/website you are using") - .font(.subheadline) - .foregroundColor(.secondary) + Spacer() + + // Add button in header for better macOS UX + Button(action: { + configurationMode = .add + navigationPath.append(configurationMode!) + }) { + HStack(spacing: 6) { + Image(systemName: "plus") + .font(.system(size: 12, weight: .medium)) + Text("Add Power Mode") + .font(.system(size: 13, weight: .medium)) + } + .foregroundColor(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.accentColor) + .cornerRadius(6) + } + .buttonStyle(PlainButtonStyle()) } - .padding(.horizontal) - .padding(.top, 8) - - // Configurations Container - VStack(spacing: 0) { - // Custom Configurations Section - VStack(alignment: .leading, spacing: 16) { - Text("Custom Power Modes") - .font(.headline) - .foregroundColor(.primary) - .padding(.horizontal) - + } + .padding(.horizontal, 24) + .padding(.top, 20) + .padding(.bottom, 16) + + // Separator + Rectangle() + .fill(Color(NSColor.separatorColor)) + .frame(height: 1) + .padding(.horizontal, 24) + + // Main Content Area + GeometryReader { geometry in + ScrollView { + VStack(spacing: 0) { if powerModeManager.configurations.isEmpty { - VStack(spacing: 20) { - Image(systemName: "square.grid.2x2") - .font(.system(size: 36)) - .foregroundColor(.secondary) + // Empty State - Centered and symmetric + VStack(spacing: 24) { + Spacer() + .frame(height: geometry.size.height * 0.2) - Text("No power modes configured") - .font(.title3) - .fontWeight(.medium) + VStack(spacing: 16) { + Image(systemName: "square.grid.2x2.fill") + .font(.system(size: 48, weight: .regular)) + .foregroundColor(.secondary.opacity(0.6)) + + VStack(spacing: 8) { + Text("No Power Modes Yet") + .font(.system(size: 20, weight: .medium)) + .foregroundColor(.primary) + + Text("Create your first power mode to enhance your productivity\nwith context-aware AI assistance") + .font(.system(size: 14)) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .lineSpacing(2) + } + } - Text("Create a new power mode to get started.") - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) + Spacer() } .frame(maxWidth: .infinity) - .padding(.vertical, 30) + .frame(minHeight: geometry.size.height) } else { - PowerModeConfigurationsGrid( - powerModeManager: powerModeManager, - onEditConfig: { config in - configurationMode = .edit(config) - navigationPath.append(configurationMode!) - } - ) + // Configurations Grid with symmetric padding + VStack(spacing: 0) { + PowerModeConfigurationsGrid( + powerModeManager: powerModeManager, + onEditConfig: { config in + configurationMode = .edit(config) + navigationPath.append(configurationMode!) + } + ) + .padding(.horizontal, 24) + .padding(.vertical, 20) + + // Bottom spacing for visual balance + Spacer() + .frame(height: 40) + } } } - - Spacer(minLength: 24) - - // Add Configuration button at the bottom (centered) - HStack { - VoiceInkButton( - title: "Add New Power Mode", - action: { - configurationMode = .add - navigationPath.append(configurationMode!) - } - ) - } - .padding(.horizontal) - .padding(.bottom, 16) } - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color(NSColor.controlBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color(NSColor.separatorColor), lineWidth: 1) - ) - .shadow(color: Color(NSColor.shadowColor).opacity(0.05), radius: 5, y: 2) - .padding(.horizontal) } - .padding(.bottom, 24) } .background(Color(NSColor.controlBackgroundColor)) - .navigationTitle("") .navigationDestination(for: ConfigurationMode.self) { mode in ConfigurationView(mode: mode, powerModeManager: powerModeManager) } } } +} diff --git a/VoiceInk/PowerMode/PowerModeViewComponents.swift b/VoiceInk/PowerMode/PowerModeViewComponents.swift index 27ac6e0..2ebfae9 100644 --- a/VoiceInk/PowerMode/PowerModeViewComponents.swift +++ b/VoiceInk/PowerMode/PowerModeViewComponents.swift @@ -58,15 +58,12 @@ struct PowerModeConfigurationsGrid: View { var body: some View { LazyVStack(spacing: 12) { - ForEach(powerModeManager.configurations) { config in + ForEach($powerModeManager.configurations) { $config in ConfigurationRow( - config: config, + config: $config, isEditing: false, - isDefault: false, powerModeManager: powerModeManager, - action: { - onEditConfig(config) - } + onEditConfig: onEditConfig ) .contextMenu { Button(action: { @@ -87,11 +84,10 @@ struct PowerModeConfigurationsGrid: View { } struct ConfigurationRow: View { - @State var config: PowerModeConfig + @Binding var config: PowerModeConfig let isEditing: Bool - let isDefault: Bool let powerModeManager: PowerModeManager - let action: () -> Void + let onEditConfig: (PowerModeConfig) -> Void @EnvironmentObject var enhancementService: AIEnhancementService @EnvironmentObject var whisperState: WhisperState @@ -150,203 +146,174 @@ struct ConfigurationRow: View { } var body: some View { - HStack { - Button(action: action) { - VStack(spacing: 0) { - // Top row: Emoji, Name, and App/Website counts - HStack(spacing: 12) { - // Left: Emoji/Icon - ZStack { - Circle() - .fill(isDefault ? Color.accentColor.opacity(0.15) : Color(.controlBackgroundColor)) - .frame(width: 40, height: 40) - - if isDefault { - Image(systemName: "gearshape.fill") - .font(.system(size: 18)) - .foregroundColor(.accentColor) - } else { - Text(config.emoji) - .font(.system(size: 20)) - } - } + VStack(spacing: 0) { + // Top row: Emoji, Name, and App/Website counts + HStack(spacing: 12) { + // Left: Emoji/Icon + ZStack { + Circle() + .fill(Color(NSColor.controlBackgroundColor)) + .frame(width: 40, height: 40) - // Middle: Name and badge - VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 6) { - Text(config.name) - .font(.system(size: 15, weight: .semibold)) - - if isDefault { - Text("Default") - .font(.system(size: 10, weight: .medium)) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(Capsule().fill(Color.accentColor.opacity(0.15))) - .foregroundColor(.accentColor) - } - } - - if isDefault { - Text("Fallback power mode") - .font(.caption2) - .foregroundColor(.secondary) - } - } - - Spacer() - - // Right: App Icons and Website Count - if !isDefault { - HStack(alignment: .center, spacing: 6) { - // App Count - if appCount > 0 { - HStack(spacing: 3) { - Text(appText) - .font(.caption) - .foregroundColor(.secondary) - - Image(systemName: "app.fill") - .font(.system(size: 9)) - .foregroundColor(.secondary) - } - } - - // Website Count - if websiteCount > 0 { - HStack(spacing: 3) { - Text(websiteText) - .font(.caption) - .foregroundColor(.secondary) - - Image(systemName: "globe") - .font(.system(size: 9)) - .foregroundColor(.secondary) - } - } - } - } + Text(config.emoji) + .font(.system(size: 20)) } - .padding(.vertical, 12) - .padding(.horizontal, 14) - - // Only add divider and settings row if we have settings - if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled { - Divider() - .padding(.horizontal, 16) + + // Middle: Name and badge + VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 6) { + Text(config.name) + .font(.system(size: 15, weight: .semibold)) + } - // Settings badges in specified order - HStack(spacing: 8) { - // 1. Voice Model badge - if let model = selectedModel, model != "Default" { + // Display app and website counts + HStack(spacing: 12) { + if appCount > 0 { HStack(spacing: 4) { - Image(systemName: "waveform") + Image(systemName: "app.fill") .font(.system(size: 10)) - Text(model) - .font(.caption) + Text(appText) + .font(.caption2) } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) } - // 2. Language badge - if let language = selectedLanguage, language != "Default" { + if websiteCount > 0 { HStack(spacing: 4) { Image(systemName: "globe") .font(.system(size: 10)) - Text(language) - .font(.caption) + Text(websiteText) + .font(.caption2) } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) } - - // 3. AI Model badge if specified (moved before AI Enhancement) - if config.isAIEnhancementEnabled, let modelName = config.selectedAIModel, !modelName.isEmpty { - HStack(spacing: 4) { - Image(systemName: "cpu") - .font(.system(size: 10)) - // Display a shortened version of the model name if it's too long (increased limit) - Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName) - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) - } - - // 4. AI Enhancement badge - if config.isAIEnhancementEnabled { - // Context Awareness badge (moved before AI Enhancement) - if config.useScreenCapture { - HStack(spacing: 4) { - Image(systemName: "camera.viewfinder") - .font(.system(size: 10)) - Text("Context Awareness") - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) - } - - HStack(spacing: 4) { - Image(systemName: "sparkles") - .font(.system(size: 10)) - Text(selectedPrompt?.title ?? "AI") - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color.accentColor.opacity(0.1))) - .foregroundColor(.accentColor) - } - - Spacer() } - .padding(.vertical, 10) - .padding(.horizontal, 16) + .foregroundColor(.secondary) } - } - .background(CardBackground(isSelected: isEditing)) + + Spacer() + + // Right: Toggle Switch + Toggle("", isOn: $config.isEnabled) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .labelsHidden() } - .buttonStyle(.plain) + .padding(.vertical, 12) + .padding(.horizontal, 14) - Toggle("", isOn: $config.isEnabled) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .labelsHidden() - .onChange(of: config.isEnabled) { oldValue, newValue in - if newValue { - powerModeManager.enableConfiguration(with: config.id) - } else { - powerModeManager.disableConfiguration(with: config.id) + // Only add divider and settings row if we have settings + if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled { + Divider() + .padding(.horizontal, 16) + + // Settings badges in specified order + HStack(spacing: 8) { + // 1. Voice Model badge + if let model = selectedModel, model != "Default" { + HStack(spacing: 4) { + Image(systemName: "waveform") + .font(.system(size: 10)) + Text(model) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(NSColor.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) + ) } + + // 2. Language badge + if let language = selectedLanguage, language != "Default" { + HStack(spacing: 4) { + Image(systemName: "globe") + .font(.system(size: 10)) + Text(language) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(NSColor.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) + ) + } + + // 3. AI Model badge if specified (moved before AI Enhancement) + if config.isAIEnhancementEnabled, let modelName = config.selectedAIModel, !modelName.isEmpty { + HStack(spacing: 4) { + Image(systemName: "cpu") + .font(.system(size: 10)) + // Display a shortened version of the model name if it's too long (increased limit) + Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(NSColor.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) + ) + } + + // 4. AI Enhancement badge + if config.isAIEnhancementEnabled { + // Context Awareness badge (moved before AI Enhancement) + if config.useScreenCapture { + HStack(spacing: 4) { + Image(systemName: "camera.viewfinder") + .font(.system(size: 10)) + Text("Context Awareness") + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(NSColor.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) + ) + } + + HStack(spacing: 4) { + Image(systemName: "sparkles") + .font(.system(size: 10)) + Text(selectedPrompt?.title ?? "AI") + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color.accentColor.opacity(0.1))) + .foregroundColor(.accentColor) + } + + Spacer() } + .padding(.vertical, 10) + .padding(.horizontal, 16) + } + } + .background(CardBackground(isSelected: isEditing)) + .opacity(config.isEnabled ? 1.0 : 0.5) + .contextMenu { + Button(action: { + onEditConfig(config) + }) { + Label("Edit", systemImage: "pencil") } - .opacity(config.isEnabled ? 1.0 : 0.5) + Button(role: .destructive, action: { + powerModeManager.removeConfiguration(with: config.id) + }) { + Label("Remove", systemImage: "trash") + } + } } private var isSelected: Bool {