Browse Source

feat: adaptive menu bar icon, fully English UI, double-tap Fn preset, and advanced settings

main
Ali Alavi 17 hours ago
parent
commit
ab4b435331
  1. 8
      src/AppDelegate.swift
  2. 70
      src/HotkeyManager.swift
  3. 14
      src/SonioxClient.swift
  4. 135
      src/SonioxSettings.swift
  5. 122
      src/StatusBarController.swift

8
src/AppDelegate.swift

@ -80,8 +80,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
} }
private func playSystemSound(name: String) { private func playSystemSound(name: String) {
let soundsEnabled = UserDefaults.standard.bool(forKey: "SonioxPlaySounds")
if soundsEnabled {
if SonioxSettings.shared.playSounds {
NSSound(named: NSSound.Name(name))?.play() NSSound(named: NSSound.Name(name))?.play()
} }
} }
@ -172,8 +171,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
private func pasteTextToFrontmostApp(text: String) { private func pasteTextToFrontmostApp(text: String) {
guard !text.isEmpty else { return } guard !text.isEmpty else { return }
// 1. Put into system pasteboard with a trailing space
let textToPaste = text + " "
// 1. Put into system pasteboard
let textToPaste = SonioxSettings.shared.addTrailingSpace ? (text + " ") : text
let pb = NSPasteboard.general let pb = NSPasteboard.general
pb.clearContents() pb.clearContents()
pb.setString(textToPaste, forType: .string) pb.setString(textToPaste, forType: .string)
@ -183,7 +182,6 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
print("Accessibility permission NOT granted for auto-paste!") print("Accessibility permission NOT granted for auto-paste!")
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
AXIsProcessTrustedWithOptions(options) AXIsProcessTrustedWithOptions(options)
HUDOverlayController.shared.show(state: .error(message: "نیاز به تیک Accessibility برای تایپ خودکار"))
return return
} }

70
src/HotkeyManager.swift

@ -3,21 +3,23 @@ import Carbon
import ApplicationServices import ApplicationServices
public enum DictationMode: String, CaseIterable { public enum DictationMode: String, CaseIterable {
case toggle = "toggle" // Press once to start, press again to stop
case pushToTalk = "pushToTalk" // Hold to record, release to transcribe case pushToTalk = "pushToTalk" // Hold to record, release to transcribe
case toggle = "toggle" // Press once to start, press again to stop
public var localizedTitle: String { public var localizedTitle: String {
switch self { switch self {
case .toggle:
return "فشردن برای شروع / توقف (Toggle)"
case .pushToTalk: case .pushToTalk:
return "نگه‌داشتن برای صحبت (Push to Talk)"
return "Push to Talk (Hold Key)"
case .toggle:
return "Toggle (Press to Start / Stop)"
} }
} }
} }
public enum HotkeyPreset: String, CaseIterable { public enum HotkeyPreset: String, CaseIterable {
case option = "Option (⌥ نگه‌داشتن)"
case option = "Option (Hold ⌥)"
case doubleTapFn = "Double-Tap Fn (Globe 🌐)"
case holdFn = "Hold Fn (Globe 🌐)"
case f5 = "F5 (Fn + F5)" case f5 = "F5 (Fn + F5)"
case controlSpace = "Control + Space" case controlSpace = "Control + Space"
case capsLock = "Caps Lock" case capsLock = "Caps Lock"
@ -31,6 +33,8 @@ public enum HotkeyPreset: String, CaseIterable {
return UInt32(kVK_Space) // 49 return UInt32(kVK_Space) // 49
case .option: case .option:
return UInt32(kVK_Option) // 58 return UInt32(kVK_Option) // 58
case .doubleTapFn, .holdFn:
return UInt32(kVK_Function) // 63
case .capsLock: case .capsLock:
return UInt32(kVK_CapsLock) // 57 return UInt32(kVK_CapsLock) // 57
case .f8: case .f8:
@ -48,7 +52,7 @@ public enum HotkeyPreset: String, CaseIterable {
return UInt32(optionKey) return UInt32(optionKey)
case .cmdShiftSpace: case .cmdShiftSpace:
return UInt32(cmdKey | shiftKey) return UInt32(cmdKey | shiftKey)
case .option, .capsLock, .f8, .f5:
case .option, .doubleTapFn, .holdFn, .capsLock, .f8, .f5:
return 0 return 0
} }
} }
@ -69,6 +73,8 @@ public final class HotkeyManager {
private var isOptionPhysicallyDown = false private var isOptionPhysicallyDown = false
private var isCapsLockPhysicallyDown = false private var isCapsLockPhysicallyDown = false
private var isF5PhysicallyDown = false private var isF5PhysicallyDown = false
private var isFnPhysicallyDown = false
private var lastFnPressTime: TimeInterval = 0
private var lastTriggerTime: TimeInterval = 0 private var lastTriggerTime: TimeInterval = 0
public var currentPreset: HotkeyPreset { public var currentPreset: HotkeyPreset {
@ -101,8 +107,8 @@ public final class HotkeyManager {
let preset = currentPreset let preset = currentPreset
print("HotkeyManager: Registering hotkey for '\(preset.rawValue)', mode: '\(currentMode.rawValue)'") print("HotkeyManager: Registering hotkey for '\(preset.rawValue)', mode: '\(currentMode.rawValue)'")
// 1. Carbon HotKey for multi-key combos (Control+Space, etc.)
if preset != .option && preset != .capsLock && preset != .f5 {
// 1. Carbon HotKey for multi-key combos (Control+Space, Option+Space, Cmd+Shift+Space, F8)
if preset != .option && preset != .capsLock && preset != .f5 && preset != .doubleTapFn && preset != .holdFn {
var eventTypes = [ var eventTypes = [
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)),
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased))
@ -139,7 +145,7 @@ public final class HotkeyManager {
) )
} }
// 2. Global Event Tap for high-priority interception (Swallow Control+Space & modifier handling)
// 2. Global Event Tap for high-priority interception (Option, CapsLock, F5, Fn/Globe)
setupEventTap() setupEventTap()
// 3. Fallback NSEvent monitor // 3. Fallback NSEvent monitor
@ -148,7 +154,7 @@ public final class HotkeyManager {
private func triggerAction() { private func triggerAction() {
let now = Date().timeIntervalSince1970 let now = Date().timeIntervalSince1970
guard (now - lastTriggerTime) > 0.35 else { return }
guard (now - lastTriggerTime) > 0.30 else { return }
lastTriggerTime = now lastTriggerTime = now
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.onHotkeyPressed?() self?.onHotkeyPressed?()
@ -166,20 +172,20 @@ public final class HotkeyManager {
let flags = event.flags.rawValue let flags = event.flags.rawValue
let keyCode = event.getIntegerValueField(.keyboardEventKeycode) let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
// 1. Control + Space (Swallow keystroke and toggle speech)
// 1. Control + Space
if manager.currentPreset == .controlSpace { if manager.currentPreset == .controlSpace {
if keyCode == 49 && type == .keyDown { // 49 = Space if keyCode == 49 && type == .keyDown { // 49 = Space
let isCtrl = (flags & CGEventFlags.maskControl.rawValue) != 0 let isCtrl = (flags & CGEventFlags.maskControl.rawValue) != 0
if isCtrl { if isCtrl {
manager.triggerAction() manager.triggerAction()
return nil // Swallow event so space is not typed in the active app
return nil // Swallow event
} }
} }
} }
// 2. Option Key // 2. Option Key
else if manager.currentPreset == .option { else if manager.currentPreset == .option {
if keyCode == 58 || keyCode == 61 {
if keyCode == 58 || keyCode == 61 { // 58 = Left Option, 61 = Right Option
let isAlt = (flags & CGEventFlags.maskAlternate.rawValue) != 0 let isAlt = (flags & CGEventFlags.maskAlternate.rawValue) != 0
if isAlt != manager.isOptionPhysicallyDown { if isAlt != manager.isOptionPhysicallyDown {
manager.isOptionPhysicallyDown = isAlt manager.isOptionPhysicallyDown = isAlt
@ -196,7 +202,7 @@ public final class HotkeyManager {
// 3. CapsLock Key // 3. CapsLock Key
else if manager.currentPreset == .capsLock { else if manager.currentPreset == .capsLock {
if keyCode == 57 {
if keyCode == 57 { // 57 = CapsLock
if !manager.isCapsLockPhysicallyDown { if !manager.isCapsLockPhysicallyDown {
manager.isCapsLockPhysicallyDown = true manager.isCapsLockPhysicallyDown = true
manager.triggerAction() manager.triggerAction()
@ -214,7 +220,7 @@ public final class HotkeyManager {
// 4. F5 Key (Fn + F5) // 4. F5 Key (Fn + F5)
else if manager.currentPreset == .f5 { else if manager.currentPreset == .f5 {
if keyCode == 96 {
if keyCode == 96 { // 96 = kVK_F5
if type == .keyDown { if type == .keyDown {
if !manager.isF5PhysicallyDown { if !manager.isF5PhysicallyDown {
manager.isF5PhysicallyDown = true manager.isF5PhysicallyDown = true
@ -233,6 +239,40 @@ public final class HotkeyManager {
} }
} }
// 5. Fn / Globe Key (Double-Tap or Hold)
else if manager.currentPreset == .doubleTapFn || manager.currentPreset == .holdFn {
if keyCode == 63 { // 63 = kVK_Function (Fn / Globe)
let isFnActive = (flags & CGEventFlags.maskSecondaryFn.rawValue) != 0
if isFnActive != manager.isFnPhysicallyDown {
manager.isFnPhysicallyDown = isFnActive
if isFnActive {
let now = Date().timeIntervalSince1970
let interval = now - manager.lastFnPressTime
manager.lastFnPressTime = now
if manager.currentPreset == .doubleTapFn {
if interval > 0.05 && interval < 0.45 {
manager.lastFnPressTime = 0
manager.triggerAction()
return nil
}
} else if manager.currentPreset == .holdFn {
manager.triggerAction()
return nil
}
} else {
if manager.currentPreset == .holdFn && manager.currentMode == .pushToTalk {
DispatchQueue.main.async {
manager.onHotkeyReleased?()
}
return nil
}
}
}
}
}
return Unmanaged.passRetained(event) return Unmanaged.passRetained(event)
} }

14
src/SonioxClient.swift

@ -298,10 +298,19 @@ public final class SonioxSessionPool {
return return
} }
let session = SonioxLiveSession()
let hints = SonioxSettings.shared.language.hints
let session = SonioxLiveSession(languageHints: hints)
self.prewarmedSession = session self.prewarmedSession = session
} }
public func reconnectAll() {
lock.lock()
prewarmedSession?.cancel()
prewarmedSession = nil
lock.unlock()
prewarmNextSession()
}
public func acquireSession() -> SonioxLiveSession { public func acquireSession() -> SonioxLiveSession {
lock.lock() lock.lock()
let session = prewarmedSession let session = prewarmedSession
@ -316,6 +325,7 @@ public final class SonioxSessionPool {
return session return session
} }
return SonioxLiveSession()
let hints = SonioxSettings.shared.language.hints
return SonioxLiveSession(languageHints: hints)
} }
} }

135
src/SonioxSettings.swift

@ -0,0 +1,135 @@
import Foundation
import Cocoa
public enum RecognitionLanguage: String, CaseIterable {
case multi = "multi"
case persian = "fa"
case english = "en"
case persianEnglish = "fa_en"
public var displayName: String {
switch self {
case .multi:
return "Multilingual (Persian, English, Arabic)"
case .persian:
return "Persian Only (فارسی)"
case .english:
return "English Only"
case .persianEnglish:
return "Persian + English"
}
}
public var hints: [String] {
switch self {
case .multi:
return ["fa", "en", "ar"]
case .persian:
return ["fa"]
case .english:
return ["en"]
case .persianEnglish:
return ["fa", "en"]
}
}
}
public final class SonioxSettings {
public static let shared = SonioxSettings()
private let defaults = UserDefaults.standard
public var language: RecognitionLanguage {
get {
let val = defaults.string(forKey: "SonioxLanguage") ?? RecognitionLanguage.multi.rawValue
return RecognitionLanguage(rawValue: val) ?? .multi
}
set {
defaults.set(newValue.rawValue, forKey: "SonioxLanguage")
SonioxSessionPool.shared.reconnectAll()
}
}
public var addTrailingSpace: Bool {
get {
if defaults.object(forKey: "SonioxAddTrailingSpace") == nil { return true }
return defaults.bool(forKey: "SonioxAddTrailingSpace")
}
set {
defaults.set(newValue, forKey: "SonioxAddTrailingSpace")
}
}
public var normalizeText: Bool {
get {
if defaults.object(forKey: "SonioxNormalizeText") == nil { return true }
return defaults.bool(forKey: "SonioxNormalizeText")
}
set {
defaults.set(newValue, forKey: "SonioxNormalizeText")
}
}
public var playSounds: Bool {
get {
if defaults.object(forKey: "SonioxPlaySounds") == nil { return true }
return defaults.bool(forKey: "SonioxPlaySounds")
}
set {
defaults.set(newValue, forKey: "SonioxPlaySounds")
}
}
public var launchAtLogin: Bool {
get {
return defaults.bool(forKey: "SonioxLaunchAtLogin")
}
set {
defaults.set(newValue, forKey: "SonioxLaunchAtLogin")
updateLaunchAtLogin(enabled: newValue)
}
}
public func resetToDefaults() {
defaults.removeObject(forKey: "SonioxHotkeyPreset")
defaults.removeObject(forKey: "SonioxDictationMode")
defaults.removeObject(forKey: "SonioxLanguage")
defaults.removeObject(forKey: "SonioxAddTrailingSpace")
defaults.removeObject(forKey: "SonioxNormalizeText")
defaults.removeObject(forKey: "SonioxPlaySounds")
defaults.removeObject(forKey: "SonioxLaunchAtLogin")
updateLaunchAtLogin(enabled: false)
HotkeyManager.shared.registerHotkeys()
SonioxSessionPool.shared.reconnectAll()
}
private func updateLaunchAtLogin(enabled: Bool) {
let launchAgentDir = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/LaunchAgents")
let plistURL = launchAgentDir.appendingPathComponent("com.soniox.voice.plist")
if enabled {
try? FileManager.default.createDirectory(at: launchAgentDir, withIntermediateDirectories: true)
let plistContent = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.soniox.voice</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/Soniox Voice.app/Contents/MacOS/SonioxVoice</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>
"""
try? plistContent.write(to: plistURL, atomically: true, encoding: .utf8)
} else {
try? FileManager.default.removeItem(at: plistURL)
}
}
}

122
src/StatusBarController.swift

@ -25,25 +25,31 @@ public final class StatusBarController {
switch state { switch state {
case .idle: case .idle:
if let image = NSImage(systemSymbolName: "mic", accessibilityDescription: "Soniox Voice") {
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .regular)
if let image = NSImage(systemSymbolName: "mic", accessibilityDescription: "Soniox Voice")?.withSymbolConfiguration(config) {
image.isTemplate = true image.isTemplate = true
button.image = image button.image = image
} }
button.toolTip = "Soniox Voice (آماده)"
button.contentTintColor = nil
button.toolTip = "Soniox Voice (Ready)"
case .recording: case .recording:
if let image = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording") {
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
if let image = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording")?.withSymbolConfiguration(config) {
image.isTemplate = false image.isTemplate = false
button.image = image button.image = image
button.contentTintColor = NSColor.systemRed button.contentTintColor = NSColor.systemRed
} }
button.toolTip = "در حال ضبط صدا..."
button.toolTip = "Recording speech..."
case .transcribing: case .transcribing:
if let image = NSImage(systemSymbolName: "waveform", accessibilityDescription: "Transcribing") {
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
if let image = NSImage(systemSymbolName: "waveform", accessibilityDescription: "Transcribing")?.withSymbolConfiguration(config) {
image.isTemplate = false image.isTemplate = false
button.image = image button.image = image
button.contentTintColor = NSColor.systemOrange button.contentTintColor = NSColor.systemOrange
} }
button.toolTip = "در حال تبدیل به متن..."
button.toolTip = "Transcribing with Soniox..."
} }
} }
@ -51,15 +57,15 @@ public final class StatusBarController {
let menu = NSMenu() let menu = NSMenu()
menu.autoenablesItems = false menu.autoenablesItems = false
// 1. Record Action
let recordTitle = isRecording ? "⏹️ توقف ضبط و درج متن" : "🎙️ شروع ضبط صدا (\(HotkeyManager.shared.currentPreset.rawValue))"
// 1. Primary Action: Start / Stop Recording
let recordTitle = isRecording ? "⏹️ Stop Recording & Insert Text" : "🎙️ Start Recording (\(HotkeyManager.shared.currentPreset.rawValue))"
let recordItem = NSMenuItem(title: recordTitle, action: #selector(toggleRecordAction), keyEquivalent: "") let recordItem = NSMenuItem(title: recordTitle, action: #selector(toggleRecordAction), keyEquivalent: "")
recordItem.target = self recordItem.target = self
menu.addItem(recordItem) menu.addItem(recordItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// 2. Mode Submenu
// 2. Dictation Mode Submenu
let modeMenu = NSMenu() let modeMenu = NSMenu()
for mode in DictationMode.allCases { for mode in DictationMode.allCases {
let item = NSMenuItem(title: mode.localizedTitle, action: #selector(selectModeAction(_:)), keyEquivalent: "") let item = NSMenuItem(title: mode.localizedTitle, action: #selector(selectModeAction(_:)), keyEquivalent: "")
@ -68,11 +74,11 @@ public final class StatusBarController {
item.state = (HotkeyManager.shared.currentMode == mode) ? .on : .off item.state = (HotkeyManager.shared.currentMode == mode) ? .on : .off
modeMenu.addItem(item) modeMenu.addItem(item)
} }
let modeMenuItem = NSMenuItem(title: "⚙️ حالت کارکرد", action: nil, keyEquivalent: "")
let modeMenuItem = NSMenuItem(title: "⚙️ Dictation Mode", action: nil, keyEquivalent: "")
modeMenuItem.submenu = modeMenu modeMenuItem.submenu = modeMenu
menu.addItem(modeMenuItem) menu.addItem(modeMenuItem)
// 3. Hotkey Submenu
// 3. Hotkey Preset Submenu
let hotkeyMenu = NSMenu() let hotkeyMenu = NSMenu()
for preset in HotkeyPreset.allCases { for preset in HotkeyPreset.allCases {
let item = NSMenuItem(title: preset.rawValue, action: #selector(selectHotkeyAction(_:)), keyEquivalent: "") let item = NSMenuItem(title: preset.rawValue, action: #selector(selectHotkeyAction(_:)), keyEquivalent: "")
@ -81,41 +87,73 @@ public final class StatusBarController {
item.state = (HotkeyManager.shared.currentPreset == preset) ? .on : .off item.state = (HotkeyManager.shared.currentPreset == preset) ? .on : .off
hotkeyMenu.addItem(item) hotkeyMenu.addItem(item)
} }
let hotkeyMenuItem = NSMenuItem(title: "⌨️ کلید میانبر (Hotkey)", action: nil, keyEquivalent: "")
let hotkeyMenuItem = NSMenuItem(title: "⌨️ Shortcut Key", action: nil, keyEquivalent: "")
hotkeyMenuItem.submenu = hotkeyMenu hotkeyMenuItem.submenu = hotkeyMenu
menu.addItem(hotkeyMenuItem) menu.addItem(hotkeyMenuItem)
// 4. Recognition Language Submenu
let langMenu = NSMenu()
for lang in RecognitionLanguage.allCases {
let item = NSMenuItem(title: lang.displayName, action: #selector(selectLanguageAction(_:)), keyEquivalent: "")
item.target = self
item.representedObject = lang
item.state = (SonioxSettings.shared.language == lang) ? .on : .off
langMenu.addItem(item)
}
let langMenuItem = NSMenuItem(title: "🌐 Recognition Language", action: nil, keyEquivalent: "")
langMenuItem.submenu = langMenu
menu.addItem(langMenuItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// 4. Sound Effects Toggle
let soundsEnabled = UserDefaults.standard.bool(forKey: "SonioxPlaySounds")
let soundItem = NSMenuItem(title: "🔊 پخش افکت صوتی", action: #selector(toggleSoundsAction(_:)), keyEquivalent: "")
// 5. Advanced Insertion Settings Submenu
let advancedMenu = NSMenu()
let trailingSpaceItem = NSMenuItem(title: "␣ Add Trailing Space", action: #selector(toggleTrailingSpaceAction(_:)), keyEquivalent: "")
trailingSpaceItem.target = self
trailingSpaceItem.state = SonioxSettings.shared.addTrailingSpace ? .on : .off
advancedMenu.addItem(trailingSpaceItem)
let normalizeItem = NSMenuItem(title: "✨ Persian Text Normalization", action: #selector(toggleNormalizeAction(_:)), keyEquivalent: "")
normalizeItem.target = self
normalizeItem.state = SonioxSettings.shared.normalizeText ? .on : .off
advancedMenu.addItem(normalizeItem)
let soundItem = NSMenuItem(title: "🔊 Sound Feedback", action: #selector(toggleSoundsAction(_:)), keyEquivalent: "")
soundItem.target = self soundItem.target = self
soundItem.state = soundsEnabled ? .on : .off
menu.addItem(soundItem)
soundItem.state = SonioxSettings.shared.playSounds ? .on : .off
advancedMenu.addItem(soundItem)
// 5. Accessibility Permission Check
let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "")
advancedMenuItem.submenu = advancedMenu
menu.addItem(advancedMenuItem)
// 6. System Permissions & Launch at Login
let isAxTrusted = AXIsProcessTrusted() let isAxTrusted = AXIsProcessTrusted()
let axTitle = isAxTrusted ? "✅ دسترسی Accessibility فعال است" : "🔑 اعطای دسترسی Accessibility..."
let axTitle = isAxTrusted ? "Accessibility Granted" : "🔑 Grant Accessibility Access..."
let axItem = NSMenuItem(title: axTitle, action: #selector(openAccessibilitySettings), keyEquivalent: "") let axItem = NSMenuItem(title: axTitle, action: #selector(openAccessibilitySettings), keyEquivalent: "")
axItem.target = self axItem.target = self
menu.addItem(axItem) menu.addItem(axItem)
// 6. Launch at Login
let launchLogin = UserDefaults.standard.bool(forKey: "SonioxLaunchAtLogin")
let launchItem = NSMenuItem(title: "🚀 اجرا هنگام بالا آمدن سیستم", action: #selector(toggleLaunchAtLoginAction(_:)), keyEquivalent: "")
let launchLogin = SonioxSettings.shared.launchAtLogin
let launchItem = NSMenuItem(title: "🚀 Launch at Login", action: #selector(toggleLaunchAtLoginAction(_:)), keyEquivalent: "")
launchItem.target = self launchItem.target = self
launchItem.state = launchLogin ? .on : .off launchItem.state = launchLogin ? .on : .off
menu.addItem(launchItem) menu.addItem(launchItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// 7. About & Quit
let aboutItem = NSMenuItem(title: "ℹ️ درباره Soniox Voice", action: #selector(aboutAction), keyEquivalent: "")
// 7. Reset to Defaults
let resetItem = NSMenuItem(title: "🔄 Reset to Recommended Defaults", action: #selector(resetDefaultsAction), keyEquivalent: "")
resetItem.target = self
menu.addItem(resetItem)
// 8. About & Quit
let aboutItem = NSMenuItem(title: "ℹ️ About Soniox Voice", action: #selector(aboutAction), keyEquivalent: "")
aboutItem.target = self aboutItem.target = self
menu.addItem(aboutItem) menu.addItem(aboutItem)
let quitItem = NSMenuItem(title: "❌ خروج", action: #selector(quitAction), keyEquivalent: "q")
let quitItem = NSMenuItem(title: "Quit", action: #selector(quitAction), keyEquivalent: "q")
quitItem.target = self quitItem.target = self
menu.addItem(quitItem) menu.addItem(quitItem)
@ -140,15 +178,35 @@ public final class StatusBarController {
} }
} }
@objc private func selectLanguageAction(_ sender: NSMenuItem) {
if let lang = sender.representedObject as? RecognitionLanguage {
SonioxSettings.shared.language = lang
buildMenu()
}
}
@objc private func toggleTrailingSpaceAction(_ sender: NSMenuItem) {
SonioxSettings.shared.addTrailingSpace.toggle()
buildMenu()
}
@objc private func toggleNormalizeAction(_ sender: NSMenuItem) {
SonioxSettings.shared.normalizeText.toggle()
buildMenu()
}
@objc private func toggleSoundsAction(_ sender: NSMenuItem) { @objc private func toggleSoundsAction(_ sender: NSMenuItem) {
let current = UserDefaults.standard.bool(forKey: "SonioxPlaySounds")
UserDefaults.standard.set(!current, forKey: "SonioxPlaySounds")
SonioxSettings.shared.playSounds.toggle()
buildMenu() buildMenu()
} }
@objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) { @objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) {
let current = UserDefaults.standard.bool(forKey: "SonioxLaunchAtLogin")
UserDefaults.standard.set(!current, forKey: "SonioxLaunchAtLogin")
SonioxSettings.shared.launchAtLogin.toggle()
buildMenu()
}
@objc private func resetDefaultsAction() {
SonioxSettings.shared.resetToDefaults()
buildMenu() buildMenu()
} }
@ -159,10 +217,10 @@ public final class StatusBarController {
@objc private func aboutAction() { @objc private func aboutAction() {
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Soniox Voice v1.0"
alert.informativeText = "تبدیل بلادرنگ گفتار به متن فارسی و انگلیسی با موتور ابری فوق سریع Soniox.\n\nتوسعه یافته برای macOS."
alert.messageText = "Soniox Voice for macOS"
alert.informativeText = "High-accuracy, real-time speech-to-text dictation powered by Soniox AI.\n\nOptimized for macOS Apple Silicon."
alert.alertStyle = .informational alert.alertStyle = .informational
alert.addButton(withTitle: "باشه")
alert.addButton(withTitle: "OK")
alert.runModal() alert.runModal()
} }

Loading…
Cancel
Save