diff --git a/VoiceInk.xcodeproj/project.pbxproj b/VoiceInk.xcodeproj/project.pbxproj index 2442f26..ae8bd8c 100644 --- a/VoiceInk.xcodeproj/project.pbxproj +++ b/VoiceInk.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + E10340512DE1F666008BCBE5 /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E136D0102DA3EE57000E1E8A /* whisper.xcframework */; }; + E10340522DE1F666008BCBE5 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E136D0102DA3EE57000E1E8A /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = E1A261112CC143AC00B233D1 /* KeyboardShortcuts */; }; E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; }; E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; }; - E1C7A8112DE06FC60034EDA0 /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E11CB51D2DB1F8AF00F9F3ED /* whisper.xcframework */; }; - E1C7A8122DE06FC60034EDA0 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E11CB51D2DB1F8AF00F9F3ED /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = E1F5FA792DA6CBF900B1FD8A /* Zip */; }; /* End PBXBuildFile section */ @@ -39,7 +39,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - E1C7A8122DE06FC60034EDA0 /* whisper.xcframework in Embed Frameworks */, + E10340522DE1F666008BCBE5 /* whisper.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -80,7 +80,7 @@ E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */, E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */, E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */, - E1C7A8112DE06FC60034EDA0 /* whisper.xcframework in Frameworks */, + E10340512DE1F666008BCBE5 /* whisper.xcframework in Frameworks */, E1F5FA7A2DA6CBF900B1FD8A /* Zip in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/VoiceInk/PowerMode/PowerModeConfig.swift b/VoiceInk/PowerMode/PowerModeConfig.swift index 65b292d..3ef72ee 100644 --- a/VoiceInk/PowerMode/PowerModeConfig.swift +++ b/VoiceInk/PowerMode/PowerModeConfig.swift @@ -76,10 +76,12 @@ class PowerModeManager: ObservableObject { @Published var configurations: [PowerModeConfig] = [] @Published var defaultConfig: PowerModeConfig @Published var isPowerModeEnabled: Bool + @Published var activeConfiguration: PowerModeConfig? private let configKey = "powerModeConfigurationsV2" private let defaultConfigKey = "defaultPowerModeConfigV2" private let powerModeEnabledKey = "isPowerModeEnabled" + private let activeConfigIdKey = "activeConfigurationId" private init() { // Load power mode enabled state or default to false if not set @@ -111,6 +113,20 @@ class PowerModeManager: ObservableObject { saveDefaultConfig() } loadConfigurations() + + // Set the active configuration, either from saved ID or default to the default config + if let activeConfigIdString = UserDefaults.standard.string(forKey: activeConfigIdKey), + let activeConfigId = UUID(uuidString: activeConfigIdString) { + if let savedConfig = configurations.first(where: { $0.id == activeConfigId }) { + activeConfiguration = savedConfig + } else if activeConfigId == defaultConfig.id { + activeConfiguration = defaultConfig + } else { + activeConfiguration = defaultConfig + } + } else { + activeConfiguration = defaultConfig + } } private func loadConfigurations() { @@ -237,4 +253,16 @@ class PowerModeManager: ObservableObject { func savePowerModeEnabled() { UserDefaults.standard.set(isPowerModeEnabled, forKey: powerModeEnabledKey) } + + // Set active configuration + func setActiveConfiguration(_ config: PowerModeConfig) { + activeConfiguration = config + UserDefaults.standard.set(config.id.uuidString, forKey: activeConfigIdKey) + self.objectWillChange.send() + } + + // Get current active configuration + var currentActiveConfiguration: PowerModeConfig { + return activeConfiguration ?? defaultConfig + } } \ No newline at end of file diff --git a/VoiceInk/PowerMode/PowerModePopover.swift b/VoiceInk/PowerMode/PowerModePopover.swift new file mode 100644 index 0000000..f6f2468 --- /dev/null +++ b/VoiceInk/PowerMode/PowerModePopover.swift @@ -0,0 +1,91 @@ +import SwiftUI + +// Power Mode Popover for recorder views +struct PowerModePopover: View { + @ObservedObject var powerModeManager = PowerModeManager.shared + @State private var selectedConfig: PowerModeConfig? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Select Power Mode") + .font(.headline) + .foregroundColor(.white.opacity(0.9)) + .padding(.horizontal) + .padding(.top, 8) + + Divider() + .background(Color.white.opacity(0.1)) + + ScrollView { + VStack(alignment: .leading, spacing: 4) { + // Default Configuration + PowerModeRow( + config: powerModeManager.defaultConfig, + isSelected: selectedConfig?.id == powerModeManager.defaultConfig.id, + action: { + powerModeManager.setActiveConfiguration(powerModeManager.defaultConfig) + selectedConfig = powerModeManager.defaultConfig + } + ) + + // Custom Configurations + ForEach(powerModeManager.configurations) { config in + PowerModeRow( + config: config, + isSelected: selectedConfig?.id == config.id, + action: { + powerModeManager.setActiveConfiguration(config) + selectedConfig = config + } + ) + } + } + .padding(.horizontal) + } + } + .frame(width: 180) + .frame(maxHeight: 300) + .padding(.vertical, 8) + .background(Color.black) + .environment(\.colorScheme, .dark) + .onAppear { + // Set the initially selected configuration + selectedConfig = powerModeManager.activeConfiguration + } + } +} + +// Row view for each power mode in the popover +struct PowerModeRow: View { + let config: PowerModeConfig + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + // Always use the emoji from the configuration + Text(config.emoji) + .font(.system(size: 14)) + + Text(config.name) + .foregroundColor(.white.opacity(0.9)) + .font(.system(size: 13)) + .lineLimit(1) + + if isSelected { + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.green) + .font(.system(size: 10)) + } + } + .contentShape(Rectangle()) + .padding(.vertical, 4) + .padding(.horizontal, 8) + } + .buttonStyle(.plain) + .background(isSelected ? Color.white.opacity(0.1) : Color.clear) + .cornerRadius(4) + } +} \ No newline at end of file diff --git a/VoiceInk/Views/MiniRecorderView.swift b/VoiceInk/Views/MiniRecorderView.swift index 1558a0e..645ea2c 100644 --- a/VoiceInk/Views/MiniRecorderView.swift +++ b/VoiceInk/Views/MiniRecorderView.swift @@ -4,7 +4,8 @@ struct MiniRecorderView: View { @ObservedObject var whisperState: WhisperState @ObservedObject var recorder: Recorder @EnvironmentObject var windowManager: MiniWindowManager - @State private var showPromptPopover = false + @State private var showPowerModePopover = false + @ObservedObject private var powerModeManager = PowerModeManager.shared var body: some View { Group { @@ -49,36 +50,7 @@ struct MiniRecorderView: View { .frame(width: 18) .padding(.leading, -4) - // AI Enhancement Toggle - if let enhancementService = whisperState.getEnhancementService() { - NotchToggleButton( - isEnabled: enhancementService.isEnhancementEnabled, - icon: "sparkles", - color: .blue - ) { - enhancementService.isEnhancementEnabled.toggle() - } - .frame(width: 18) - .disabled(!enhancementService.isConfigured) - } - - // Custom Prompt Toggle and Selector - if let enhancementService = whisperState.getEnhancementService() { - NotchToggleButton( - isEnabled: enhancementService.isEnhancementEnabled, - icon: enhancementService.activePrompt?.icon.rawValue ?? "text.badge.checkmark", - color: .green - ) { - showPromptPopover.toggle() - } - .frame(width: 18) - .disabled(!enhancementService.isEnhancementEnabled) - .popover(isPresented: $showPromptPopover, arrowEdge: .bottom) { - NotchPromptPopover(enhancementService: enhancementService) - } - } - - // Visualizer + // Visualizer - moved to middle position Group { if whisperState.isProcessing { NotchStaticVisualizer(color: .white) @@ -91,7 +63,24 @@ struct MiniRecorderView: View { } } .frame(width: 18) + + // Empty space for future use + Spacer() + .frame(width: 18) + + // Power Mode Button - moved to last position + NotchToggleButton( + isEnabled: powerModeManager.isPowerModeEnabled, + icon: powerModeManager.currentActiveConfiguration.emoji, + color: .orange + ) { + showPowerModePopover.toggle() + } + .frame(width: 18) .padding(.trailing, -4) + .popover(isPresented: $showPowerModePopover, arrowEdge: .bottom) { + PowerModePopover() + } } .padding(.horizontal, 8) .padding(.vertical, 8) diff --git a/VoiceInk/Views/NotchRecorderView.swift b/VoiceInk/Views/NotchRecorderView.swift index 74c2c5c..675db46 100644 --- a/VoiceInk/Views/NotchRecorderView.swift +++ b/VoiceInk/Views/NotchRecorderView.swift @@ -5,7 +5,8 @@ struct NotchRecorderView: View { @ObservedObject var recorder: Recorder @EnvironmentObject var windowManager: NotchWindowManager @State private var isHovering = false - @State private var showPromptPopover = false + @State private var showPowerModePopover = false + @ObservedObject private var powerModeManager = PowerModeManager.shared private var menuBarHeight: CGFloat { if let screen = NSScreen.main { @@ -46,18 +47,9 @@ struct NotchRecorderView: View { } .frame(width: 22) - // AI Enhancement Toggle - if let enhancementService = whisperState.getEnhancementService() { - NotchToggleButton( - isEnabled: enhancementService.isEnhancementEnabled, - icon: "sparkles", - color: .blue - ) { - enhancementService.isEnhancementEnabled.toggle() - } + // Empty space for future use + Spacer() .frame(width: 22) - .disabled(!enhancementService.isConfigured) - } } .frame(width: 44) // Fixed width for controls .padding(.leading, 16) @@ -70,23 +62,7 @@ struct NotchRecorderView: View { // Right side group with fixed width HStack(spacing: 8) { - // Custom Prompt Toggle and Selector - if let enhancementService = whisperState.getEnhancementService() { - NotchToggleButton( - isEnabled: enhancementService.isEnhancementEnabled, - icon: enhancementService.activePrompt?.icon.rawValue ?? "text.badge.checkmark", - color: .green - ) { - showPromptPopover.toggle() - } - .frame(width: 22) - .disabled(!enhancementService.isEnhancementEnabled) - .popover(isPresented: $showPromptPopover, arrowEdge: .bottom) { - NotchPromptPopover(enhancementService: enhancementService) - } - } - - // Visualizer + // Visualizer - moved to first position Group { if whisperState.isProcessing { NotchStaticVisualizer(color: .white) @@ -99,6 +75,19 @@ struct NotchRecorderView: View { } } .frame(width: 22) + + // Power Mode Button - moved to second position + NotchToggleButton( + isEnabled: powerModeManager.isPowerModeEnabled, + icon: powerModeManager.currentActiveConfiguration.emoji, + color: .orange + ) { + showPowerModePopover.toggle() + } + .frame(width: 22) + .popover(isPresented: $showPowerModePopover, arrowEdge: .bottom) { + PowerModePopover() + } } .frame(width: 44) // Fixed width for controls .padding(.trailing, 16) @@ -195,15 +184,9 @@ struct NotchToggleButton: View { var body: some View { Button(action: action) { - ZStack { - Circle() - .fill(isEnabled ? color.opacity(0.2) : Color(red: 0.4, green: 0.4, blue: 0.45).opacity(0.2)) - .frame(width: 20, height: 20) - - Image(systemName: icon) - .font(.system(size: 10, weight: .medium)) - .foregroundColor(isEnabled ? color : .white.opacity(0.6)) - } + Text(icon) + .font(.system(size: 12)) + .foregroundColor(isEnabled ? .white : .white.opacity(0.6)) } .buttonStyle(PlainButtonStyle()) }