import AppKit @MainActor final class SettingsWindowController: NSWindowController { static let shared = SettingsWindowController() private let keyField = NSSecureTextField() private let glossaryField = NSTextField() private let modelPopup = NSPopUpButton() private let fillerCheckbox = NSButton(checkboxWithTitle: "Удалять «э», «эм», «как бы», «короче»", target: nil, action: nil) private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 560, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.title = "Настройки WhisperKey" window.center() super.init(window: window) buildUI() } required init?(coder: NSCoder) { nil } func show() { loadValues() showWindow(nil) NSApp.activate(ignoringOtherApps: true) } private func buildUI() { guard let content = window?.contentView else { return } let grid = NSGridView(views: [ [label("OpenAI API key"), keyField], [label("Облачная модель"), modelPopup], [label("Словарь терминов"), glossaryField], [NSView(), fillerCheckbox], ]) grid.translatesAutoresizingMaskIntoConstraints = false grid.rowSpacing = 14 grid.columnSpacing = 16 grid.column(at: 0).xPlacement = .trailing modelPopup.addItems(withTitles: ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"]) let note = NSTextField(wrappingLabelWithString: "F5 — локально, подтверждённый текст появляется по мере речи. Option+F5 — OpenAI с обычными крупными фрагментами. После 5 секунд тишины запись завершается автоматически.") note.textColor = .secondaryLabelColor note.translatesAutoresizingMaskIntoConstraints = false let save = NSButton(title: "Сохранить", target: self, action: #selector(save)) save.keyEquivalent = "\r" save.translatesAutoresizingMaskIntoConstraints = false content.addSubview(grid) content.addSubview(note) content.addSubview(save) NSLayoutConstraint.activate([ grid.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 24), grid.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24), grid.topAnchor.constraint(equalTo: content.topAnchor, constant: 28), note.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 24), note.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24), note.topAnchor.constraint(equalTo: grid.bottomAnchor, constant: 24), save.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24), save.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -20), ]) } private func label(_ value: String) -> NSTextField { NSTextField(labelWithString: value) } private func loadValues() { let settings = AppSettings.shared keyField.stringValue = settings.apiKey() ?? "" glossaryField.stringValue = settings.glossary modelPopup.selectItem(withTitle: settings.cloudModel) fillerCheckbox.state = settings.removeFillers ? .on : .off } @objc private func save() { do { let settings = AppSettings.shared if !keyField.stringValue.isEmpty { try settings.saveAPIKey(keyField.stringValue) } settings.glossary = glossaryField.stringValue settings.cloudModel = modelPopup.titleOfSelectedItem ?? "gpt-4o-transcribe" settings.removeFillers = fillerCheckbox.state == .on window?.close() } catch { let alert = NSAlert(error: error) alert.runModal() } } }