|
|
@@ -40,6 +40,11 @@ private enum SettingsAction: Int {
|
|
40
|
40
|
case termsOfServices = 7
|
|
41
|
41
|
}
|
|
42
|
42
|
|
|
|
43
|
+private struct SpeechLocaleOption {
|
|
|
44
|
+ let identifier: String
|
|
|
45
|
+ let displayName: String
|
|
|
46
|
+}
|
|
|
47
|
+
|
|
43
|
48
|
private enum PremiumPlan: Int {
|
|
44
|
49
|
case weekly = 0
|
|
45
|
50
|
case monthly = 1
|
|
|
@@ -471,6 +476,8 @@ final class ViewController: NSViewController {
|
|
471
|
476
|
private weak var schedulePageRangeErrorLabel: NSTextField?
|
|
472
|
477
|
private weak var schedulePageCardsStack: NSStackView?
|
|
473
|
478
|
private weak var schedulePageCardsScrollView: NSScrollView?
|
|
|
479
|
+ private weak var settingsPagePrimaryLanguagePopup: NSPopUpButton?
|
|
|
480
|
+ private weak var settingsPageSecondaryLanguagePopup: NSPopUpButton?
|
|
474
|
481
|
|
|
475
|
482
|
// MARK: - Calendar page (custom month UI)
|
|
476
|
483
|
private var calendarPageMonthAnchor: Date = Calendar.current.startOfDay(for: Date())
|
|
|
@@ -493,6 +500,8 @@ final class ViewController: NSViewController {
|
|
493
|
500
|
private let ratingStateMigrationV2DoneDefaultsKey = "rating.stateMigrationV2Done"
|
|
494
|
501
|
private let nonPremiumJoinTrialConsumedDefaultsKey = "join.nonPremiumTrialConsumed"
|
|
495
|
502
|
private let aiCompanionLocalRecordingsDefaultsKey = "aiCompanion.localRecordings"
|
|
|
503
|
+ private let aiCompanionPreferredLanguage1DefaultsKey = "aiCompanion.preferredLanguage1"
|
|
|
504
|
+ private let aiCompanionPreferredLanguage2DefaultsKey = "aiCompanion.preferredLanguage2"
|
|
496
|
505
|
private let ratingEligibleUsageSeconds: TimeInterval = 30 * 60
|
|
497
|
506
|
private let meetingTranscriptionService = MeetingTranscriptionService()
|
|
498
|
507
|
private let meetingNotesService = MeetingNotesService()
|
|
|
@@ -521,15 +530,22 @@ final class ViewController: NSViewController {
|
|
521
|
530
|
popover.animates = true
|
|
522
|
531
|
let showUpgradeInSettings = storeKitCoordinator.hasPremiumAccess && !storeKitCoordinator.hasLifetimeAccess
|
|
523
|
532
|
let showRateUsInSettings = shouldShowRateUsInSettings
|
|
|
533
|
+ let speechOptions = aiCompanionSupportedSpeechLocaleOptions()
|
|
524
|
534
|
popover.contentViewController = SettingsMenuViewController(
|
|
525
|
535
|
palette: palette,
|
|
526
|
536
|
typography: typography,
|
|
527
|
537
|
darkModeEnabled: darkModeEnabled,
|
|
528
|
538
|
showRateUsInSettings: showRateUsInSettings,
|
|
529
|
539
|
showUpgradeInSettings: showUpgradeInSettings,
|
|
|
540
|
+ speechLocaleOptions: speechOptions,
|
|
|
541
|
+ selectedPrimaryLanguageIdentifier: UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage1DefaultsKey),
|
|
|
542
|
+ selectedSecondaryLanguageIdentifier: UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage2DefaultsKey),
|
|
530
|
543
|
onToggleDarkMode: { [weak self] enabled in
|
|
531
|
544
|
self?.setDarkMode(enabled)
|
|
532
|
545
|
},
|
|
|
546
|
+ onUpdatePreferredSpeechLanguages: { [weak self] primary, secondary in
|
|
|
547
|
+ self?.updateAiCompanionPreferredSpeechLanguages(primary: primary, secondary: secondary)
|
|
|
548
|
+ },
|
|
533
|
549
|
onAction: { [weak self] action, sourceView, clickPoint in
|
|
534
|
550
|
self?.handleSettingsAction(action, sourceView: sourceView, clickLocationInSourceView: clickPoint)
|
|
535
|
551
|
}
|
|
|
@@ -3150,6 +3166,7 @@ private extension ViewController {
|
|
3150
|
3166
|
}
|
|
3151
|
3167
|
}
|
|
3152
|
3168
|
|
|
|
3169
|
+ let preferredLocales = aiCompanionPreferredTranscriptionLocales()
|
|
3153
|
3170
|
let progressHandler: @Sendable (MeetingTranscriptionProgress) -> Void = { [weak self] progress in
|
|
3154
|
3171
|
Task { @MainActor [weak self] in
|
|
3155
|
3172
|
guard let self else { return }
|
|
|
@@ -3170,6 +3187,7 @@ private extension ViewController {
|
|
3170
|
3187
|
segments = try await meetingTranscriptionService.transcribeMeeting(
|
|
3171
|
3188
|
micURL: micURL,
|
|
3172
|
3189
|
systemURL: systemURL,
|
|
|
3190
|
+ locales: preferredLocales,
|
|
3173
|
3191
|
onProgress: progressHandler
|
|
3174
|
3192
|
)
|
|
3175
|
3193
|
source = .localMultiChannelAppleSpeech
|
|
|
@@ -3178,6 +3196,7 @@ private extension ViewController {
|
|
3178
|
3196
|
segments = try await meetingTranscriptionService.transcribeMeeting(
|
|
3179
|
3197
|
micURL: nil,
|
|
3180
|
3198
|
systemURL: mixedURL,
|
|
|
3199
|
+ locales: preferredLocales,
|
|
3181
|
3200
|
onProgress: progressHandler
|
|
3182
|
3201
|
)
|
|
3183
|
3202
|
source = .localAudioAppleSpeech
|
|
|
@@ -3194,6 +3213,83 @@ private extension ViewController {
|
|
3194
|
3213
|
return (renderedText, segmentsJSON, source)
|
|
3195
|
3214
|
}
|
|
3196
|
3215
|
|
|
|
3216
|
+ private func aiCompanionPreferredTranscriptionLocales() -> [Locale] {
|
|
|
3217
|
+ let defaults = UserDefaults.standard
|
|
|
3218
|
+ let selectedIdentifiers = [
|
|
|
3219
|
+ defaults.string(forKey: aiCompanionPreferredLanguage1DefaultsKey),
|
|
|
3220
|
+ defaults.string(forKey: aiCompanionPreferredLanguage2DefaultsKey)
|
|
|
3221
|
+ ]
|
|
|
3222
|
+ .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
|
3223
|
+ .filter { $0.isEmpty == false }
|
|
|
3224
|
+
|
|
|
3225
|
+ var locales: [Locale] = []
|
|
|
3226
|
+ var seen = Set<String>()
|
|
|
3227
|
+
|
|
|
3228
|
+ func appendIdentifier(_ identifier: String) {
|
|
|
3229
|
+ let locale = Locale(identifier: identifier)
|
|
|
3230
|
+ guard SFSpeechRecognizer(locale: locale)?.isAvailable == true else { return }
|
|
|
3231
|
+ let normalized = identifier.replacingOccurrences(of: "_", with: "-").lowercased()
|
|
|
3232
|
+ guard seen.contains(normalized) == false else { return }
|
|
|
3233
|
+ seen.insert(normalized)
|
|
|
3234
|
+ locales.append(locale)
|
|
|
3235
|
+ }
|
|
|
3236
|
+
|
|
|
3237
|
+ for identifier in selectedIdentifiers {
|
|
|
3238
|
+ appendIdentifier(identifier)
|
|
|
3239
|
+ }
|
|
|
3240
|
+
|
|
|
3241
|
+ // Safe defaults for mixed-language meetings if user has not configured preferences yet.
|
|
|
3242
|
+ appendIdentifier(Locale.current.identifier)
|
|
|
3243
|
+ appendIdentifier("en-US")
|
|
|
3244
|
+
|
|
|
3245
|
+ if locales.isEmpty {
|
|
|
3246
|
+ return [Locale(identifier: "en-US")]
|
|
|
3247
|
+ }
|
|
|
3248
|
+ return locales
|
|
|
3249
|
+ }
|
|
|
3250
|
+
|
|
|
3251
|
+ private func aiCompanionSupportedSpeechLocaleOptions() -> [SpeechLocaleOption] {
|
|
|
3252
|
+ let supported = SFSpeechRecognizer.supportedLocales()
|
|
|
3253
|
+ let currentIdentifier = Locale.current.identifier
|
|
|
3254
|
+ let englishUSIdentifier = "en-US"
|
|
|
3255
|
+ var candidates = Set<String>(supported.map { $0.identifier })
|
|
|
3256
|
+ for preferred in Locale.preferredLanguages {
|
|
|
3257
|
+ candidates.insert(preferred)
|
|
|
3258
|
+ }
|
|
|
3259
|
+ candidates.insert(currentIdentifier)
|
|
|
3260
|
+ candidates.insert(englishUSIdentifier)
|
|
|
3261
|
+
|
|
|
3262
|
+ return candidates
|
|
|
3263
|
+ .map { identifier -> SpeechLocaleOption in
|
|
|
3264
|
+ let locale = Locale(identifier: identifier)
|
|
|
3265
|
+ let languageCode = locale.languageCode ?? Locale.components(fromIdentifier: identifier)[NSLocale.Key.languageCode.rawValue]
|
|
|
3266
|
+ let languageName = Locale.current.localizedString(forIdentifier: identifier)
|
|
|
3267
|
+ ?? languageCode.flatMap { Locale.current.localizedString(forLanguageCode: $0) }
|
|
|
3268
|
+ ?? identifier
|
|
|
3269
|
+ return SpeechLocaleOption(identifier: identifier, displayName: "\(languageName) (\(identifier))")
|
|
|
3270
|
+ }
|
|
|
3271
|
+ .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
|
|
|
3272
|
+ }
|
|
|
3273
|
+
|
|
|
3274
|
+ private func updateAiCompanionPreferredSpeechLanguages(primary: String, secondary: String?) {
|
|
|
3275
|
+ let normalizedPrimary = primary.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
3276
|
+ guard normalizedPrimary.isEmpty == false else { return }
|
|
|
3277
|
+ UserDefaults.standard.set(normalizedPrimary, forKey: aiCompanionPreferredLanguage1DefaultsKey)
|
|
|
3278
|
+
|
|
|
3279
|
+ let cleanedSecondary = secondary?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
3280
|
+ if let cleanedSecondary, cleanedSecondary.isEmpty == false {
|
|
|
3281
|
+ let normalizedPrimaryKey = normalizedPrimary.replacingOccurrences(of: "_", with: "-").lowercased()
|
|
|
3282
|
+ let normalizedSecondaryKey = cleanedSecondary.replacingOccurrences(of: "_", with: "-").lowercased()
|
|
|
3283
|
+ if normalizedPrimaryKey == normalizedSecondaryKey {
|
|
|
3284
|
+ UserDefaults.standard.removeObject(forKey: aiCompanionPreferredLanguage2DefaultsKey)
|
|
|
3285
|
+ } else {
|
|
|
3286
|
+ UserDefaults.standard.set(cleanedSecondary, forKey: aiCompanionPreferredLanguage2DefaultsKey)
|
|
|
3287
|
+ }
|
|
|
3288
|
+ } else {
|
|
|
3289
|
+ UserDefaults.standard.removeObject(forKey: aiCompanionPreferredLanguage2DefaultsKey)
|
|
|
3290
|
+ }
|
|
|
3291
|
+ }
|
|
|
3292
|
+
|
|
3197
|
3293
|
@objc private func aiCompanionNotesTapped(_ sender: NSButton) {
|
|
3198
|
3294
|
let senderId = ObjectIdentifier(sender)
|
|
3199
|
3295
|
guard let meetingId = aiCompanionNotesMeetingIdByView[senderId] else { return }
|
|
|
@@ -3678,6 +3774,16 @@ private extension ViewController {
|
|
3678
|
3774
|
darkModeRow.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
3679
|
3775
|
stack.setCustomSpacing(24, after: darkModeRow)
|
|
3680
|
3776
|
|
|
|
3777
|
+ let aiCompanionTitle = textLabel("AI Companion", font: typography.joinWithURLTitle, color: palette.textPrimary)
|
|
|
3778
|
+ stack.addArrangedSubview(aiCompanionTitle)
|
|
|
3779
|
+ let language1Row = makeSettingsSpeechLanguageRow(title: "Preferred Language 1", isPrimary: true)
|
|
|
3780
|
+ stack.addArrangedSubview(language1Row)
|
|
|
3781
|
+ language1Row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
3782
|
+ let language2Row = makeSettingsSpeechLanguageRow(title: "Preferred Language 2", isPrimary: false)
|
|
|
3783
|
+ stack.addArrangedSubview(language2Row)
|
|
|
3784
|
+ language2Row.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
|
3785
|
+ stack.setCustomSpacing(24, after: language2Row)
|
|
|
3786
|
+
|
|
3681
|
3787
|
let accountTitle = textLabel("Account", font: typography.joinWithURLTitle, color: palette.textPrimary)
|
|
3682
|
3788
|
stack.addArrangedSubview(accountTitle)
|
|
3683
|
3789
|
let googleAccountRow = makeSettingsGoogleAccountRow()
|
|
|
@@ -3782,6 +3888,83 @@ private extension ViewController {
|
|
3782
|
3888
|
return row
|
|
3783
|
3889
|
}
|
|
3784
|
3890
|
|
|
|
3891
|
+ private func makeSettingsSpeechLanguageRow(title: String, isPrimary: Bool) -> NSView {
|
|
|
3892
|
+ let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
|
|
|
3893
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3894
|
+ row.heightAnchor.constraint(equalToConstant: 72).isActive = true
|
|
|
3895
|
+ styleSurface(row, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
3896
|
+
|
|
|
3897
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
|
|
|
3898
|
+ let popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
3899
|
+ popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
3900
|
+ popup.target = self
|
|
|
3901
|
+ popup.action = #selector(settingsPageSpeechLanguageChanged(_:))
|
|
|
3902
|
+
|
|
|
3903
|
+ let options = aiCompanionSupportedSpeechLocaleOptions()
|
|
|
3904
|
+ if isPrimary == false {
|
|
|
3905
|
+ popup.addItem(withTitle: "None")
|
|
|
3906
|
+ popup.lastItem?.representedObject = ""
|
|
|
3907
|
+ }
|
|
|
3908
|
+ for option in options {
|
|
|
3909
|
+ popup.addItem(withTitle: option.displayName)
|
|
|
3910
|
+ popup.lastItem?.representedObject = option.identifier
|
|
|
3911
|
+ }
|
|
|
3912
|
+
|
|
|
3913
|
+ if isPrimary {
|
|
|
3914
|
+ let selected = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage1DefaultsKey)
|
|
|
3915
|
+ ?? options.first?.identifier
|
|
|
3916
|
+ ?? Locale.current.identifier
|
|
|
3917
|
+ selectSettingsPageLanguage(identifier: selected, in: popup)
|
|
|
3918
|
+ settingsPagePrimaryLanguagePopup = popup
|
|
|
3919
|
+ } else {
|
|
|
3920
|
+ if let selected = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage2DefaultsKey),
|
|
|
3921
|
+ selected.isEmpty == false {
|
|
|
3922
|
+ selectSettingsPageLanguage(identifier: selected, in: popup)
|
|
|
3923
|
+ } else {
|
|
|
3924
|
+ popup.selectItem(at: 0)
|
|
|
3925
|
+ }
|
|
|
3926
|
+ settingsPageSecondaryLanguagePopup = popup
|
|
|
3927
|
+ }
|
|
|
3928
|
+
|
|
|
3929
|
+ row.addSubview(titleLabel)
|
|
|
3930
|
+ row.addSubview(popup)
|
|
|
3931
|
+ NSLayoutConstraint.activate([
|
|
|
3932
|
+ titleLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
|
|
|
3933
|
+ titleLabel.topAnchor.constraint(equalTo: row.topAnchor, constant: 10),
|
|
|
3934
|
+ titleLabel.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
|
|
|
3935
|
+
|
|
|
3936
|
+ popup.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
|
|
|
3937
|
+ popup.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
|
|
|
3938
|
+ popup.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
|
|
|
3939
|
+ popup.heightAnchor.constraint(equalToConstant: 28)
|
|
|
3940
|
+ ])
|
|
|
3941
|
+ return row
|
|
|
3942
|
+ }
|
|
|
3943
|
+
|
|
|
3944
|
+ private func selectSettingsPageLanguage(identifier: String, in popup: NSPopUpButton) {
|
|
|
3945
|
+ for item in popup.itemArray {
|
|
|
3946
|
+ if let value = item.representedObject as? String, value == identifier {
|
|
|
3947
|
+ popup.select(item)
|
|
|
3948
|
+ return
|
|
|
3949
|
+ }
|
|
|
3950
|
+ }
|
|
|
3951
|
+ }
|
|
|
3952
|
+
|
|
|
3953
|
+ @objc private func settingsPageSpeechLanguageChanged(_ sender: NSPopUpButton) {
|
|
|
3954
|
+ guard let primary = settingsPagePrimaryLanguagePopup?.selectedItem?.representedObject as? String,
|
|
|
3955
|
+ primary.isEmpty == false else { return }
|
|
|
3956
|
+ var secondary: String? = settingsPageSecondaryLanguagePopup?.selectedItem?.representedObject as? String
|
|
|
3957
|
+ if secondary?.isEmpty == true {
|
|
|
3958
|
+ secondary = nil
|
|
|
3959
|
+ }
|
|
|
3960
|
+ if let secondaryValue = secondary,
|
|
|
3961
|
+ secondaryValue.replacingOccurrences(of: "_", with: "-").lowercased() == primary.replacingOccurrences(of: "_", with: "-").lowercased() {
|
|
|
3962
|
+ secondary = nil
|
|
|
3963
|
+ settingsPageSecondaryLanguagePopup?.selectItem(at: 0)
|
|
|
3964
|
+ }
|
|
|
3965
|
+ updateAiCompanionPreferredSpeechLanguages(primary: primary, secondary: secondary)
|
|
|
3966
|
+ }
|
|
|
3967
|
+
|
|
3785
|
3968
|
private func makeSettingsRemindersSection() -> NSView {
|
|
3786
|
3969
|
let container = NSStackView()
|
|
3787
|
3970
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -7011,10 +7194,16 @@ private final class GoogleAccountMenuViewController: NSViewController {
|
|
7011
|
7194
|
private final class SettingsMenuViewController: NSViewController {
|
|
7012
|
7195
|
private let palette: Palette
|
|
7013
|
7196
|
private let typography: Typography
|
|
|
7197
|
+ private let speechLocaleOptions: [SpeechLocaleOption]
|
|
|
7198
|
+ private let selectedPrimaryLanguageIdentifier: String?
|
|
|
7199
|
+ private let selectedSecondaryLanguageIdentifier: String?
|
|
7014
|
7200
|
private let onToggleDarkMode: (Bool) -> Void
|
|
|
7201
|
+ private let onUpdatePreferredSpeechLanguages: (String, String?) -> Void
|
|
7015
|
7202
|
private let onAction: (SettingsAction, NSView?, NSPoint?) -> Void
|
|
7016
|
7203
|
|
|
7017
|
7204
|
private var darkToggle: NSSwitch?
|
|
|
7205
|
+ private var primaryLanguagePopup: NSPopUpButton?
|
|
|
7206
|
+ private var secondaryLanguagePopup: NSPopUpButton?
|
|
7018
|
7207
|
|
|
7019
|
7208
|
init(
|
|
7020
|
7209
|
palette: Palette,
|
|
|
@@ -7022,12 +7211,20 @@ private final class SettingsMenuViewController: NSViewController {
|
|
7022
|
7211
|
darkModeEnabled: Bool,
|
|
7023
|
7212
|
showRateUsInSettings: Bool,
|
|
7024
|
7213
|
showUpgradeInSettings: Bool,
|
|
|
7214
|
+ speechLocaleOptions: [SpeechLocaleOption],
|
|
|
7215
|
+ selectedPrimaryLanguageIdentifier: String?,
|
|
|
7216
|
+ selectedSecondaryLanguageIdentifier: String?,
|
|
7025
|
7217
|
onToggleDarkMode: @escaping (Bool) -> Void,
|
|
|
7218
|
+ onUpdatePreferredSpeechLanguages: @escaping (String, String?) -> Void,
|
|
7026
|
7219
|
onAction: @escaping (SettingsAction, NSView?, NSPoint?) -> Void
|
|
7027
|
7220
|
) {
|
|
7028
|
7221
|
self.palette = palette
|
|
7029
|
7222
|
self.typography = typography
|
|
|
7223
|
+ self.speechLocaleOptions = speechLocaleOptions
|
|
|
7224
|
+ self.selectedPrimaryLanguageIdentifier = selectedPrimaryLanguageIdentifier
|
|
|
7225
|
+ self.selectedSecondaryLanguageIdentifier = selectedSecondaryLanguageIdentifier
|
|
7030
|
7226
|
self.onToggleDarkMode = onToggleDarkMode
|
|
|
7227
|
+ self.onUpdatePreferredSpeechLanguages = onUpdatePreferredSpeechLanguages
|
|
7031
|
7228
|
self.onAction = onAction
|
|
7032
|
7229
|
super.init(nibName: nil, bundle: nil)
|
|
7033
|
7230
|
self.view = makeView(
|
|
|
@@ -7061,7 +7258,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
7061
|
7258
|
card.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
|
7062
|
7259
|
card.topAnchor.constraint(equalTo: root.topAnchor),
|
|
7063
|
7260
|
card.bottomAnchor.constraint(equalTo: root.bottomAnchor),
|
|
7064
|
|
- root.widthAnchor.constraint(equalToConstant: 260)
|
|
|
7261
|
+ root.widthAnchor.constraint(equalToConstant: 320)
|
|
7065
|
7262
|
])
|
|
7066
|
7263
|
|
|
7067
|
7264
|
let stack = NSStackView()
|
|
|
@@ -7079,6 +7276,14 @@ private final class SettingsMenuViewController: NSViewController {
|
|
7079
|
7276
|
])
|
|
7080
|
7277
|
|
|
7081
|
7278
|
stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled))
|
|
|
7279
|
+ stack.addArrangedSubview(settingsSpeechLanguageRow(
|
|
|
7280
|
+ title: "AI Language 1",
|
|
|
7281
|
+ isPrimary: true
|
|
|
7282
|
+ ))
|
|
|
7283
|
+ stack.addArrangedSubview(settingsSpeechLanguageRow(
|
|
|
7284
|
+ title: "AI Language 2",
|
|
|
7285
|
+ isPrimary: false
|
|
|
7286
|
+ ))
|
|
7082
|
7287
|
if showRateUsInSettings {
|
|
7083
|
7288
|
stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
|
|
7084
|
7289
|
}
|
|
|
@@ -7193,6 +7398,90 @@ private final class SettingsMenuViewController: NSViewController {
|
|
7193
|
7398
|
return row
|
|
7194
|
7399
|
}
|
|
7195
|
7400
|
|
|
|
7401
|
+ private func settingsSpeechLanguageRow(title: String, isPrimary: Bool) -> NSView {
|
|
|
7402
|
+ let row = NSView()
|
|
|
7403
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
7404
|
+ row.heightAnchor.constraint(equalToConstant: 62).isActive = true
|
|
|
7405
|
+ row.wantsLayer = true
|
|
|
7406
|
+ row.layer?.cornerRadius = 10
|
|
|
7407
|
+ row.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
7408
|
+
|
|
|
7409
|
+ let titleLabel = NSTextField(labelWithString: title)
|
|
|
7410
|
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
7411
|
+ titleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
|
|
7412
|
+ titleLabel.textColor = palette.textPrimary
|
|
|
7413
|
+
|
|
|
7414
|
+ let popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
7415
|
+ popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
7416
|
+ popup.target = self
|
|
|
7417
|
+ popup.action = #selector(speechLanguageChanged(_:))
|
|
|
7418
|
+
|
|
|
7419
|
+ if isPrimary {
|
|
|
7420
|
+ for option in speechLocaleOptions {
|
|
|
7421
|
+ popup.addItem(withTitle: option.displayName)
|
|
|
7422
|
+ popup.lastItem?.representedObject = option.identifier
|
|
|
7423
|
+ }
|
|
|
7424
|
+ let preferred = selectedPrimaryLanguageIdentifier ?? speechLocaleOptions.first?.identifier
|
|
|
7425
|
+ if let preferred {
|
|
|
7426
|
+ selectLocale(identifier: preferred, in: popup)
|
|
|
7427
|
+ }
|
|
|
7428
|
+ primaryLanguagePopup = popup
|
|
|
7429
|
+ } else {
|
|
|
7430
|
+ popup.addItem(withTitle: "None")
|
|
|
7431
|
+ popup.lastItem?.representedObject = ""
|
|
|
7432
|
+ for option in speechLocaleOptions {
|
|
|
7433
|
+ popup.addItem(withTitle: option.displayName)
|
|
|
7434
|
+ popup.lastItem?.representedObject = option.identifier
|
|
|
7435
|
+ }
|
|
|
7436
|
+ if let secondary = selectedSecondaryLanguageIdentifier,
|
|
|
7437
|
+ secondary.isEmpty == false {
|
|
|
7438
|
+ selectLocale(identifier: secondary, in: popup)
|
|
|
7439
|
+ } else {
|
|
|
7440
|
+ popup.selectItem(at: 0)
|
|
|
7441
|
+ }
|
|
|
7442
|
+ secondaryLanguagePopup = popup
|
|
|
7443
|
+ }
|
|
|
7444
|
+
|
|
|
7445
|
+ row.addSubview(titleLabel)
|
|
|
7446
|
+ row.addSubview(popup)
|
|
|
7447
|
+ NSLayoutConstraint.activate([
|
|
|
7448
|
+ titleLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
|
|
|
7449
|
+ titleLabel.topAnchor.constraint(equalTo: row.topAnchor, constant: 6),
|
|
|
7450
|
+ titleLabel.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -4),
|
|
|
7451
|
+
|
|
|
7452
|
+ popup.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
|
|
|
7453
|
+ popup.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -2),
|
|
|
7454
|
+ popup.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
|
|
|
7455
|
+ popup.heightAnchor.constraint(equalToConstant: 28)
|
|
|
7456
|
+ ])
|
|
|
7457
|
+ return row
|
|
|
7458
|
+ }
|
|
|
7459
|
+
|
|
|
7460
|
+ private func selectLocale(identifier: String, in popup: NSPopUpButton) {
|
|
|
7461
|
+ for item in popup.itemArray {
|
|
|
7462
|
+ if let represented = item.representedObject as? String, represented == identifier {
|
|
|
7463
|
+ popup.select(item)
|
|
|
7464
|
+ return
|
|
|
7465
|
+ }
|
|
|
7466
|
+ }
|
|
|
7467
|
+ }
|
|
|
7468
|
+
|
|
|
7469
|
+ @objc private func speechLanguageChanged(_ sender: NSPopUpButton) {
|
|
|
7470
|
+ guard let primary = primaryLanguagePopup?.selectedItem?.representedObject as? String,
|
|
|
7471
|
+ primary.isEmpty == false else { return }
|
|
|
7472
|
+
|
|
|
7473
|
+ var normalizedSecondary: String? = secondaryLanguagePopup?.selectedItem?.representedObject as? String
|
|
|
7474
|
+ if normalizedSecondary?.isEmpty == true {
|
|
|
7475
|
+ normalizedSecondary = nil
|
|
|
7476
|
+ }
|
|
|
7477
|
+ if let secondaryValue = normalizedSecondary,
|
|
|
7478
|
+ secondaryValue.replacingOccurrences(of: "_", with: "-").lowercased() == primary.replacingOccurrences(of: "_", with: "-").lowercased() {
|
|
|
7479
|
+ normalizedSecondary = nil
|
|
|
7480
|
+ secondaryLanguagePopup?.selectItem(at: 0)
|
|
|
7481
|
+ }
|
|
|
7482
|
+ onUpdatePreferredSpeechLanguages(primary, normalizedSecondary)
|
|
|
7483
|
+ }
|
|
|
7484
|
+
|
|
7196
|
7485
|
@objc private func darkModeToggled(_ sender: NSSwitch) {
|
|
7197
|
7486
|
onToggleDarkMode(sender.state == .on)
|
|
7198
|
7487
|
}
|