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
This commit is contained in:
parent
7ba5895406
commit
54c3b65d4a
25 changed files with 3086 additions and 235 deletions
|
|
@ -3,20 +3,23 @@ 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, initialTab: Int = 0) {
|
||||
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: 600, height: 500),
|
||||
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
|
|
@ -24,13 +27,16 @@ class PreferencesWindowController: NSWindowController {
|
|||
|
||||
super.init(window: window)
|
||||
|
||||
window.title = "MenuWhisper Preferences"
|
||||
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()
|
||||
|
|
@ -53,6 +59,7 @@ 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
|
||||
|
|
@ -61,10 +68,11 @@ struct PreferencesView: View {
|
|||
@State private var showingDeleteAlert = false
|
||||
@State private var modelToDelete: ModelInfo?
|
||||
|
||||
init(modelManager: ModelManager, whisperEngine: WhisperCPPEngine, permissionManager: PermissionManager, initialTab: Int = 0, onClose: @escaping () -> Void) {
|
||||
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)
|
||||
}
|
||||
|
|
@ -75,37 +83,56 @@ struct PreferencesView: View {
|
|||
|
||||
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("Models", systemImage: "brain.head.profile")
|
||||
Label(NSLocalizedString("preferences.models", comment: "Models"), systemImage: "brain.head.profile")
|
||||
}
|
||||
.tag(0)
|
||||
.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("Permissions", systemImage: "lock.shield")
|
||||
Label(NSLocalizedString("preferences.permissions", comment: "Permissions"), systemImage: "lock.shield")
|
||||
}
|
||||
.tag(1)
|
||||
|
||||
GeneralTab()
|
||||
.tabItem {
|
||||
Label("General", systemImage: "gearshape")
|
||||
}
|
||||
.tag(2)
|
||||
.tag(5)
|
||||
}
|
||||
.frame(width: 600, height: 500)
|
||||
.alert("Delete Model", isPresented: $showingDeleteAlert) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
.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("Delete", role: .destructive) {
|
||||
Button(NSLocalizedString("preferences.models.delete", comment: "Delete"), role: .destructive) {
|
||||
if let model = modelToDelete {
|
||||
deleteModel(model)
|
||||
}
|
||||
|
|
@ -113,7 +140,7 @@ struct PreferencesView: View {
|
|||
}
|
||||
} message: {
|
||||
if let model = modelToDelete {
|
||||
Text("Are you sure you want to delete '\(model.name)'? This action cannot be undone.")
|
||||
Text(String(format: NSLocalizedString("preferences.models.delete_confirm", comment: "Delete confirmation"), model.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +157,7 @@ struct PreferencesView: View {
|
|||
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]
|
||||
|
|
@ -138,17 +166,17 @@ struct ModelsTab: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Speech Recognition Models")
|
||||
Text(NSLocalizedString("preferences.models.title", comment: "Speech Recognition Models"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Download and manage speech recognition models. Larger models provide better accuracy but use more memory and processing time.")
|
||||
Text(NSLocalizedString("preferences.models.description", comment: "Model description"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Current Model Status
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Current Model")
|
||||
Text(NSLocalizedString("preferences.models.current_model", comment: "Current Model"))
|
||||
.font(.headline)
|
||||
|
||||
if let activeModel = modelManager.activeModel {
|
||||
|
|
@ -168,7 +196,7 @@ struct ModelsTab: View {
|
|||
.fill(whisperEngine.isModelLoaded() ? Color.green : Color.orange)
|
||||
.frame(width: 8, height: 8)
|
||||
|
||||
Text(whisperEngine.isModelLoaded() ? "Loaded" : "Loading...")
|
||||
Text(whisperEngine.isModelLoaded() ? NSLocalizedString("status.loaded", comment: "Loaded") : NSLocalizedString("status.loading", comment: "Loading..."))
|
||||
.font(.caption)
|
||||
.foregroundColor(whisperEngine.isModelLoaded() ? .green : .orange)
|
||||
}
|
||||
|
|
@ -176,7 +204,7 @@ struct ModelsTab: View {
|
|||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
} else {
|
||||
Text("No model selected")
|
||||
Text(NSLocalizedString("preferences.models.no_model", comment: "No model selected"))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -185,9 +213,47 @@ struct ModelsTab: View {
|
|||
}
|
||||
}
|
||||
|
||||
// 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("Available Models")
|
||||
Text(NSLocalizedString("preferences.models.available_models", comment: "Available Models"))
|
||||
.font(.headline)
|
||||
|
||||
ScrollView {
|
||||
|
|
@ -286,7 +352,7 @@ struct ModelRow: View {
|
|||
.fontWeight(.medium)
|
||||
|
||||
if isActive {
|
||||
Text("ACTIVE")
|
||||
Text(NSLocalizedString("preferences.models.active_badge", comment: "ACTIVE"))
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
|
|
@ -315,13 +381,13 @@ struct ModelRow: View {
|
|||
if model.isDownloaded {
|
||||
HStack(spacing: 8) {
|
||||
if !isActive {
|
||||
Button("Select") {
|
||||
Button(NSLocalizedString("general.select", comment: "Select")) {
|
||||
onSelect()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
Button("Delete") {
|
||||
Button(NSLocalizedString("preferences.models.delete", comment: "Delete")) {
|
||||
onDelete()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -329,17 +395,20 @@ struct ModelRow: View {
|
|||
}
|
||||
} else {
|
||||
if isDownloading {
|
||||
VStack {
|
||||
ProgressView(value: downloadProgress)
|
||||
.frame(width: 80)
|
||||
Text("\(Int(downloadProgress * 100))%")
|
||||
HStack {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text(NSLocalizedString("preferences.models.downloading", comment: "Downloading..."))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Button("Download") {
|
||||
Button(NSLocalizedString("preferences.models.download", comment: "Download")) {
|
||||
onDownload()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityLabel("Download \(model.name) model")
|
||||
.accessibilityHint("Downloads the speech recognition model for offline use")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -359,19 +428,19 @@ struct PermissionsTab: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Permissions")
|
||||
Text(NSLocalizedString("preferences.permissions", comment: "Permissions"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("MenuWhisper requires certain system permissions to function properly. Click the buttons below to grant permissions in System Settings.")
|
||||
Text(NSLocalizedString("preferences.permissions.description", comment: "Permissions description"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Microphone Permission
|
||||
PermissionRow(
|
||||
title: "Microphone",
|
||||
description: "Required to capture speech for transcription",
|
||||
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)
|
||||
|
|
@ -385,8 +454,8 @@ struct PermissionsTab: View {
|
|||
|
||||
// Accessibility Permission
|
||||
PermissionRow(
|
||||
title: "Accessibility",
|
||||
description: "Required to insert transcribed text into other applications",
|
||||
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)
|
||||
|
|
@ -400,8 +469,8 @@ struct PermissionsTab: View {
|
|||
|
||||
// Input Monitoring Permission
|
||||
PermissionRow(
|
||||
title: "Input Monitoring",
|
||||
description: "Required to send keyboard events for text insertion",
|
||||
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)
|
||||
|
|
@ -417,17 +486,17 @@ struct PermissionsTab: View {
|
|||
|
||||
// Help text
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Need Help?")
|
||||
Text(NSLocalizedString("preferences.permissions.need_help", comment: "Need Help?"))
|
||||
.font(.headline)
|
||||
|
||||
Text("After granting permissions in System Settings:")
|
||||
Text(NSLocalizedString("preferences.permissions.after_granting", comment: "After granting permissions"))
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("1. Close System Settings")
|
||||
Text("2. Click 'Refresh Status' to update permission status")
|
||||
Text("3. Some permissions may require restarting MenuWhisper")
|
||||
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)
|
||||
|
|
@ -479,14 +548,14 @@ struct PermissionRow: View {
|
|||
|
||||
VStack(spacing: 6) {
|
||||
if status != .granted {
|
||||
Button("Open System Settings") {
|
||||
Button(NSLocalizedString("permissions.open_settings", comment: "Open System Settings")) {
|
||||
onOpenSettings()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
Button("Refresh Status") {
|
||||
Button(NSLocalizedString("permissions.refresh_status", comment: "Refresh Status")) {
|
||||
onRefresh()
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
|
@ -510,30 +579,415 @@ struct PermissionRow: View {
|
|||
private var statusText: String {
|
||||
switch status {
|
||||
case .granted:
|
||||
return "Granted"
|
||||
return NSLocalizedString("status.granted", comment: "Granted")
|
||||
case .denied:
|
||||
return "Denied"
|
||||
return NSLocalizedString("status.denied", comment: "Denied")
|
||||
case .notDetermined:
|
||||
return "Not Set"
|
||||
return NSLocalizedString("status.not_set", comment: "Not Set")
|
||||
case .restricted:
|
||||
return "Restricted"
|
||||
return NSLocalizedString("status.restricted", comment: "Restricted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GeneralTab: View {
|
||||
@ObservedObject var settings: CoreSettings.Settings
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("General Settings")
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text(NSLocalizedString("preferences.general.title", comment: "General Settings"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Additional settings will be available in Phase 4.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue