ui: move translation credentials into settings

This commit is contained in:
2026-10-17 22:17:21 +02:00
parent b847a483f0
commit 9eb16ddd5a
7 changed files with 186 additions and 86 deletions
+27 -62
View File
@@ -3,13 +3,11 @@ import ConjureKit
import Security
@MainActor
final class TranslationController: NSViewController, NSTextFieldDelegate {
private let keyField = NSSecureTextField()
private let provider = NSPopUpButton()
private let credentials = NSStackView()
private var usesMyMemory: Bool { provider.indexOfSelectedItem == 0 }
final class TranslationController: NSViewController {
let providerPicker = NSPopUpButton()
private var usesMyMemory: Bool { providerPicker.indexOfSelectedItem == 0 }
private let output = NSTextView()
private let disclosure = NSTextField(wrappingLabelWithString: "")
private let settingsButton = NSButton(title: "Open Translate Settings", target: nil, action: nil)
private let status = NSTextField(wrappingLabelWithString: "")
private let translateButton = NSButton(title: "Translate", target: nil, action: nil)
private var task: Task<Void, Never>?
@@ -40,19 +38,16 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
title.font = .systemFont(ofSize: 13, weight: .semibold)
title.textColor = SunsetPalette.text
provider.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
provider.selectItem(at: defaults.string(forKey: "translationProvider") == "openai" ? 1 : 0)
provider.setAccessibilityLabel("Translation provider")
provider.target = self
provider.action = #selector(providerChanged)
keyField.placeholderString = "OpenAI API key (saved in Keychain)"
keyField.setAccessibilityLabel("OpenAI API key")
keyField.delegate = self
keyField.widthAnchor.constraint(greaterThanOrEqualToConstant: 220).isActive = true
let save = NSButton(title: "Save Key", target: self, action: #selector(saveKey))
let forget = NSButton(title: "Forget Key", target: self, action: #selector(forgetKey))
for control in [keyField, save, forget] { credentials.addArrangedSubview(control) }
credentials.distribution = .fill
providerPicker.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
providerPicker.selectItem(at: defaults.string(forKey: "translationProvider") == "openai" ? 1 : 0)
providerPicker.setAccessibilityLabel("Translation provider")
providerPicker.target = self
providerPicker.action = #selector(providerChanged)
settingsButton.target = self
settingsButton.action = #selector(openSettings)
settingsButton.isHidden = true
NotificationCenter.default.addObserver(self, selector: #selector(credentialsChanged),
name: Notification.Name("ConjureTranslationCredentialsChanged"), object: nil)
translateButton.target = self
translateButton.action = #selector(translate)
translateButton.keyEquivalent = "\r"
@@ -79,7 +74,7 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
])
let footer = NSStackView(views: [status, NSView(), actionPill])
let footer = NSStackView(views: [status, settingsButton, NSView(), actionPill])
footer.orientation = .horizontal
footer.alignment = .centerY
footer.spacing = 12
@@ -98,17 +93,13 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
output.isVerticallyResizable = true
output.autoresizingMask = [.width]
output.textContainer?.widthTracksTextView = true
disclosure.textColor = SunsetPalette.secondaryText
disclosure.font = .systemFont(ofSize: 12)
disclosure.setAccessibilityLabel("Translation provider, privacy, and limits")
status.font = .systemFont(ofSize: 12)
status.setAccessibilityLabel("Translation status and errors")
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let stack = NSStackView(views: [title, provider, disclosure, credentials, scroll])
let stack = NSStackView(views: [title, scroll])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 12
stack.setCustomSpacing(4, after: provider)
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
@@ -120,15 +111,17 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36),
credentials.widthAnchor.constraint(equalTo: stack.widthAnchor),
disclosure.widthAnchor.constraint(equalTo: stack.widthAnchor),
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
])
updateProvider()
inputChanged()
}
func controlTextDidChange(_ notification: Notification) { inputChanged() }
@objc private func openSettings() {
NSApp.sendAction(NSSelectorFromString("showSettings:"), to: NSApp.delegate, from: self)
}
@objc private func credentialsChanged() { inputChanged() }
@objc func goBack() {
cancelRequest()
@@ -138,45 +131,15 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
func inputChanged() {
cancelRequest()
output.string = ""
settingsButton.isHidden = true
setStatus("Press Return to translate.")
}
@objc private func providerChanged() {
defaults.set(usesMyMemory ? "mymemory" : "openai", forKey: "translationProvider")
keyField.stringValue = ""
updateProvider()
}
private func updateProvider() {
credentials.isHidden = usesMyMemory
disclosure.stringValue = usesMyMemory
? "MyMemory · No key · 500 bytes/request · 5,000 characters/day. Text is sent to MyMemory; avoid sensitive content."
: "OpenAI · gpt-6-luna · Text is sent to OpenAI only when you translate."
inputChanged()
}
@objc private func saveKey() {
let key = keyField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty, !key.contains(where: { $0.isNewline }) else {
setStatus("Enter a valid API key before saving.", isError: true)
return
}
do {
try credentialsStore.save(key)
keyField.stringValue = ""
setStatus("API key saved in Keychain. Press Return to translate.")
} catch { setStatus(error.localizedDescription, isError: true) }
}
@objc private func forgetKey() {
cancelRequest()
do {
try credentialsStore.forget()
keyField.stringValue = ""
setStatus("API key removed.")
} catch { setStatus(error.localizedDescription, isError: true) }
}
private func cancelRequest() {
task?.cancel()
task = nil
@@ -198,14 +161,15 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
let myMemory = usesMyMemory
var key = ""
if !myMemory {
if !keyField.stringValue.isEmpty { saveKey(); guard keyField.stringValue.isEmpty else { return } }
do {
guard let savedKey = try credentialsStore.load() else {
setStatus("Enter and save your OpenAI API key first.", isError: true)
settingsButton.isHidden = false
setStatus("Save your OpenAI API key in Settings first.", isError: true)
return
}
key = savedKey
} catch {
settingsButton.isHidden = false
setStatus(error.localizedDescription, isError: true)
return
}
@@ -225,6 +189,7 @@ final class TranslationController: NSViewController, NSTextFieldDelegate {
self.setStatus("Japanese · Select text to copy (⌘C).")
} catch {
guard !Task.isCancelled, let self else { return }
self.settingsButton.isHidden = myMemory
self.setStatus("Translation failed: \(error.localizedDescription)", isError: true)
}
guard let self else { return }