Feat: Overhaul Power Mode to use individual configurations
This commit is contained in:
parent
3cf8f33161
commit
5b43f3a2b3
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user