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
258
Sources/App/HotkeyRecorder.swift
Normal file
258
Sources/App/HotkeyRecorder.swift
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import SwiftUI
|
||||
import Carbon
|
||||
import CoreSettings
|
||||
|
||||
struct HotkeyRecorder: View {
|
||||
@Binding var hotkey: HotkeyConfig
|
||||
@State private var isRecording = false
|
||||
@State private var recordedKeyCode: UInt32?
|
||||
@State private var recordedModifiers: UInt32?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
if isRecording {
|
||||
stopRecording()
|
||||
} else {
|
||||
startRecording()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
if isRecording {
|
||||
Text(NSLocalizedString("hotkey.press_keys", comment: "Press keys..."))
|
||||
.foregroundColor(.primary)
|
||||
} else {
|
||||
Text(hotkeyDisplayString)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 150, minHeight: 30)
|
||||
.background(isRecording ? Color.blue.opacity(0.2) : Color(NSColor.controlBackgroundColor))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(isRecording ? Color.blue : Color.gray.opacity(0.3), lineWidth: 2)
|
||||
)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onKeyDown { event in
|
||||
guard isRecording else { return false }
|
||||
handleKeyEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
if hotkey.keyCode != HotkeyConfig.default.keyCode || hotkey.modifiers != HotkeyConfig.default.modifiers {
|
||||
Button(NSLocalizedString("hotkey.reset_default", comment: "Reset to Default")) {
|
||||
hotkey = HotkeyConfig.default
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.controlSize(.small)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Text(NSLocalizedString("hotkey.record_description", comment: "Click to record a new hotkey combination"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.focusable()
|
||||
}
|
||||
|
||||
private var hotkeyDisplayString: String {
|
||||
return hotkeyToString(keyCode: hotkey.keyCode, modifiers: hotkey.modifiers)
|
||||
}
|
||||
|
||||
private func startRecording() {
|
||||
isRecording = true
|
||||
recordedKeyCode = nil
|
||||
recordedModifiers = nil
|
||||
}
|
||||
|
||||
private func stopRecording() {
|
||||
if let keyCode = recordedKeyCode, let modifiers = recordedModifiers {
|
||||
hotkey = HotkeyConfig(keyCode: keyCode, modifiers: modifiers)
|
||||
}
|
||||
isRecording = false
|
||||
recordedKeyCode = nil
|
||||
recordedModifiers = nil
|
||||
}
|
||||
|
||||
private func handleKeyEvent(_ event: NSEvent) {
|
||||
let keyCode = event.keyCode
|
||||
let modifierFlags = event.modifierFlags
|
||||
|
||||
// Convert NSEvent modifier flags to Carbon modifier flags
|
||||
var carbonModifiers: UInt32 = 0
|
||||
|
||||
if modifierFlags.contains(.command) {
|
||||
carbonModifiers |= UInt32(cmdKey)
|
||||
}
|
||||
if modifierFlags.contains(.shift) {
|
||||
carbonModifiers |= UInt32(shiftKey)
|
||||
}
|
||||
if modifierFlags.contains(.option) {
|
||||
carbonModifiers |= UInt32(optionKey)
|
||||
}
|
||||
if modifierFlags.contains(.control) {
|
||||
carbonModifiers |= UInt32(controlKey)
|
||||
}
|
||||
|
||||
// Only accept combinations with at least one modifier
|
||||
guard carbonModifiers != 0 else { return }
|
||||
|
||||
recordedKeyCode = UInt32(keyCode)
|
||||
recordedModifiers = carbonModifiers
|
||||
|
||||
// Auto-stop recording after a brief delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
if isRecording {
|
||||
stopRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private func hotkeyToString(keyCode: UInt32, modifiers: UInt32) -> String {
|
||||
var result = ""
|
||||
|
||||
// Add modifier symbols
|
||||
if modifiers & UInt32(controlKey) != 0 {
|
||||
result += "⌃"
|
||||
}
|
||||
if modifiers & UInt32(optionKey) != 0 {
|
||||
result += "⌥"
|
||||
}
|
||||
if modifiers & UInt32(shiftKey) != 0 {
|
||||
result += "⇧"
|
||||
}
|
||||
if modifiers & UInt32(cmdKey) != 0 {
|
||||
result += "⌘"
|
||||
}
|
||||
|
||||
// Add key name
|
||||
result += keyCodeToString(keyCode)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func keyCodeToString(_ keyCode: UInt32) -> String {
|
||||
// Map common key codes to their string representations
|
||||
switch keyCode {
|
||||
case 0: return "A"
|
||||
case 1: return "S"
|
||||
case 2: return "D"
|
||||
case 3: return "F"
|
||||
case 4: return "H"
|
||||
case 5: return "G"
|
||||
case 6: return "Z"
|
||||
case 7: return "X"
|
||||
case 8: return "C"
|
||||
case 9: return "V"
|
||||
case 10: return "§"
|
||||
case 11: return "B"
|
||||
case 12: return "Q"
|
||||
case 13: return "W"
|
||||
case 14: return "E"
|
||||
case 15: return "R"
|
||||
case 16: return "Y"
|
||||
case 17: return "T"
|
||||
case 18: return "1"
|
||||
case 19: return "2"
|
||||
case 20: return "3"
|
||||
case 21: return "4"
|
||||
case 22: return "6"
|
||||
case 23: return "5"
|
||||
case 24: return "="
|
||||
case 25: return "9"
|
||||
case 26: return "7"
|
||||
case 27: return "-"
|
||||
case 28: return "8"
|
||||
case 29: return "0"
|
||||
case 30: return "]"
|
||||
case 31: return "O"
|
||||
case 32: return "U"
|
||||
case 33: return "["
|
||||
case 34: return "I"
|
||||
case 35: return "P"
|
||||
case 36: return "⏎"
|
||||
case 37: return "L"
|
||||
case 38: return "J"
|
||||
case 39: return "'"
|
||||
case 40: return "K"
|
||||
case 41: return ";"
|
||||
case 42: return "\\"
|
||||
case 43: return ","
|
||||
case 44: return "/"
|
||||
case 45: return "N"
|
||||
case 46: return "M"
|
||||
case 47: return "."
|
||||
case 48: return "⇥"
|
||||
case 49: return "Space"
|
||||
case 50: return "`"
|
||||
case 51: return "⌫"
|
||||
case 53: return "⎋"
|
||||
case 96: return "F5"
|
||||
case 97: return "F6"
|
||||
case 98: return "F7"
|
||||
case 99: return "F3"
|
||||
case 100: return "F8"
|
||||
case 101: return "F9"
|
||||
case 103: return "F11"
|
||||
case 105: return "F13"
|
||||
case 107: return "F14"
|
||||
case 109: return "F10"
|
||||
case 111: return "F12"
|
||||
case 113: return "F15"
|
||||
case 114: return "Help"
|
||||
case 115: return "Home"
|
||||
case 116: return "⇞"
|
||||
case 117: return "⌦"
|
||||
case 118: return "F4"
|
||||
case 119: return "End"
|
||||
case 120: return "F2"
|
||||
case 121: return "⇟"
|
||||
case 122: return "F1"
|
||||
case 123: return "←"
|
||||
case 124: return "→"
|
||||
case 125: return "↓"
|
||||
case 126: return "↑"
|
||||
default:
|
||||
return "Key \(keyCode)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
extension View {
|
||||
func onKeyDown(perform action: @escaping (NSEvent) -> Bool) -> some View {
|
||||
self.background(KeyEventHandlingView(onKeyDown: action))
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyEventHandlingView: NSViewRepresentable {
|
||||
let onKeyDown: (NSEvent) -> Bool
|
||||
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let view = KeyHandlingNSView()
|
||||
view.onKeyDown = onKeyDown
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {}
|
||||
}
|
||||
|
||||
class KeyHandlingNSView: NSView {
|
||||
var onKeyDown: ((NSEvent) -> Bool)?
|
||||
|
||||
override var acceptsFirstResponder: Bool { true }
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if let handler = onKeyDown, handler(event) {
|
||||
return
|
||||
}
|
||||
super.keyDown(with: event)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue