Feat: Overhaul Power Mode to use individual configurations

This commit is contained in:
Beingpax 2025-08-05 08:14:10 +05:45
parent 3cf8f33161
commit 5b43f3a2b3
12 changed files with 474 additions and 598 deletions

View File

@ -175,8 +175,8 @@ class MiniRecorderShortcutManager: ObservableObject {
let powerModeManager = PowerModeManager.shared
if powerModeManager.isPowerModeEnabled {
let availableConfigurations = powerModeManager.getAllAvailableConfigurations()
if !powerModeManager.enabledConfigurations.isEmpty {
let availableConfigurations = powerModeManager.enabledConfigurations
if index < availableConfigurations.count {
let selectedConfig = availableConfigurations[index]
powerModeManager.setActiveConfiguration(selectedConfig)

View File

@ -25,10 +25,6 @@ class ActiveWindowService: ObservableObject {
}
func applyConfigurationForCurrentApp() async {
guard PowerModeManager.shared.isPowerModeEnabled else {
return
}
guard let frontmostApp = NSWorkspace.shared.frontmostApplication,
let bundleIdentifier = frontmostApp.bundleIdentifier else { return }
@ -59,13 +55,16 @@ class ActiveWindowService: ObservableObject {
}
}
let config = PowerModeManager.shared.getConfigurationForApp(bundleIdentifier) ?? PowerModeManager.shared.defaultConfig
await MainActor.run {
PowerModeManager.shared.setActiveConfiguration(config)
if let config = PowerModeManager.shared.getConfigurationForApp(bundleIdentifier) {
await MainActor.run {
PowerModeManager.shared.setActiveConfiguration(config)
}
await applyConfiguration(config)
} else {
await MainActor.run {
PowerModeManager.shared.setActiveConfiguration(nil)
}
}
await applyConfiguration(config)
}
/// Applies a specific configuration

View File

@ -13,12 +13,12 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
var useScreenCapture: Bool
var selectedAIProvider: String?
var selectedAIModel: String?
// NEW: Automatically press the Return key after pasting
var isAutoSendEnabled: Bool = false
var isEnabled: Bool = true
// Custom coding keys to handle migration from selectedWhisperModel
enum CodingKeys: String, CodingKey {
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled
case selectedWhisperModel // Old key
case selectedTranscriptionModelName // New key
}
@ -26,7 +26,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
init(id: UUID = UUID(), name: String, emoji: String, appConfigs: [AppConfig]? = nil,
urlConfigs: [URLConfig]? = nil, isAIEnhancementEnabled: Bool, selectedPrompt: String? = nil,
selectedTranscriptionModelName: String? = nil, selectedLanguage: String? = nil, useScreenCapture: Bool = false,
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false) {
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true) {
self.id = id
self.name = name
self.emoji = emoji
@ -40,6 +40,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
self.selectedAIModel = selectedAIModel
self.selectedTranscriptionModelName = selectedTranscriptionModelName ?? UserDefaults.standard.string(forKey: "CurrentTranscriptionModel")
self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en"
self.isEnabled = isEnabled
}
init(from decoder: Decoder) throws {
@ -56,6 +57,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
selectedAIProvider = try container.decodeIfPresent(String.self, forKey: .selectedAIProvider)
selectedAIModel = try container.decodeIfPresent(String.self, forKey: .selectedAIModel)
isAutoSendEnabled = try container.decodeIfPresent(Bool.self, forKey: .isAutoSendEnabled) ?? false
isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
if let newModelName = try container.decodeIfPresent(String.self, forKey: .selectedTranscriptionModelName) {
selectedTranscriptionModelName = newModelName
@ -81,6 +83,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
try container.encodeIfPresent(selectedAIModel, forKey: .selectedAIModel)
try container.encode(isAutoSendEnabled, forKey: .isAutoSendEnabled)
try container.encodeIfPresent(selectedTranscriptionModelName, forKey: .selectedTranscriptionModelName)
try container.encode(isEnabled, forKey: .isEnabled)
}
@ -124,111 +127,64 @@ struct URLConfig: Codable, Identifiable, Equatable {
class PowerModeManager: ObservableObject {
static let shared = PowerModeManager()
@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
if UserDefaults.standard.object(forKey: powerModeEnabledKey) != nil {
self.isPowerModeEnabled = UserDefaults.standard.bool(forKey: powerModeEnabledKey)
} else {
self.isPowerModeEnabled = false
UserDefaults.standard.set(false, forKey: powerModeEnabledKey)
}
// Initialize default config with default values
if let data = UserDefaults.standard.data(forKey: defaultConfigKey),
let config = try? JSONDecoder().decode(PowerModeConfig.self, from: data) {
defaultConfig = config
} else {
// Get default values from UserDefaults if available
let defaultModelName = UserDefaults.standard.string(forKey: "CurrentTranscriptionModel")
let defaultLanguage = UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en"
defaultConfig = PowerModeConfig(
id: UUID(),
name: "Default Configuration",
emoji: "⚙️",
isAIEnhancementEnabled: false,
selectedPrompt: nil,
selectedTranscriptionModelName: defaultModelName,
selectedLanguage: defaultLanguage
)
saveDefaultConfig()
}
loadConfigurations()
// Set the active configuration, either from saved ID or default to the default config
// Set the active configuration from saved ID
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
}
activeConfiguration = configurations.first { $0.id == activeConfigId }
} else {
activeConfiguration = defaultConfig
activeConfiguration = nil
}
}
private func loadConfigurations() {
if let data = UserDefaults.standard.data(forKey: configKey),
let configs = try? JSONDecoder().decode([PowerModeConfig].self, from: data) {
configurations = configs
}
}
func saveConfigurations() {
if let data = try? JSONEncoder().encode(configurations) {
UserDefaults.standard.set(data, forKey: configKey)
}
}
private func saveDefaultConfig() {
if let data = try? JSONEncoder().encode(defaultConfig) {
UserDefaults.standard.set(data, forKey: defaultConfigKey)
}
}
func addConfiguration(_ config: PowerModeConfig) {
if !configurations.contains(where: { $0.id == config.id }) {
configurations.append(config)
saveConfigurations()
}
}
func removeConfiguration(with id: UUID) {
configurations.removeAll { $0.id == id }
saveConfigurations()
}
func getConfiguration(with id: UUID) -> PowerModeConfig? {
return configurations.first { $0.id == id }
}
func updateConfiguration(_ config: PowerModeConfig) {
if config.id == defaultConfig.id {
defaultConfig = config
saveDefaultConfig()
} else if let index = configurations.firstIndex(where: { $0.id == config.id }) {
if let index = configurations.firstIndex(where: { $0.id == config.id }) {
configurations[index] = config
saveConfigurations()
}
}
// Get configuration for a specific URL
func getConfigurationForURL(_ url: String) -> PowerModeConfig? {
let cleanedURL = cleanURL(url)
for config in configurations {
for config in configurations.filter({ $0.isEnabled }) {
if let urlConfigs = config.urlConfigs {
for urlConfig in urlConfigs {
let configURL = cleanURL(urlConfig.url)
@ -244,7 +200,7 @@ class PowerModeManager: ObservableObject {
// Get configuration for an application bundle ID
func getConfigurationForApp(_ bundleId: String) -> PowerModeConfig? {
for config in configurations {
for config in configurations.filter({ $0.isEnabled }) {
if let appConfigs = config.appConfigs {
if appConfigs.contains(where: { $0.bundleIdentifier == bundleId }) {
return config
@ -254,6 +210,27 @@ class PowerModeManager: ObservableObject {
return nil
}
// Enable a configuration
func enableConfiguration(with id: UUID) {
if let index = configurations.firstIndex(where: { $0.id == id }) {
configurations[index].isEnabled = true
saveConfigurations()
}
}
// Disable a configuration
func disableConfiguration(with id: UUID) {
if let index = configurations.firstIndex(where: { $0.id == id }) {
configurations[index].isEnabled = false
saveConfigurations()
}
}
// Get all enabled configurations
var enabledConfigurations: [PowerModeConfig] {
return configurations.filter { $0.isEnabled }
}
// Add app configuration
func addAppConfig(_ appConfig: AppConfig, to config: PowerModeConfig) {
if var updatedConfig = configurations.first(where: { $0.id == config.id }) {
@ -263,7 +240,7 @@ class PowerModeManager: ObservableObject {
updateConfiguration(updatedConfig)
}
}
// Remove app configuration
func removeAppConfig(_ appConfig: AppConfig, from config: PowerModeConfig) {
if var updatedConfig = configurations.first(where: { $0.id == config.id }) {
@ -271,7 +248,7 @@ class PowerModeManager: ObservableObject {
updateConfiguration(updatedConfig)
}
}
// Add URL configuration
func addURLConfig(_ urlConfig: URLConfig, to config: PowerModeConfig) {
if var updatedConfig = configurations.first(where: { $0.id == config.id }) {
@ -281,7 +258,7 @@ class PowerModeManager: ObservableObject {
updateConfiguration(updatedConfig)
}
}
// Remove URL configuration
func removeURLConfig(_ urlConfig: URLConfig, from config: PowerModeConfig) {
if var updatedConfig = configurations.first(where: { $0.id == config.id }) {
@ -289,7 +266,7 @@ class PowerModeManager: ObservableObject {
updateConfiguration(updatedConfig)
}
}
// Clean URL for comparison
func cleanURL(_ url: String) -> String {
return url.lowercased()
@ -298,33 +275,25 @@ class PowerModeManager: ObservableObject {
.replacingOccurrences(of: "www.", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
// Save power mode enabled state
func savePowerModeEnabled() {
UserDefaults.standard.set(isPowerModeEnabled, forKey: powerModeEnabledKey)
}
// Set active configuration
func setActiveConfiguration(_ config: PowerModeConfig) {
func setActiveConfiguration(_ config: PowerModeConfig?) {
activeConfiguration = config
UserDefaults.standard.set(config.id.uuidString, forKey: activeConfigIdKey)
UserDefaults.standard.set(config?.id.uuidString, forKey: activeConfigIdKey)
self.objectWillChange.send()
}
// Get current active configuration
var currentActiveConfiguration: PowerModeConfig {
return activeConfiguration ?? defaultConfig
var currentActiveConfiguration: PowerModeConfig? {
return activeConfiguration
}
// Get all available configurations in order (default first, then custom configurations)
// Get all available configurations in order
func getAllAvailableConfigurations() -> [PowerModeConfig] {
return [defaultConfig] + configurations
return configurations
}
func isEmojiInUse(_ emoji: String) -> Bool {
if defaultConfig.emoji == emoji {
return true
}
return configurations.contains { $0.emoji == emoji }
}
}

View File

@ -96,19 +96,6 @@ struct ConfigurationView: View {
_isAutoSendEnabled = State(initialValue: latestConfig.isAutoSendEnabled)
_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)
_isAutoSendEnabled = State(initialValue: latestConfig.isAutoSendEnabled)
_selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider)
_selectedAIModel = State(initialValue: latestConfig.selectedAIModel)
}
}
@ -159,8 +146,6 @@ struct ConfigurationView: View {
}
}
.buttonStyle(.plain)
.disabled(mode.isEditingDefault)
.opacity(mode.isEditingDefault ? 0.5 : 1)
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
EmojiPickerView(
selectedEmoji: $selectedEmoji,
@ -173,12 +158,9 @@ struct ConfigurationView: View {
.textFieldStyle(.plain)
.foregroundColor(.primary)
.tint(.accentColor)
.disabled(mode.isEditingDefault)
.focused($isNameFieldFocused)
.onAppear {
if !mode.isEditingDefault {
isNameFieldFocused = true
}
isNameFieldFocused = true
}
}
.padding(.horizontal, 20)
@ -196,154 +178,152 @@ struct ConfigurationView: View {
// }
// 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(CardBackground(isSelected: false))
} 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")
VStack(spacing: 16) {
// Section Header
SectionHeader(title: "When to Trigger")
// Applications Subsection
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Applications")
.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(CardBackground(isSelected: false))
} 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)
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(CardBackground(isSelected: false))
} 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: {
websiteConfigs.removeAll(where: { $0.id == urlConfig.id })
selectedAppConfigs.removeAll(where: { $0.id == appConfig.id })
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 9))
.foregroundColor(.secondary)
.font(.system(size: 14))
.foregroundColor(.white)
.background(Circle().fill(Color.black.opacity(0.6)))
}
.buttonStyle(.plain)
.offset(x: 6, y: -6)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.frame(height: 28)
.background(CardBackground(isSelected: false, cornerRadius: 10))
}
.frame(width: 50, height: 50)
.background(CardBackground(isSelected: false, cornerRadius: 10))
}
.padding(8)
}
}
}
.padding()
.background(CardBackground(isSelected: false))
.padding(.horizontal)
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(CardBackground(isSelected: false))
} 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(CardBackground(isSelected: false))
.padding(.horizontal)
// SECTION 2: TRANSCRIPTION
VStack(spacing: 16) {
@ -713,20 +693,6 @@ struct ConfigurationView: View {
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.isAutoSendEnabled = isAutoSendEnabled
updatedConfig.selectedAIProvider = selectedAIProvider
updatedConfig.selectedAIModel = selectedAIModel
return updatedConfig
}
}
@ -790,7 +756,7 @@ struct ConfigurationView: View {
switch mode {
case .add:
powerModeManager.addConfiguration(config)
case .edit, .editDefault:
case .edit:
powerModeManager.updateConfiguration(config)
}

View File

@ -18,18 +18,28 @@ struct PowerModePopover: View {
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
// Apply configuration immediately
applySelectedConfiguration()
// "Disable" option if a power mode is active
if powerModeManager.activeConfiguration != nil {
Button(action: {
powerModeManager.setActiveConfiguration(nil)
selectedConfig = nil
// Here we might want to revert to a default state,
// but for now, we'll just deactivate the power mode.
}) {
HStack {
Text("Disable Power Mode")
.foregroundColor(.red.opacity(0.9))
.font(.system(size: 13))
Spacer()
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red.opacity(0.9))
}
.padding(.vertical, 4)
.padding(.horizontal, 8)
}
)
.buttonStyle(.plain)
}
// Custom Configurations
ForEach(powerModeManager.configurations) { config in
PowerModeRow(

View File

@ -64,43 +64,41 @@ struct PowerModeValidator {
errors.append(.duplicateName(config.name))
}
// For non-default modes, check that there's at least one trigger
if !mode.isEditingDefault {
if (config.appConfigs == nil || config.appConfigs?.isEmpty == true) &&
(config.urlConfigs == nil || config.urlConfigs?.isEmpty == true) {
errors.append(.noTriggers)
}
// Check for duplicate app configurations
if let appConfigs = config.appConfigs {
for appConfig in appConfigs {
for existingConfig in powerModeManager.configurations {
// Skip checking against itself when editing
if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id {
continue
}
if let existingAppConfigs = existingConfig.appConfigs,
existingAppConfigs.contains(where: { $0.bundleIdentifier == appConfig.bundleIdentifier }) {
errors.append(.duplicateAppTrigger(appConfig.appName, existingConfig.name))
}
// For all modes, check that there's at least one trigger
if (config.appConfigs == nil || config.appConfigs?.isEmpty == true) &&
(config.urlConfigs == nil || config.urlConfigs?.isEmpty == true) {
errors.append(.noTriggers)
}
// Check for duplicate app configurations
if let appConfigs = config.appConfigs {
for appConfig in appConfigs {
for existingConfig in powerModeManager.configurations {
// Skip checking against itself when editing
if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id {
continue
}
if let existingAppConfigs = existingConfig.appConfigs,
existingAppConfigs.contains(where: { $0.bundleIdentifier == appConfig.bundleIdentifier }) {
errors.append(.duplicateAppTrigger(appConfig.appName, existingConfig.name))
}
}
}
// Check for duplicate website configurations
if let urlConfigs = config.urlConfigs {
for urlConfig in urlConfigs {
for existingConfig in powerModeManager.configurations {
// Skip checking against itself when editing
if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id {
continue
}
if let existingUrlConfigs = existingConfig.urlConfigs,
existingUrlConfigs.contains(where: { $0.url == urlConfig.url }) {
errors.append(.duplicateWebsiteTrigger(urlConfig.url, existingConfig.name))
}
}
// Check for duplicate website configurations
if let urlConfigs = config.urlConfigs {
for urlConfig in urlConfigs {
for existingConfig in powerModeManager.configurations {
// Skip checking against itself when editing
if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id {
continue
}
if let existingUrlConfigs = existingConfig.urlConfigs,
existingUrlConfigs.contains(where: { $0.url == urlConfig.url }) {
errors.append(.duplicateWebsiteTrigger(urlConfig.url, existingConfig.name))
}
}
}

View File

@ -17,22 +17,15 @@ extension View {
enum ConfigurationMode: Hashable {
case add
case edit(PowerModeConfig)
case editDefault(PowerModeConfig)
var isAdding: Bool {
if case .add = self { return true }
return false
}
var isEditingDefault: Bool {
if case .editDefault = self { return true }
return false
}
var title: String {
switch self {
case .add: return "Add Power Mode"
case .editDefault: return "Edit Default Power Mode"
case .edit: return "Edit Power Mode"
}
}
@ -45,9 +38,6 @@ enum ConfigurationMode: Hashable {
case .edit(let config):
hasher.combine(1) // Use a unique value for edit
hasher.combine(config.id)
case .editDefault(let config):
hasher.combine(2) // Use a unique value for editDefault
hasher.combine(config.id)
}
}
@ -58,8 +48,6 @@ enum ConfigurationMode: Hashable {
return true
case (.edit(let lhsConfig), .edit(let rhsConfig)):
return lhsConfig.id == rhsConfig.id
case (.editDefault(let lhsConfig), .editDefault(let rhsConfig)):
return lhsConfig.id == rhsConfig.id
default:
return false
}
@ -100,14 +88,7 @@ struct PowerModeView: View {
)
Spacer()
Toggle("", isOn: $powerModeManager.isPowerModeEnabled)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.labelsHidden()
.scaleEffect(1.2)
.onChange(of: powerModeManager.isPowerModeEnabled) { oldValue, newValue in
powerModeManager.savePowerModeEnabled()
}
}
}
Text("Automatically apply custom configurations based on the app/website you are using")
@ -117,125 +98,68 @@ struct PowerModeView: View {
.padding(.horizontal)
.padding(.top, 8)
if powerModeManager.isPowerModeEnabled {
// Configurations Container
VStack(spacing: 0) {
// Default Configuration Section
VStack(alignment: .leading, spacing: 16) {
Text("Default Power Mode")
.font(.headline)
.foregroundColor(.primary)
.padding(.horizontal)
.padding(.top, 16)
ConfigurationRow(
config: powerModeManager.defaultConfig,
isEditing: false,
isDefault: true,
action: {
configurationMode = .editDefault(powerModeManager.defaultConfig)
navigationPath.append(configurationMode!)
}
)
// Configurations Container
VStack(spacing: 0) {
// Custom Configurations Section
VStack(alignment: .leading, spacing: 16) {
Text("Custom Power Modes")
.font(.headline)
.foregroundColor(.primary)
.padding(.horizontal)
}
// Divider between sections
Divider()
.padding(.vertical, 16)
// Custom Configurations Section
VStack(alignment: .leading, spacing: 16) {
Text("Custom Power Modes")
.font(.headline)
.foregroundColor(.primary)
.padding(.horizontal)
if powerModeManager.configurations.isEmpty {
VStack(spacing: 20) {
Image(systemName: "square.grid.2x2")
.font(.system(size: 36))
.foregroundColor(.secondary)
Text("No custom power modes")
.font(.title3)
.fontWeight(.medium)
Text("Create a new mode for specific apps/websites")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 30)
} else {
PowerModeConfigurationsGrid(
powerModeManager: powerModeManager,
onEditConfig: { config in
configurationMode = .edit(config)
navigationPath.append(configurationMode!)
}
)
if powerModeManager.configurations.isEmpty {
VStack(spacing: 20) {
Image(systemName: "square.grid.2x2")
.font(.system(size: 36))
.foregroundColor(.secondary)
Text("No power modes configured")
.font(.title3)
.fontWeight(.medium)
Text("Create a new power mode to get started.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
Spacer(minLength: 24)
// Add Configuration button at the bottom (centered)
HStack {
VoiceInkButton(
title: "Add New Power Mode",
action: {
configurationMode = .add
.frame(maxWidth: .infinity)
.padding(.vertical, 30)
} else {
PowerModeConfigurationsGrid(
powerModeManager: powerModeManager,
onEditConfig: { config in
configurationMode = .edit(config)
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)
} else {
// Disabled state
VStack(spacing: 24) {
Image(systemName: "bolt.slash.circle")
.font(.system(size: 56))
.foregroundColor(.secondary)
Text("Power Mode is disabled")
.font(.title2)
.fontWeight(.semibold)
Text("Enable Power Mode to create context-specific configurations that automatically apply based on your current app or website.")
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 400)
Spacer(minLength: 24)
// Add Configuration button at the bottom (centered)
HStack {
VoiceInkButton(
title: "Enable Power Mode",
title: "Add New Power Mode",
action: {
powerModeManager.isPowerModeEnabled = true
powerModeManager.savePowerModeEnabled()
configurationMode = .add
navigationPath.append(configurationMode!)
}
)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.padding(40)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(16)
.shadow(color: Color(NSColor.shadowColor).opacity(0.05), radius: 5, y: 2)
.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)
}
@ -246,7 +170,7 @@ struct PowerModeView: View {
}
}
}
}
// New component for section headers

View File

@ -63,6 +63,7 @@ struct PowerModeConfigurationsGrid: View {
config: config,
isEditing: false,
isDefault: false,
powerModeManager: powerModeManager,
action: {
onEditConfig(config)
}
@ -86,9 +87,10 @@ struct PowerModeConfigurationsGrid: View {
}
struct ConfigurationRow: View {
let config: PowerModeConfig
@State var config: PowerModeConfig
let isEditing: Bool
let isDefault: Bool
let powerModeManager: PowerModeManager
let action: () -> Void
@EnvironmentObject var enhancementService: AIEnhancementService
@EnvironmentObject var whisperState: WhisperState
@ -148,155 +150,99 @@ struct ConfigurationRow: View {
}
var body: some View {
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))
}
}
// Middle: Name and badge
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 6) {
Text(config.name)
.font(.system(size: 15, weight: .semibold))
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 {
Text("Default")
.font(.system(size: 10, weight: .medium))
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(Capsule().fill(Color.accentColor.opacity(0.15)))
Image(systemName: "gearshape.fill")
.font(.system(size: 18))
.foregroundColor(.accentColor)
} else {
Text(config.emoji)
.font(.system(size: 20))
}
}
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)
// 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)
}
}
// Website Count
if websiteCount > 0 {
HStack(spacing: 3) {
Text(websiteText)
.font(.caption)
.foregroundColor(.secondary)
Image(systemName: "globe")
.font(.system(size: 9))
.foregroundColor(.secondary)
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)
}
}
}
}
}
}
.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)
.padding(.vertical, 12)
.padding(.horizontal, 14)
// 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(.controlBackgroundColor)))
.overlay(
Capsule()
.stroke(Color(.separatorColor), lineWidth: 0.5)
)
}
// Only add divider and settings row if we have settings
if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled {
Divider()
.padding(.horizontal, 16)
// 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(.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 {
// Settings badges in specified order
HStack(spacing: 8) {
// 1. Voice Model badge
if let model = selectedModel, model != "Default" {
HStack(spacing: 4) {
Image(systemName: "camera.viewfinder")
Image(systemName: "waveform")
.font(.system(size: 10))
Text("Context Awareness")
Text(model)
.font(.caption)
}
.padding(.horizontal, 8)
@ -309,28 +255,98 @@ struct ConfigurationRow: View {
)
}
HStack(spacing: 4) {
Image(systemName: "sparkles")
.font(.system(size: 10))
Text(selectedPrompt?.title ?? "AI")
.font(.caption)
// 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(.controlBackgroundColor)))
.overlay(
Capsule()
.stroke(Color(.separatorColor), lineWidth: 0.5)
)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Capsule()
.fill(Color.accentColor.opacity(0.1)))
.foregroundColor(.accentColor)
// 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()
}
Spacer()
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
.background(CardBackground(isSelected: isEditing))
}
.background(CardBackground(isSelected: isEditing))
.buttonStyle(.plain)
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)
}
}
}
.buttonStyle(.plain)
.opacity(config.isEnabled ? 1.0 : 0.5)
}
private var isSelected: Bool {

View File

@ -26,7 +26,6 @@ struct VoiceInkExportedSettings: Codable {
let version: String
let customPrompts: [CustomPrompt]
let powerModeConfigs: [PowerModeConfig]
let defaultPowerModeConfig: PowerModeConfig
let dictionaryItems: [DictionaryItem]?
let wordReplacements: [String: String]?
let generalSettings: GeneralSettings?
@ -67,7 +66,6 @@ class ImportExportService {
let exportablePrompts = enhancementService.customPrompts.filter { !$0.isPredefined }
let powerConfigs = powerModeManager.configurations
let defaultPowerConfig = powerModeManager.defaultConfig
// Export custom models
let customModels = CustomModelManager.shared.customModels
@ -102,7 +100,6 @@ class ImportExportService {
version: currentSettingsVersion,
customPrompts: exportablePrompts,
powerModeConfigs: powerConfigs,
defaultPowerModeConfig: defaultPowerConfig,
dictionaryItems: exportedDictionaryItems,
wordReplacements: exportedWordReplacements,
generalSettings: generalSettingsToExport,
@ -172,9 +169,7 @@ class ImportExportService {
let powerModeManager = PowerModeManager.shared
powerModeManager.configurations = importedSettings.powerModeConfigs
powerModeManager.defaultConfig = importedSettings.defaultPowerModeConfig
powerModeManager.saveConfigurations()
powerModeManager.updateConfiguration(powerModeManager.defaultConfig)
// Import Custom Models
if let modelsToImport = importedSettings.customCloudModels {

View File

@ -36,10 +36,10 @@ struct MiniRecorderView: View {
private var rightButton: some View {
Group {
if powerModeManager.isPowerModeEnabled {
if !powerModeManager.enabledConfigurations.isEmpty {
RecorderToggleButton(
isEnabled: powerModeManager.isPowerModeEnabled,
icon: powerModeManager.currentActiveConfiguration.emoji,
isEnabled: !powerModeManager.enabledConfigurations.isEmpty,
icon: powerModeManager.currentActiveConfiguration?.emoji ?? "⚙️",
color: .orange,
disabled: false
) {

View File

@ -54,10 +54,10 @@ struct NotchRecorderView: View {
private var rightToggleButton: some View {
Group {
if powerModeManager.isPowerModeEnabled {
if !powerModeManager.enabledConfigurations.isEmpty {
RecorderToggleButton(
isEnabled: powerModeManager.isPowerModeEnabled,
icon: powerModeManager.currentActiveConfiguration.emoji,
isEnabled: !powerModeManager.enabledConfigurations.isEmpty,
icon: powerModeManager.currentActiveConfiguration?.emoji ?? "⚙️",
color: .orange,
disabled: false
) {

View File

@ -351,9 +351,8 @@ class WhisperState: NSObject, ObservableObject {
CursorPaster.pasteAtCursor(text, shouldPreserveClipboard: true)
// Automatically press Enter if the active Power Mode configuration allows it.
let powerMode = PowerModeManager.shared
if powerMode.isPowerModeEnabled && powerMode.currentActiveConfiguration.isAutoSendEnabled {
if let activeConfig = powerMode.currentActiveConfiguration, activeConfig.isAutoSendEnabled {
// Slight delay to ensure the paste operation completes
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
CursorPaster.pressEnter()