Merge pull request #465 from Beingpax/better-powermode-ui
Better powermode UI
This commit is contained in:
commit
f712652278
@ -99,7 +99,6 @@ struct EmojiPickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(.regularMaterial)
|
|
||||||
.frame(minWidth: 260, idealWidth: 300, maxWidth: 320, minHeight: 150, idealHeight: 280, maxHeight: 350)
|
.frame(minWidth: 260, idealWidth: 300, maxWidth: 320, minHeight: 150, idealHeight: 280, maxHeight: 350)
|
||||||
.alert("Emoji in Use", isPresented: $showingEmojiInUseAlert, presenting: emojiForAlert) { emojiStr in
|
.alert("Emoji in Use", isPresented: $showingEmojiInUseAlert, presenting: emojiForAlert) { emojiStr in
|
||||||
Button("OK", role: .cancel) { }
|
Button("OK", role: .cancel) { }
|
||||||
@ -161,10 +160,6 @@ private struct EmojiButton: View {
|
|||||||
Text(emoji)
|
Text(emoji)
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(isSelected ? Color.accentColor.opacity(0.25) : Color.clear)
|
|
||||||
)
|
|
||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(isSelected ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
.strokeBorder(isSelected ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||||
@ -197,10 +192,6 @@ private struct AddEmojiButton: View {
|
|||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(Color.secondary.opacity(0.1))
|
|
||||||
)
|
|
||||||
.overlay(
|
.overlay(
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 1)
|
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 1)
|
||||||
|
|||||||
@ -39,9 +39,7 @@ struct ConfigurationView: View {
|
|||||||
@State private var isAutoSendEnabled = false
|
@State private var isAutoSendEnabled = false
|
||||||
@State private var isDefault = false
|
@State private var isDefault = false
|
||||||
|
|
||||||
// State for prompt editing (similar to EnhancementSettingsView)
|
@State private var isShowingDeleteConfirmation = false
|
||||||
@State private var isEditingPrompt = false
|
|
||||||
@State private var selectedPromptForEdit: CustomPrompt?
|
|
||||||
|
|
||||||
// PowerMode hotkey configuration
|
// PowerMode hotkey configuration
|
||||||
@State private var powerModeConfigId: UUID = UUID()
|
@State private var powerModeConfigId: UUID = UUID()
|
||||||
@ -126,550 +124,371 @@ struct ConfigurationView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
Form {
|
||||||
// Header with Title and Cancel button
|
Section("General") {
|
||||||
HStack {
|
HStack(spacing: 12) {
|
||||||
Text(mode.title)
|
Button {
|
||||||
.font(.largeTitle)
|
isShowingEmojiPicker.toggle()
|
||||||
.fontWeight(.bold)
|
} label: {
|
||||||
|
Text(selectedEmoji)
|
||||||
Spacer()
|
.font(.system(size: 22))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
if case .edit(let config) = mode {
|
.background(
|
||||||
Button("Delete") {
|
RoundedRectangle(cornerRadius: 8)
|
||||||
let alert = NSAlert()
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
alert.messageText = "Delete Power Mode?"
|
)
|
||||||
alert.informativeText = "Are you sure you want to delete the '\(config.name)' power mode? This action cannot be undone."
|
}
|
||||||
alert.alertStyle = .warning
|
.buttonStyle(.plain)
|
||||||
alert.addButton(withTitle: "Delete")
|
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
|
||||||
alert.addButton(withTitle: "Cancel")
|
EmojiPickerView(
|
||||||
|
selectedEmoji: $selectedEmoji,
|
||||||
// Style the Delete button as destructive
|
isPresented: $isShowingEmojiPicker
|
||||||
alert.buttons[0].hasDestructiveAction = true
|
)
|
||||||
|
}
|
||||||
let response = alert.runModal()
|
|
||||||
if response == .alertFirstButtonReturn {
|
TextField("Name", text: $configName)
|
||||||
powerModeManager.removeConfiguration(with: config.id)
|
.textFieldStyle(.roundedBorder)
|
||||||
presentationMode.wrappedValue.dismiss()
|
.focused($isNameFieldFocused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Trigger Scenarios") {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
Text("Applications")
|
||||||
|
Spacer()
|
||||||
|
AddIconButton(helpText: "Add application") {
|
||||||
|
loadInstalledApps()
|
||||||
|
isShowingAppPicker = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundColor(.red)
|
|
||||||
.padding(.trailing, 8)
|
if selectedAppConfigs.isEmpty {
|
||||||
|
Text("No applications added")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.subheadline)
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 44, maximum: 50), spacing: 10)], spacing: 10) {
|
||||||
|
ForEach(selectedAppConfigs) { appConfig in
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) {
|
||||||
|
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "app.fill")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 26, height: 26)
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
selectedAppConfigs.removeAll(where: { $0.id == appConfig.id })
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.offset(x: 6, y: -6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
Button("Cancel") {
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Websites")
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.onSubmit { addWebsite() }
|
||||||
|
|
||||||
|
AddIconButton(helpText: "Add website", isDisabled: newWebsiteURL.isEmpty) {
|
||||||
|
addWebsite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if websiteConfigs.isEmpty {
|
||||||
|
Text("No websites added")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.subheadline)
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 140, maximum: 220), spacing: 10)], spacing: 10) {
|
||||||
|
ForEach(websiteConfigs) { urlConfig in
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "globe")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(urlConfig.url)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Button {
|
||||||
|
websiteConfigs.removeAll(where: { $0.id == urlConfig.id })
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Transcription") {
|
||||||
|
if whisperState.usableModels.isEmpty {
|
||||||
|
Text("No transcription models available. Please connect to a cloud service or download a local model in the AI Models tab.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
let modelBinding = Binding<String?>(
|
||||||
|
get: { selectedTranscriptionModelName ?? whisperState.usableModels.first?.name },
|
||||||
|
set: { selectedTranscriptionModelName = $0 }
|
||||||
|
)
|
||||||
|
|
||||||
|
Picker("Model", selection: modelBinding) {
|
||||||
|
ForEach(whisperState.usableModels, id: \.name) { model in
|
||||||
|
Text(model.displayName).tag(model.name as String?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if languageSelectionDisabled() {
|
||||||
|
LabeledContent("Language") {
|
||||||
|
Text("Autodetected")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else if let selectedModel = effectiveModelName,
|
||||||
|
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||||
|
modelInfo.isMultilingualModel {
|
||||||
|
let languageBinding = Binding<String?>(
|
||||||
|
get: { selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "auto" },
|
||||||
|
set: { selectedLanguage = $0 }
|
||||||
|
)
|
||||||
|
|
||||||
|
Picker("Language", selection: languageBinding) {
|
||||||
|
ForEach(modelInfo.supportedLanguages.sorted(by: {
|
||||||
|
if $0.key == "auto" { return true }
|
||||||
|
if $1.key == "auto" { return false }
|
||||||
|
return $0.value < $1.value
|
||||||
|
}), id: \.key) { key, value in
|
||||||
|
Text(value).tag(key as String?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let selectedModel = effectiveModelName,
|
||||||
|
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||||
|
!modelInfo.isMultilingualModel {
|
||||||
|
EmptyView()
|
||||||
|
.onAppear {
|
||||||
|
if selectedLanguage == nil {
|
||||||
|
selectedLanguage = "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("AI Enhancement") {
|
||||||
|
Toggle("Enable AI Enhancement", isOn: $isAIEnhancementEnabled)
|
||||||
|
.onChange(of: isAIEnhancementEnabled) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
if selectedAIProvider == nil {
|
||||||
|
selectedAIProvider = aiService.selectedProvider.rawValue
|
||||||
|
}
|
||||||
|
if selectedAIModel == nil {
|
||||||
|
selectedAIModel = aiService.currentModel
|
||||||
|
}
|
||||||
|
if selectedPromptId == nil {
|
||||||
|
selectedPromptId = enhancementService.allPrompts.first?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let providerBinding = Binding<AIProvider>(
|
||||||
|
get: {
|
||||||
|
if let providerName = selectedAIProvider,
|
||||||
|
let provider = AIProvider(rawValue: providerName) {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
return aiService.selectedProvider
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
selectedAIProvider = newValue.rawValue
|
||||||
|
aiService.selectedProvider = newValue
|
||||||
|
selectedAIModel = nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if isAIEnhancementEnabled {
|
||||||
|
if aiService.connectedProviders.isEmpty {
|
||||||
|
LabeledContent("AI Provider") {
|
||||||
|
Text("No providers connected")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.italic()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Picker("AI Provider", selection: providerBinding) {
|
||||||
|
ForEach(aiService.connectedProviders.filter { $0 != .elevenLabs && $0 != .deepgram }, id: \.self) { provider in
|
||||||
|
Text(provider.rawValue).tag(provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: selectedAIProvider) { _, newValue in
|
||||||
|
if let provider = newValue.flatMap({ AIProvider(rawValue: $0) }) {
|
||||||
|
selectedAIModel = provider.defaultModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let providerName = selectedAIProvider ?? aiService.selectedProvider.rawValue
|
||||||
|
if let provider = AIProvider(rawValue: providerName),
|
||||||
|
provider != .custom {
|
||||||
|
if aiService.availableModels.isEmpty {
|
||||||
|
LabeledContent("AI Model") {
|
||||||
|
Text(provider == .openRouter ? "No models loaded" : "No models available")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.italic()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let modelBinding = Binding<String>(
|
||||||
|
get: {
|
||||||
|
if let model = selectedAIModel, !model.isEmpty { return model }
|
||||||
|
return aiService.currentModel
|
||||||
|
},
|
||||||
|
set: { newModelValue in
|
||||||
|
selectedAIModel = newModelValue
|
||||||
|
aiService.selectModel(newModelValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let models = provider == .openRouter ? aiService.availableModels : (provider == .ollama ? aiService.availableModels : provider.availableModels)
|
||||||
|
|
||||||
|
Picker("AI Model", selection: modelBinding) {
|
||||||
|
ForEach(models, id: \.self) { model in
|
||||||
|
Text(model).tag(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider == .openRouter {
|
||||||
|
Button("Refresh Models") {
|
||||||
|
Task { await aiService.fetchOpenRouterModels() }
|
||||||
|
}
|
||||||
|
.help("Refresh models")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if enhancementService.allPrompts.isEmpty {
|
||||||
|
LabeledContent("Enhancement Prompt") {
|
||||||
|
Text("No prompts available")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Picker("Enhancement Prompt", selection: $selectedPromptId) {
|
||||||
|
ForEach(enhancementService.allPrompts) { prompt in
|
||||||
|
Text(prompt.title).tag(prompt.id as UUID?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle("Context Awareness", isOn: $useScreenCapture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Advanced") {
|
||||||
|
Toggle(isOn: $isDefault) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Set as default")
|
||||||
|
InfoTip(
|
||||||
|
title: "Default Power Mode",
|
||||||
|
message: "Default power mode is used when no specific app or website matches are found"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $isAutoSendEnabled) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Auto Send")
|
||||||
|
InfoTip(
|
||||||
|
title: "Auto Send",
|
||||||
|
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Keyboard Shortcut")
|
||||||
|
InfoTip(
|
||||||
|
title: "Power Mode Hotkey",
|
||||||
|
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
|
||||||
|
.controlSize(.regular)
|
||||||
|
.frame(minHeight: 28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
|
.navigationTitle(mode.title)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button("Save") {
|
||||||
|
saveConfiguration()
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
.disabled(!canSave)
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.regular)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .edit = mode {
|
||||||
|
ToolbarItem {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
isShowingDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.regular)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
"Delete Power Mode?",
|
||||||
|
isPresented: $isShowingDeleteConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
if case .edit(let config) = mode {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
powerModeManager.removeConfiguration(with: config.id)
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.escape, modifiers: [])
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
Button("Cancel", role: .cancel) { }
|
||||||
.padding(.top)
|
} message: {
|
||||||
.padding(.bottom, 10)
|
if case .edit(let config) = mode {
|
||||||
|
Text("Are you sure you want to delete the '\(config.name)' power mode? This action cannot be undone.")
|
||||||
Divider()
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// Main Input Section
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
Button(action: {
|
|
||||||
isShowingEmojiPicker.toggle()
|
|
||||||
}) {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor.opacity(0.15))
|
|
||||||
.frame(width: 48, height: 48)
|
|
||||||
|
|
||||||
Text(selectedEmoji)
|
|
||||||
.font(.system(size: 24))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
|
|
||||||
EmojiPickerView(
|
|
||||||
selectedEmoji: $selectedEmoji,
|
|
||||||
isPresented: $isShowingEmojiPicker
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("Name your power mode", text: $configName)
|
|
||||||
.font(.system(size: 18, weight: .bold))
|
|
||||||
.textFieldStyle(.plain)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.tint(.accentColor)
|
|
||||||
.focused($isNameFieldFocused)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default Power Mode Toggle
|
|
||||||
HStack {
|
|
||||||
Toggle("Set as default power mode", isOn: $isDefault)
|
|
||||||
.font(.system(size: 14))
|
|
||||||
|
|
||||||
InfoTip(
|
|
||||||
title: "Default Power Mode",
|
|
||||||
message: "Default power mode is used when no specific app or website matches are found"
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.onAppear {
|
|
||||||
// Add a small delay to ensure the view is fully loaded
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
isNameFieldFocused = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
SectionHeader(title: "When to Trigger")
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
SectionHeader(title: "Transcription")
|
|
||||||
|
|
||||||
if whisperState.usableModels.isEmpty {
|
|
||||||
Text("No transcription models available. Please connect to a cloud service or download a local model in the AI Models tab.")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
} else {
|
|
||||||
let modelBinding = Binding<String?>(
|
|
||||||
get: {
|
|
||||||
selectedTranscriptionModelName ?? whisperState.usableModels.first?.name
|
|
||||||
},
|
|
||||||
set: { selectedTranscriptionModelName = $0 }
|
|
||||||
)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Model")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Picker("", selection: modelBinding) {
|
|
||||||
ForEach(whisperState.usableModels, id: \.name) { model in
|
|
||||||
Text(model.displayName).tag(model.name as String?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if languageSelectionDisabled() {
|
|
||||||
HStack {
|
|
||||||
Text("Language")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Autodetected")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
} else if let selectedModel = effectiveModelName,
|
|
||||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
|
||||||
modelInfo.isMultilingualModel {
|
|
||||||
|
|
||||||
let languageBinding = Binding<String?>(
|
|
||||||
get: {
|
|
||||||
selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "auto"
|
|
||||||
},
|
|
||||||
set: { selectedLanguage = $0 }
|
|
||||||
)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Language")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Picker("", selection: languageBinding) {
|
|
||||||
ForEach(modelInfo.supportedLanguages.sorted(by: {
|
|
||||||
if $0.key == "auto" { return true }
|
|
||||||
if $1.key == "auto" { return false }
|
|
||||||
return $0.value < $1.value
|
|
||||||
}), id: \.key) { key, value in
|
|
||||||
Text(value).tag(key as String?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
} else if let selectedModel = effectiveModelName,
|
|
||||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
|
||||||
!modelInfo.isMultilingualModel {
|
|
||||||
|
|
||||||
EmptyView()
|
|
||||||
.onAppear {
|
|
||||||
if selectedLanguage == nil {
|
|
||||||
selectedLanguage = "en"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
SectionHeader(title: "AI Enhancement")
|
|
||||||
|
|
||||||
Toggle("Enable AI Enhancement", isOn: $isAIEnhancementEnabled)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.onChange(of: isAIEnhancementEnabled) { oldValue, newValue in
|
|
||||||
if newValue {
|
|
||||||
if selectedAIProvider == nil {
|
|
||||||
selectedAIProvider = aiService.selectedProvider.rawValue
|
|
||||||
}
|
|
||||||
if selectedAIModel == nil {
|
|
||||||
selectedAIModel = aiService.currentModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
let providerBinding = Binding<AIProvider>(
|
|
||||||
get: {
|
|
||||||
if let providerName = selectedAIProvider,
|
|
||||||
let provider = AIProvider(rawValue: providerName) {
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
return aiService.selectedProvider
|
|
||||||
},
|
|
||||||
set: { newValue in
|
|
||||||
selectedAIProvider = newValue.rawValue
|
|
||||||
aiService.selectedProvider = newValue
|
|
||||||
selectedAIModel = nil
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if isAIEnhancementEnabled {
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("AI Provider")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if aiService.connectedProviders.isEmpty {
|
|
||||||
Text("No providers connected")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.italic()
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
} else {
|
|
||||||
Picker("", selection: providerBinding) {
|
|
||||||
ForEach(aiService.connectedProviders.filter { $0 != .elevenLabs && $0 != .deepgram }, id: \.self) { provider in
|
|
||||||
Text(provider.rawValue).tag(provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.onChange(of: selectedAIProvider) { oldValue, newValue in
|
|
||||||
if let provider = newValue.flatMap({ AIProvider(rawValue: $0) }) {
|
|
||||||
selectedAIModel = provider.defaultModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let providerName = selectedAIProvider ?? aiService.selectedProvider.rawValue
|
|
||||||
if let provider = AIProvider(rawValue: providerName),
|
|
||||||
provider != .custom {
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("AI Model")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if aiService.availableModels.isEmpty {
|
|
||||||
Text(provider == .openRouter ? "No models loaded" : "No models available")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.italic()
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
} else {
|
|
||||||
let modelBinding = Binding<String>(
|
|
||||||
get: {
|
|
||||||
if let model = selectedAIModel, !model.isEmpty {
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
return aiService.currentModel
|
|
||||||
},
|
|
||||||
set: { newModelValue in
|
|
||||||
selectedAIModel = newModelValue
|
|
||||||
aiService.selectModel(newModelValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
let models = provider == .openRouter ? aiService.availableModels : (provider == .ollama ? aiService.availableModels : provider.availableModels)
|
|
||||||
|
|
||||||
Picker("", selection: modelBinding) {
|
|
||||||
ForEach(models, id: \.self) { model in
|
|
||||||
Text(model).tag(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
|
|
||||||
if provider == .openRouter {
|
|
||||||
Button(action: {
|
|
||||||
Task {
|
|
||||||
await aiService.fetchOpenRouterModels()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Image(systemName: "arrow.clockwise")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.help("Refresh models")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Enhancement Prompt")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
PromptSelectionGrid(
|
|
||||||
prompts: enhancementService.allPrompts,
|
|
||||||
selectedPromptId: selectedPromptId,
|
|
||||||
onPromptSelected: { prompt in
|
|
||||||
selectedPromptId = prompt.id
|
|
||||||
},
|
|
||||||
onEditPrompt: { prompt in
|
|
||||||
selectedPromptForEdit = prompt
|
|
||||||
},
|
|
||||||
onDeletePrompt: { prompt in
|
|
||||||
enhancementService.deletePrompt(prompt)
|
|
||||||
},
|
|
||||||
onAddNewPrompt: {
|
|
||||||
isEditingPrompt = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
|
|
||||||
Toggle("Context Awareness", isOn: $useScreenCapture)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
SectionHeader(title: "Advanced")
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Toggle("Auto Send", isOn: $isAutoSendEnabled)
|
|
||||||
|
|
||||||
InfoTip(
|
|
||||||
title: "Auto Send",
|
|
||||||
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Keyboard Shortcut")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
|
|
||||||
.controlSize(.small)
|
|
||||||
|
|
||||||
InfoTip(
|
|
||||||
title: "Power Mode Hotkey",
|
|
||||||
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Button(action: saveConfiguration) {
|
|
||||||
Text(mode.isAdding ? "Add New Power Mode" : "Save Changes")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.fill(canSave ? Color(red: 0.3, green: 0.7, blue: 0.4) : Color(red: 0.3, green: 0.7, blue: 0.4).opacity(0.5))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.disabled(!canSave)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingAppPicker) {
|
.sheet(isPresented: $isShowingAppPicker) {
|
||||||
@ -680,15 +499,7 @@ struct ConfigurationView: View {
|
|||||||
onDismiss: { isShowingAppPicker = false }
|
onDismiss: { isShowingAppPicker = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isEditingPrompt) {
|
|
||||||
PromptEditorView(mode: .add)
|
|
||||||
}
|
|
||||||
.sheet(item: $selectedPromptForEdit) { prompt in
|
|
||||||
PromptEditorView(mode: .edit(prompt))
|
|
||||||
}
|
|
||||||
.powerModeValidationAlert(errors: validationErrors, isPresented: $showValidationAlert)
|
.powerModeValidationAlert(errors: validationErrors, isPresented: $showValidationAlert)
|
||||||
.navigationTitle("") // Explicitly set an empty title for this view
|
|
||||||
.toolbar(.hidden) // Attempt to hide the navigation bar area
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Set AI provider and model for new power modes after environment objects are available
|
// Set AI provider and model for new power modes after environment objects are available
|
||||||
if case .add = mode {
|
if case .add = mode {
|
||||||
@ -704,6 +515,11 @@ struct ConfigurationView: View {
|
|||||||
if isAIEnhancementEnabled && selectedPromptId == nil {
|
if isAIEnhancementEnabled && selectedPromptId == nil {
|
||||||
selectedPromptId = enhancementService.allPrompts.first?.id
|
selectedPromptId = enhancementService.allPrompts.first?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus the name field for faster keyboard-driven setup
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
isNameFieldFocused = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func placeholder<Content: View>(
|
func placeholder<Content: View>(
|
||||||
@ -69,6 +70,7 @@ struct PowerModeView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $navigationPath) {
|
NavigationStack(path: $navigationPath) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
// Header Section
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@ -80,7 +82,7 @@ struct PowerModeView: View {
|
|||||||
InfoTip(
|
InfoTip(
|
||||||
title: "What is Power Mode?",
|
title: "What is Power Mode?",
|
||||||
message: "Automatically apply custom configurations based on the app/website you are using",
|
message: "Automatically apply custom configurations based on the app/website you are using",
|
||||||
learnMoreURL: "https://www.youtube.com/@tryvoiceink/videos"
|
learnMoreURL: "https://tryvoiceink.com/docs/power-mode"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,120 +137,122 @@ struct PowerModeView: View {
|
|||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color(NSColor.windowBackgroundColor))
|
||||||
|
|
||||||
Rectangle()
|
// Content Section
|
||||||
.fill(Color(NSColor.separatorColor))
|
Group {
|
||||||
.frame(height: 1)
|
if isReorderMode {
|
||||||
.padding(.horizontal, 24)
|
VStack(spacing: 12) {
|
||||||
|
List {
|
||||||
if isReorderMode {
|
ForEach(powerModeManager.configurations) { config in
|
||||||
VStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
List {
|
ZStack {
|
||||||
ForEach(powerModeManager.configurations) { config in
|
Circle()
|
||||||
HStack(spacing: 12) {
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
ZStack {
|
.frame(width: 40, height: 40)
|
||||||
Circle()
|
Text(config.emoji)
|
||||||
.fill(Color(NSColor.controlBackgroundColor))
|
.font(.system(size: 20))
|
||||||
.frame(width: 40, height: 40)
|
|
||||||
Text(config.emoji)
|
|
||||||
.font(.system(size: 20))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(config.name)
|
|
||||||
.font(.system(size: 15, weight: .semibold))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
if config.isDefault {
|
|
||||||
Text("Default")
|
|
||||||
.font(.system(size: 11, weight: .medium))
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(Capsule().fill(Color.accentColor))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
}
|
||||||
if !config.isEnabled {
|
|
||||||
Text("Disabled")
|
Text(config.name)
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
.background(Capsule().fill(Color(NSColor.controlBackgroundColor)))
|
|
||||||
.overlay(
|
|
||||||
Capsule().stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
|
|
||||||
)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.padding(.horizontal, 14)
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
.listRowInsets(EdgeInsets())
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
}
|
|
||||||
.onMove(perform: powerModeManager.moveConfigurations)
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(Color(NSColor.controlBackgroundColor))
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 24)
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
} else {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
if powerModeManager.configurations.isEmpty {
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: geometry.size.height * 0.2)
|
|
||||||
|
HStack(spacing: 6) {
|
||||||
VStack(spacing: 16) {
|
if config.isDefault {
|
||||||
Image(systemName: "square.grid.2x2.fill")
|
Text("Default")
|
||||||
.font(.system(size: 48, weight: .regular))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundColor(.secondary.opacity(0.6))
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
VStack(spacing: 8) {
|
.background(Capsule().fill(Color.accentColor))
|
||||||
Text("No Power Modes Yet")
|
.foregroundColor(.white)
|
||||||
.font(.system(size: 20, weight: .medium))
|
}
|
||||||
.foregroundColor(.primary)
|
if !config.isEnabled {
|
||||||
|
Text("Disabled")
|
||||||
Text("Create first power mode to automate your VoiceInk workflow based on apps/website you are using")
|
.font(.system(size: 11, weight: .medium))
|
||||||
.font(.system(size: 14))
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Capsule().fill(Color(NSColor.controlBackgroundColor)))
|
||||||
|
.overlay(
|
||||||
|
Capsule().stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
|
||||||
|
)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineSpacing(2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.padding(.vertical, 12)
|
||||||
.frame(minHeight: geometry.size.height)
|
.padding(.horizontal, 14)
|
||||||
} else {
|
.background(CardBackground(isSelected: false))
|
||||||
VStack(spacing: 0) {
|
.listRowInsets(EdgeInsets())
|
||||||
PowerModeConfigurationsGrid(
|
.listRowBackground(Color.clear)
|
||||||
powerModeManager: powerModeManager,
|
.listRowSeparator(.hidden)
|
||||||
onEditConfig: { config in
|
.padding(.vertical, 6)
|
||||||
configurationMode = .edit(config)
|
}
|
||||||
navigationPath.append(configurationMode!)
|
.onMove(perform: powerModeManager.moveConfigurations)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
} else {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
if powerModeManager.configurations.isEmpty {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: geometry.size.height * 0.2)
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "square.grid.2x2.fill")
|
||||||
|
.font(.system(size: 48, weight: .regular))
|
||||||
|
.foregroundColor(.secondary.opacity(0.6))
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("No Power Modes Yet")
|
||||||
|
.font(.system(size: 20, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text("Create first power mode to automate your VoiceInk workflow based on apps/website you are using")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineSpacing(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.padding(.horizontal, 24)
|
Spacer()
|
||||||
.padding(.vertical, 20)
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
Spacer()
|
.frame(minHeight: geometry.size.height)
|
||||||
.frame(height: 40)
|
} else {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
PowerModeConfigurationsGrid(
|
||||||
|
powerModeManager: powerModeManager,
|
||||||
|
onEditConfig: { config in
|
||||||
|
configurationMode = .edit(config)
|
||||||
|
navigationPath.append(configurationMode!)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 40)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
}
|
}
|
||||||
.background(Color(NSColor.controlBackgroundColor))
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
.navigationDestination(for: ConfigurationMode.self) { mode in
|
.navigationDestination(for: ConfigurationMode.self) { mode in
|
||||||
@ -259,11 +263,9 @@ struct PowerModeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// New component for section headers
|
|
||||||
struct SectionHeader: View {
|
struct SectionHeader: View {
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .bold))
|
.font(.system(size: 16, weight: .bold))
|
||||||
|
|||||||
@ -65,7 +65,26 @@ struct PowerModeConfigurationsGrid: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Small, consistent icon-only add button used across Power Mode configuration rows.
|
||||||
|
struct AddIconButton: View {
|
||||||
|
let helpText: String
|
||||||
|
var isDisabled: Bool = false
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.system(size: 18))
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help(helpText)
|
||||||
|
.accessibilityLabel(helpText)
|
||||||
|
.disabled(isDisabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +185,7 @@ struct ConfigurationRow: View {
|
|||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if websiteCount > 0 {
|
if websiteCount > 0 {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "globe")
|
Image(systemName: "globe")
|
||||||
@ -176,6 +195,7 @@ struct ConfigurationRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +213,6 @@ struct ConfigurationRow: View {
|
|||||||
|
|
||||||
if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled || config.isAutoSendEnabled {
|
if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled || config.isAutoSendEnabled {
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.horizontal, 16)
|
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
if let model = selectedModel, model != "Default" {
|
if let model = selectedModel, model != "Default" {
|
||||||
@ -203,8 +222,8 @@ struct ConfigurationRow: View {
|
|||||||
Text(model)
|
Text(model)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color(NSColor.controlBackgroundColor)))
|
.fill(Color(NSColor.controlBackgroundColor)))
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -220,8 +239,8 @@ struct ConfigurationRow: View {
|
|||||||
Text(language)
|
Text(language)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color(NSColor.controlBackgroundColor)))
|
.fill(Color(NSColor.controlBackgroundColor)))
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -237,8 +256,8 @@ struct ConfigurationRow: View {
|
|||||||
Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName)
|
Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color(NSColor.controlBackgroundColor)))
|
.fill(Color(NSColor.controlBackgroundColor)))
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -254,8 +273,8 @@ struct ConfigurationRow: View {
|
|||||||
Text("Auto Send")
|
Text("Auto Send")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color(NSColor.controlBackgroundColor)))
|
.fill(Color(NSColor.controlBackgroundColor)))
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -271,8 +290,8 @@ struct ConfigurationRow: View {
|
|||||||
Text("Context Awareness")
|
Text("Context Awareness")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color(NSColor.controlBackgroundColor)))
|
.fill(Color(NSColor.controlBackgroundColor)))
|
||||||
.overlay(
|
.overlay(
|
||||||
@ -287,8 +306,8 @@ struct ConfigurationRow: View {
|
|||||||
Text(selectedPrompt?.title ?? "AI")
|
Text(selectedPrompt?.title ?? "AI")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 2)
|
||||||
.background(Capsule()
|
.background(Capsule()
|
||||||
.fill(Color.accentColor.opacity(0.1)))
|
.fill(Color.accentColor.opacity(0.1)))
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
@ -296,10 +315,13 @@ struct ConfigurationRow: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
|
||||||
|
.padding(.vertical, 6)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
|
.background(Color.secondary.opacity(0.1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
.background(CardBackground(isSelected: isEditing))
|
.background(CardBackground(isSelected: isEditing))
|
||||||
.opacity(config.isEnabled ? 1.0 : 0.5)
|
.opacity(config.isEnabled ? 1.0 : 0.5)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user