tellme/Sources/App/PreferencesWindow.swift
Felipe M. 54c3b65d4a
Complete Phase 4: Comprehensive preferences, localization, and UX polish
- Rename application from MenuWhisper to Tell me with new domain com.fmartingr.tellme
- Implement comprehensive preferences window with 6 tabs (General, Models, Text Insertion, Interface, Advanced, Permissions)
- Add full English/Spanish localization for all UI elements
- Create functional onboarding flow with model download capability
- Implement preview dialog for transcription editing
- Add settings export/import functionality
- Fix HUD content display issues and add comprehensive permission checking
- Enhance build scripts and app bundle creation for proper localization support
2025-09-19 13:55:46 +02:00

993 lines
No EOL
39 KiB
Swift

import SwiftUI
import CoreModels
import CoreSTT
import CoreUtils
import CorePermissions
import CoreSettings
class PreferencesWindowController: NSWindowController {
private let modelManager: ModelManager
private let whisperEngine: WhisperCPPEngine
private let permissionManager: PermissionManager
private let settings: CoreSettings.Settings
private var preferencesView: PreferencesView?
init(modelManager: ModelManager, whisperEngine: WhisperCPPEngine, permissionManager: PermissionManager, settings: CoreSettings.Settings, initialTab: Int = 0) {
self.modelManager = modelManager
self.whisperEngine = whisperEngine
self.permissionManager = permissionManager
self.settings = settings
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
super.init(window: window)
window.title = NSLocalizedString("preferences.title", comment: "Tell me Preferences")
window.center()
window.minSize = NSSize(width: 750, height: 600)
window.maxSize = NSSize(width: 1200, height: 800)
preferencesView = PreferencesView(
modelManager: modelManager,
whisperEngine: whisperEngine,
permissionManager: permissionManager,
settings: settings,
initialTab: initialTab,
onClose: { [weak self] in
self?.close()
}
)
window.contentView = NSHostingView(rootView: preferencesView!)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSelectedTab(_ tabIndex: Int) {
preferencesView?.setSelectedTab(tabIndex)
}
}
struct PreferencesView: View {
@ObservedObject var modelManager: ModelManager
let whisperEngine: WhisperCPPEngine
@ObservedObject var permissionManager: PermissionManager
@ObservedObject var settings: CoreSettings.Settings
let onClose: () -> Void
@State private var selectedTab: Int
@State private var isDownloading: [String: Bool] = [:]
@State private var downloadProgress: [String: Double] = [:]
@State private var showingDeleteAlert = false
@State private var modelToDelete: ModelInfo?
init(modelManager: ModelManager, whisperEngine: WhisperCPPEngine, permissionManager: PermissionManager, settings: CoreSettings.Settings, initialTab: Int = 0, onClose: @escaping () -> Void) {
self.modelManager = modelManager
self.whisperEngine = whisperEngine
self.permissionManager = permissionManager
self.settings = settings
self.onClose = onClose
self._selectedTab = State(initialValue: initialTab)
}
func setSelectedTab(_ tabIndex: Int) {
selectedTab = tabIndex
}
var body: some View {
TabView(selection: $selectedTab) {
GeneralTab(settings: settings)
.tabItem {
Label(NSLocalizedString("preferences.general", comment: "General"), systemImage: "gearshape")
}
.tag(0)
ModelsTab(
modelManager: modelManager,
whisperEngine: whisperEngine,
settings: settings,
isDownloading: $isDownloading,
downloadProgress: $downloadProgress,
showingDeleteAlert: $showingDeleteAlert,
modelToDelete: $modelToDelete
)
.tabItem {
Label(NSLocalizedString("preferences.models", comment: "Models"), systemImage: "brain.head.profile")
}
.tag(1)
InsertionTab(settings: settings)
.tabItem {
Label(NSLocalizedString("preferences.insertion", comment: "Text Insertion"), systemImage: "text.cursor")
}
.tag(2)
HUDTab(settings: settings)
.tabItem {
Label(NSLocalizedString("preferences.interface", comment: "Interface"), systemImage: "rectangle.on.rectangle")
}
.tag(3)
AdvancedTab(settings: settings)
.tabItem {
Label(NSLocalizedString("preferences.advanced", comment: "Advanced"), systemImage: "slider.horizontal.3")
}
.tag(4)
PermissionsTab(permissionManager: permissionManager)
.tabItem {
Label(NSLocalizedString("preferences.permissions", comment: "Permissions"), systemImage: "lock.shield")
}
.tag(5)
}
.frame(minWidth: 750, idealWidth: 800, maxWidth: 1200, minHeight: 600, idealHeight: 600, maxHeight: 800)
.alert(NSLocalizedString("alert.delete_model", comment: "Delete Model"), isPresented: $showingDeleteAlert) {
Button(NSLocalizedString("general.cancel", comment: "Cancel"), role: .cancel) {
modelToDelete = nil
}
Button(NSLocalizedString("preferences.models.delete", comment: "Delete"), role: .destructive) {
if let model = modelToDelete {
deleteModel(model)
}
modelToDelete = nil
}
} message: {
if let model = modelToDelete {
Text(String(format: NSLocalizedString("preferences.models.delete_confirm", comment: "Delete confirmation"), model.name))
}
}
}
private func deleteModel(_ model: ModelInfo) {
do {
try modelManager.deleteModel(model)
} catch {
print("Failed to delete model: \(error)")
}
}
}
struct ModelsTab: View {
@ObservedObject var modelManager: ModelManager
let whisperEngine: WhisperCPPEngine
@ObservedObject var settings: CoreSettings.Settings
@Binding var isDownloading: [String: Bool]
@Binding var downloadProgress: [String: Double]
@Binding var showingDeleteAlert: Bool
@Binding var modelToDelete: ModelInfo?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(NSLocalizedString("preferences.models.title", comment: "Speech Recognition Models"))
.font(.title2)
.fontWeight(.semibold)
Text(NSLocalizedString("preferences.models.description", comment: "Model description"))
.font(.caption)
.foregroundColor(.secondary)
// Current Model Status
VStack(alignment: .leading, spacing: 8) {
Text(NSLocalizedString("preferences.models.current_model", comment: "Current Model"))
.font(.headline)
if let activeModel = modelManager.activeModel {
HStack {
VStack(alignment: .leading) {
Text(activeModel.name)
.font(.body)
.fontWeight(.medium)
Text("\(activeModel.sizeMB) MB • \(activeModel.qualityTier) quality • \(activeModel.estimatedRAM)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Circle()
.fill(whisperEngine.isModelLoaded() ? Color.green : Color.orange)
.frame(width: 8, height: 8)
Text(whisperEngine.isModelLoaded() ? NSLocalizedString("status.loaded", comment: "Loaded") : NSLocalizedString("status.loading", comment: "Loading..."))
.font(.caption)
.foregroundColor(whisperEngine.isModelLoaded() ? .green : .orange)
}
.padding(12)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
} else {
Text(NSLocalizedString("preferences.models.no_model", comment: "No model selected"))
.foregroundColor(.secondary)
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
// Language Settings
VStack(alignment: .leading, spacing: 8) {
Text(NSLocalizedString("preferences.models.language", comment: "Language"))
.font(.headline)
HStack {
Text(NSLocalizedString("preferences.models.recognition_language", comment: "Recognition Language:"))
.font(.body)
Picker(NSLocalizedString("general.language", comment: "Language"), selection: Binding(
get: { settings.forcedLanguage ?? "auto" },
set: { newValue in
settings.forcedLanguage = newValue == "auto" ? nil : newValue
}
)) {
Text(NSLocalizedString("language.auto_detect", comment: "Auto-detect")).tag("auto")
Divider()
Text(NSLocalizedString("language.english", comment: "English")).tag("en")
Text(NSLocalizedString("language.spanish", comment: "Spanish")).tag("es")
Text(NSLocalizedString("language.french", comment: "French")).tag("fr")
Text(NSLocalizedString("language.german", comment: "German")).tag("de")
Text(NSLocalizedString("language.italian", comment: "Italian")).tag("it")
Text(NSLocalizedString("language.portuguese", comment: "Portuguese")).tag("pt")
Text(NSLocalizedString("language.russian", comment: "Russian")).tag("ru")
Text(NSLocalizedString("language.chinese", comment: "Chinese")).tag("zh")
Text(NSLocalizedString("language.japanese", comment: "Japanese")).tag("ja")
Text(NSLocalizedString("language.korean", comment: "Korean")).tag("ko")
}
.pickerStyle(.menu)
.frame(width: 150)
Spacer()
}
.padding(12)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
// Available Models
VStack(alignment: .leading, spacing: 8) {
Text(NSLocalizedString("preferences.models.available_models", comment: "Available Models"))
.font(.headline)
ScrollView {
LazyVStack(spacing: 8) {
ForEach(modelManager.availableModels) { model in
ModelRow(
model: model,
modelManager: modelManager,
whisperEngine: whisperEngine,
isDownloading: isDownloading[model.name] ?? false,
downloadProgress: downloadProgress[model.name] ?? 0.0,
onDownload: {
downloadModel(model)
},
onSelect: {
selectModel(model)
},
onDelete: {
modelToDelete = model
showingDeleteAlert = true
}
)
}
}
}
.frame(maxHeight: 200)
}
Spacer()
}
.padding(20)
}
private func downloadModel(_ model: ModelInfo) {
isDownloading[model.name] = true
downloadProgress[model.name] = 0.0
Task {
do {
try await modelManager.downloadModel(model) { progress in
DispatchQueue.main.async {
downloadProgress[model.name] = progress.progress
}
}
DispatchQueue.main.async {
isDownloading[model.name] = false
downloadProgress[model.name] = 1.0
}
} catch {
DispatchQueue.main.async {
isDownloading[model.name] = false
downloadProgress[model.name] = 0.0
}
print("Download failed: \(error)")
}
}
}
private func selectModel(_ model: ModelInfo) {
modelManager.setActiveModel(model)
Task {
do {
if let modelPath = modelManager.getModelPath(for: model) {
try await whisperEngine.loadModel(at: modelPath)
}
} catch {
print("Failed to load model: \(error)")
}
}
}
}
struct ModelRow: View {
let model: ModelInfo
@ObservedObject var modelManager: ModelManager
let whisperEngine: WhisperCPPEngine
let isDownloading: Bool
let downloadProgress: Double
let onDownload: () -> Void
let onSelect: () -> Void
let onDelete: () -> Void
private var isActive: Bool {
modelManager.activeModel?.name == model.name
}
var body: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(model.name)
.font(.body)
.fontWeight(.medium)
if isActive {
Text(NSLocalizedString("preferences.models.active_badge", comment: "ACTIVE"))
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue)
.cornerRadius(4)
}
}
Text("\(model.sizeMB) MB • \(model.qualityTier) quality • \(model.estimatedRAM)")
.font(.caption)
.foregroundColor(.secondary)
if !model.notes.isEmpty {
Text(model.notes)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
Spacer()
VStack(spacing: 8) {
if model.isDownloaded {
HStack(spacing: 8) {
if !isActive {
Button(NSLocalizedString("general.select", comment: "Select")) {
onSelect()
}
.buttonStyle(.bordered)
}
Button(NSLocalizedString("preferences.models.delete", comment: "Delete")) {
onDelete()
}
.buttonStyle(.bordered)
.foregroundColor(.red)
}
} else {
if isDownloading {
HStack {
ProgressView()
.scaleEffect(0.8)
Text(NSLocalizedString("preferences.models.downloading", comment: "Downloading..."))
.font(.caption)
.foregroundColor(.secondary)
}
} else {
Button(NSLocalizedString("preferences.models.download", comment: "Download")) {
onDownload()
}
.buttonStyle(.bordered)
.accessibilityLabel("Download \(model.name) model")
.accessibilityHint("Downloads the speech recognition model for offline use")
}
}
}
}
.padding(12)
.background(isActive ? Color.blue.opacity(0.1) : Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(isActive ? Color.blue : Color.clear, lineWidth: 2)
)
}
}
struct PermissionsTab: View {
@ObservedObject var permissionManager: PermissionManager
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(NSLocalizedString("preferences.permissions", comment: "Permissions"))
.font(.title2)
.fontWeight(.semibold)
Text(NSLocalizedString("preferences.permissions.description", comment: "Permissions description"))
.font(.caption)
.foregroundColor(.secondary)
VStack(alignment: .leading, spacing: 12) {
// Microphone Permission
PermissionRow(
title: NSLocalizedString("permissions.microphone.title_short", comment: "Microphone"),
description: NSLocalizedString("permissions.microphone.description_short", comment: "Microphone description"),
status: permissionManager.microphoneStatus,
onOpenSettings: {
permissionManager.openSystemSettings(for: .microphone)
},
onRefresh: {
permissionManager.checkAllPermissions()
}
)
Divider()
// Accessibility Permission
PermissionRow(
title: NSLocalizedString("permissions.accessibility.title_short", comment: "Accessibility"),
description: NSLocalizedString("permissions.accessibility.description_short", comment: "Accessibility description"),
status: permissionManager.accessibilityStatus,
onOpenSettings: {
permissionManager.openSystemSettings(for: .accessibility)
},
onRefresh: {
permissionManager.checkAllPermissions()
}
)
Divider()
// Input Monitoring Permission
PermissionRow(
title: NSLocalizedString("permissions.input_monitoring.title_short", comment: "Input Monitoring"),
description: NSLocalizedString("permissions.input_monitoring.description_short", comment: "Input Monitoring description"),
status: permissionManager.inputMonitoringStatus,
onOpenSettings: {
permissionManager.openSystemSettings(for: .inputMonitoring)
},
onRefresh: {
permissionManager.checkAllPermissions()
}
)
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
// Help text
VStack(alignment: .leading, spacing: 8) {
Text(NSLocalizedString("preferences.permissions.need_help", comment: "Need Help?"))
.font(.headline)
Text(NSLocalizedString("preferences.permissions.after_granting", comment: "After granting permissions"))
.font(.body)
.foregroundColor(.secondary)
VStack(alignment: .leading, spacing: 4) {
Text(NSLocalizedString("preferences.permissions.step1", comment: "Step 1"))
Text(NSLocalizedString("preferences.permissions.step2", comment: "Step 2"))
Text(NSLocalizedString("preferences.permissions.step3", comment: "Step 3"))
}
.font(.caption)
.foregroundColor(.secondary)
.padding(.leading, 8)
}
Spacer()
}
.padding(20)
}
}
struct PermissionRow: View {
let title: String
let description: String
let status: PermissionStatus
let onOpenSettings: () -> Void
let onRefresh: () -> Void
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(title)
.font(.body)
.fontWeight(.medium)
Spacer()
// Status indicator
HStack(spacing: 4) {
Circle()
.fill(statusColor)
.frame(width: 8, height: 8)
Text(statusText)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(statusColor)
}
}
Text(description)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
VStack(spacing: 6) {
if status != .granted {
Button(NSLocalizedString("permissions.open_settings", comment: "Open System Settings")) {
onOpenSettings()
}
.buttonStyle(.bordered)
.controlSize(.small)
}
Button(NSLocalizedString("permissions.refresh_status", comment: "Refresh Status")) {
onRefresh()
}
.buttonStyle(.borderless)
.controlSize(.small)
.foregroundColor(.secondary)
}
}
}
private var statusColor: Color {
switch status {
case .granted:
return .green
case .denied:
return .red
case .notDetermined, .restricted:
return .orange
}
}
private var statusText: String {
switch status {
case .granted:
return NSLocalizedString("status.granted", comment: "Granted")
case .denied:
return NSLocalizedString("status.denied", comment: "Denied")
case .notDetermined:
return NSLocalizedString("status.not_set", comment: "Not Set")
case .restricted:
return NSLocalizedString("status.restricted", comment: "Restricted")
}
}
}
struct GeneralTab: View {
@ObservedObject var settings: CoreSettings.Settings
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text(NSLocalizedString("preferences.general.title", comment: "General Settings"))
.font(.title2)
.fontWeight(.semibold)
// Hotkey Configuration
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.general.global_hotkey", comment: "Global Hotkey"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(NSLocalizedString("preferences.general.hotkey_combination", comment: "Hotkey Combination:"))
.frame(width: 140, alignment: .leading)
HotkeyRecorder(hotkey: $settings.hotkey)
Spacer()
}
HStack {
Text(NSLocalizedString("preferences.general.mode", comment: "Activation Mode:"))
.frame(width: 140, alignment: .leading)
Picker(NSLocalizedString("general.mode", comment: "Mode"), selection: $settings.hotkeyMode) {
ForEach(HotkeyMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.pickerStyle(.segmented)
.frame(width: 200)
Spacer()
}
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Audio and Timing
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.general.audio_timing", comment: "Audio & Timing"))
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
HStack {
Toggle(NSLocalizedString("preferences.general.sounds", comment: "Play sounds"), isOn: $settings.playSounds)
Spacer()
}
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(NSLocalizedString("preferences.general.limit", comment: "Dictation time limit:"))
Spacer()
Text("\(Int(settings.dictationTimeLimit / 60)) \(NSLocalizedString("preferences.general.minutes", comment: "minutes"))")
.foregroundColor(.secondary)
}
Slider(
value: Binding(
get: { settings.dictationTimeLimit / 60 },
set: { settings.dictationTimeLimit = $0 * 60 }
),
in: 1...30,
step: 1
) {
Text(NSLocalizedString("preferences.general.time_limit_slider", comment: "Time Limit"))
}
}
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Settings Management
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.general.settings_management", comment: "Settings Management"))
.font(.headline)
HStack(spacing: 12) {
Button(NSLocalizedString("preferences.general.export_settings", comment: "Export Settings...")) {
exportSettings()
}
.buttonStyle(.bordered)
Button(NSLocalizedString("preferences.general.import_settings", comment: "Import Settings...")) {
importSettings()
}
.buttonStyle(.bordered)
Spacer()
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
Spacer()
}
.padding(20)
}
private func exportSettings() {
let panel = NSSavePanel()
panel.allowedContentTypes = [.json]
panel.nameFieldStringValue = "\(NSLocalizedString("app.name", comment: "App name")) Settings.json"
if panel.runModal() == .OK, let url = panel.url {
do {
let data = try settings.exportSettings()
try data.write(to: url)
// Show success message
showAlert(title: NSLocalizedString("alert.success", comment: "Success"), message: NSLocalizedString("success.settings.exported", comment: "Settings exported"))
} catch {
showAlert(title: NSLocalizedString("alert.error", comment: "Error"), message: "Failed to export settings: \(error.localizedDescription)")
}
}
}
private func importSettings() {
let panel = NSOpenPanel()
panel.allowedContentTypes = [.json]
panel.allowsMultipleSelection = false
if panel.runModal() == .OK, let url = panel.url {
do {
let data = try Data(contentsOf: url)
try settings.importSettings(from: data)
showAlert(title: NSLocalizedString("alert.success", comment: "Success"), message: NSLocalizedString("success.settings.imported", comment: "Settings imported"))
} catch {
showAlert(title: NSLocalizedString("alert.error", comment: "Error"), message: "Failed to import settings: \(error.localizedDescription)")
}
}
}
private func showAlert(title: String, message: String) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.runModal()
}
}
// MARK: - New Tab Views
struct InsertionTab: View {
@ObservedObject var settings: CoreSettings.Settings
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text(NSLocalizedString("preferences.insertion.title", comment: "Text Insertion"))
.font(.title2)
.fontWeight(.semibold)
// Insertion Method
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.insertion.method", comment: "Insertion Method:"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
Picker(NSLocalizedString("general.method", comment: "Method"), selection: $settings.insertionMethod) {
ForEach(InsertionMethod.allCases, id: \.self) { method in
Text(method.displayName).tag(method)
}
}
.pickerStyle(.segmented)
Text(NSLocalizedString("preferences.insertion.method_description", comment: "Insertion method description"))
.font(.caption)
.foregroundColor(.secondary)
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Preview Settings
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.insertion.preview", comment: "Preview"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
Toggle(NSLocalizedString("preferences.insertion.preview", comment: "Show preview"), isOn: $settings.showPreview)
Text(NSLocalizedString("preferences.insertion.preview_description", comment: "Preview description"))
.font(.caption)
.foregroundColor(.secondary)
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Secure Input Information
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.insertion.secure_input_title", comment: "Secure Input Handling"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "info.circle")
.foregroundColor(.blue)
Text(NSLocalizedString("preferences.insertion.secure_input_description", comment: "Secure input description"))
.font(.body)
}
}
.padding(16)
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
Spacer()
}
.padding(20)
}
}
struct HUDTab: View {
@ObservedObject var settings: CoreSettings.Settings
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text(NSLocalizedString("preferences.hud.title", comment: "Interface Settings"))
.font(.title2)
.fontWeight(.semibold)
// HUD Appearance
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.hud.appearance", comment: "HUD Appearance"))
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(NSLocalizedString("preferences.hud.opacity", comment: "Opacity:"))
Spacer()
Text("\(Int(settings.hudOpacity * 100))%")
.foregroundColor(.secondary)
}
Slider(value: $settings.hudOpacity, in: 0.3...1.0, step: 0.1) {
Text(NSLocalizedString("preferences.hud.opacity_slider", comment: "HUD Opacity"))
}
}
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(NSLocalizedString("preferences.hud.size", comment: "Size:"))
Spacer()
Text("\(Int(settings.hudSize * 100))%")
.foregroundColor(.secondary)
}
Slider(value: $settings.hudSize, in: 0.8...1.5, step: 0.1) {
Text(NSLocalizedString("preferences.hud.size_slider", comment: "HUD Size"))
}
}
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Audio Feedback
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.hud.audio_feedback", comment: "Audio Feedback"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
Toggle(NSLocalizedString("preferences.general.sounds", comment: "Play sounds for dictation"), isOn: $settings.playSounds)
Text(NSLocalizedString("preferences.hud.sounds_description", comment: "Sounds description"))
.font(.caption)
.foregroundColor(.secondary)
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
Spacer()
}
.padding(20)
}
}
struct AdvancedTab: View {
@ObservedObject var settings: CoreSettings.Settings
@State private var showingResetAlert = false
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text(NSLocalizedString("preferences.advanced.title", comment: "Advanced Settings"))
.font(.title2)
.fontWeight(.semibold)
// Processing Settings
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.advanced.processing", comment: "Processing"))
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(NSLocalizedString("preferences.advanced.threads", comment: "Processing threads:"))
Spacer()
Text("\(settings.processingThreads)")
.foregroundColor(.secondary)
}
Slider(
value: Binding(
get: { Double(settings.processingThreads) },
set: { settings.processingThreads = Int($0) }
),
in: 1...8,
step: 1
) {
Text(NSLocalizedString("preferences.advanced.threads_slider", comment: "Processing Threads"))
}
Text(NSLocalizedString("preferences.advanced.threads_description", comment: "Threads description"))
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Logging Settings
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.advanced.logging", comment: "Logging"))
.font(.headline)
VStack(alignment: .leading, spacing: 8) {
Toggle(NSLocalizedString("preferences.advanced.enable_logging", comment: "Enable logging"), isOn: $settings.enableLogging)
Text(NSLocalizedString("preferences.advanced.logging_description", comment: "Logging description"))
.font(.caption)
.foregroundColor(.secondary)
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
// Reset Settings
VStack(alignment: .leading, spacing: 12) {
Text(NSLocalizedString("preferences.advanced.reset", comment: "Reset"))
.font(.headline)
HStack {
Button(NSLocalizedString("preferences.advanced.reset_button", comment: "Reset All Settings")) {
showingResetAlert = true
}
.buttonStyle(.bordered)
.foregroundColor(.red)
Spacer()
}
.padding(16)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(12)
}
Spacer()
}
.padding(20)
.alert(NSLocalizedString("preferences.advanced.reset_title", comment: "Reset Settings"), isPresented: $showingResetAlert) {
Button(NSLocalizedString("general.cancel", comment: "Cancel"), role: .cancel) { }
Button(NSLocalizedString("general.reset", comment: "Reset"), role: .destructive) {
resetSettings()
}
} message: {
Text(NSLocalizedString("preferences.advanced.reset_message", comment: "Reset confirmation"))
}
}
private func resetSettings() {
// Reset all settings to defaults
settings.hotkey = HotkeyConfig.default
settings.hotkeyMode = .pushToTalk
settings.playSounds = false
settings.dictationTimeLimit = 600
settings.hudOpacity = 0.9
settings.hudSize = 1.0
settings.forcedLanguage = nil
settings.insertionMethod = .paste
settings.showPreview = false
settings.enableLogging = false
settings.processingThreads = 4
}
}