From 710ed7217f9a22e10b175ea7a2d46e8887c1cb19 Mon Sep 17 00:00:00 2001 From: chris2k20 Date: Mon, 15 Jun 2026 10:57:13 +0200 Subject: [PATCH 1/2] fix(settings): show API key errors next to the key field, not by the save button Every saveErrorText message was about the OpenAI API key (empty key, save failure, clipboard paste validation), but it rendered far down next to the Save button. Clicking 'Einfuegen' next to the key field surfaced the 'clipboard has no plausible key' error well below the fold, where users miss it. Renamed to apiKeyErrorText and moved the display directly beneath the key field. Co-Authored-By: Claude Fable 5 --- .../Settings/SettingsContentView.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/BlitztextMac/Features/Settings/SettingsContentView.swift b/BlitztextMac/Features/Settings/SettingsContentView.swift index aa3c321..6b3e044 100644 --- a/BlitztextMac/Features/Settings/SettingsContentView.swift +++ b/BlitztextMac/Features/Settings/SettingsContentView.swift @@ -69,7 +69,7 @@ struct AccessSettingsView: View { @State private var openAIAPIKey = "" @State private var editingAPIKey = false @State private var saved = false - @State private var saveErrorText: String? + @State private var apiKeyErrorText: String? @State private var installActionErrorText: String? @State private var showCleanupOptions = false @State private var deleteLocalDataOnCleanup = true @@ -158,6 +158,13 @@ struct AccessSettingsView: View { .font(.system(size: 10.5)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) + + if let apiKeyErrorText { + Text(apiKeyErrorText) + .font(.system(size: 10.5)) + .foregroundStyle(.red) + .fixedSize(horizontal: false, vertical: true) + } } VStack(alignment: .leading, spacing: 8) { @@ -253,13 +260,6 @@ struct AccessSettingsView: View { .fixedSize(horizontal: false, vertical: true) } - if let saveErrorText { - Text(saveErrorText) - .font(.system(size: 10.5)) - .foregroundStyle(.red) - .fixedSize(horizontal: false, vertical: true) - } - VStack(alignment: .leading, spacing: 6) { SectionLabel(text: "Hinweis") @@ -371,7 +371,7 @@ struct AccessSettingsView: View { } private func save() { - saveErrorText = nil + apiKeyErrorText = nil cleanupStatusText = nil cleanupErrorText = nil KeychainService.invalidateCache() @@ -379,7 +379,7 @@ struct AccessSettingsView: View { if editingAPIKey || !appState.hasValue(for: .openAIAPIKey) { guard !trimmedAPIKey.isEmpty else { - saveErrorText = "Bitte trage deinen OpenAI API Key ein." + apiKeyErrorText = "Bitte trage deinen OpenAI API Key ein." return } do { @@ -387,14 +387,14 @@ struct AccessSettingsView: View { openAIAPIKey = "" editingAPIKey = false } catch { - saveErrorText = "OpenAI API Key konnte nicht gespeichert werden." + apiKeyErrorText = "OpenAI API Key konnte nicht gespeichert werden." return } } KeychainService.invalidateCache() if !appState.hasValue(for: .openAIAPIKey) { - saveErrorText = "OpenAI API Key wurde nicht persistent gespeichert. Bitte App neu starten und erneut versuchen." + apiKeyErrorText = "OpenAI API Key wurde nicht persistent gespeichert. Bitte App neu starten und erneut versuchen." return } @@ -406,20 +406,20 @@ struct AccessSettingsView: View { private func pasteAPIKeyFromClipboard() { guard let rawText = NSPasteboard.general.string(forType: .string) else { - saveErrorText = "Zwischenablage enthält keinen Text." + apiKeyErrorText = "Zwischenablage enthält keinen Text." return } let firstLine = rawText.components(separatedBy: .newlines).first ?? rawText let trimmedKey = firstLine.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmedKey.range(of: Self.openAIAPIKeyPattern, options: .regularExpression) != nil else { - saveErrorText = "Zwischenablage enthält keinen plausiblen OpenAI API Key." + apiKeyErrorText = "Zwischenablage enthält keinen plausiblen OpenAI API Key." return } openAIAPIKey = trimmedKey NSPasteboard.general.clearContents() - saveErrorText = nil + apiKeyErrorText = nil } private var installationHeadline: String { From a05ac97519f3e53275cdd8ee15acde145bd35fd4 Mon Sep 17 00:00:00 2001 From: chris2k20 Date: Mon, 15 Jun 2026 11:12:34 +0200 Subject: [PATCH 2/2] feat(settings): auto-save API key on commit/blur/paste with inline feedback Removes the hidden bottom Save button. The key now persists Apple-style the moment you press Enter, leave the field, or use Einfuegen, with an inline 'Gespeichert' confirmation. Adds format validation and a direct link to create an OpenAI API key. Co-Authored-By: Claude Fable 5 --- .../Settings/SettingsContentView.swift | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/BlitztextMac/Features/Settings/SettingsContentView.swift b/BlitztextMac/Features/Settings/SettingsContentView.swift index 6b3e044..e18b322 100644 --- a/BlitztextMac/Features/Settings/SettingsContentView.swift +++ b/BlitztextMac/Features/Settings/SettingsContentView.swift @@ -146,6 +146,15 @@ struct AccessSettingsView: View { .textFieldStyle(.roundedBorder) .font(.system(size: 11.5)) .focused($focusedField, equals: .openAIAPIKey) + .onSubmit { saveAPIKey() } + .onChange(of: focusedField) { oldValue, newValue in + // Apple-typisch: beim Verlassen des Feldes speichern. + // Leeres Feld still ignorieren, nicht rot meckern. + if oldValue == .openAIAPIKey, newValue != .openAIAPIKey, + !openAIAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + saveAPIKey() + } + } Button("Einfuegen") { pasteAPIKeyFromClipboard() @@ -154,11 +163,33 @@ struct AccessSettingsView: View { } } + if saved { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 10, weight: .bold)) + Text("Gespeichert") + } + .font(.system(size: 10.5, weight: .medium)) + .foregroundStyle(.green) + .transition(.opacity) + } + Text("Dein Key bleibt lokal in dieser App. Audio und Text werden direkt an die OpenAI API gesendet.") .font(.system(size: 10.5)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) + Link(destination: URL(string: "https://platform.openai.com/api-keys")!) { + HStack(spacing: 3) { + Text("OpenAI API Key erstellen") + Image(systemName: "arrow.up.right") + .font(.system(size: 8, weight: .semibold)) + } + .font(.system(size: 10.5, weight: .medium)) + } + .buttonStyle(.plain) + .foregroundStyle(.blue) + if let apiKeyErrorText { Text(apiKeyErrorText) .font(.system(size: 10.5)) @@ -330,31 +361,9 @@ struct AccessSettingsView: View { } } - // Save button (right-aligned, text only) - HStack { - Spacer() - Button { - save() - } label: { - if saved { - HStack(spacing: 4) { - Image(systemName: "checkmark") - .font(.system(size: 10, weight: .bold)) - Text("Gespeichert") - } - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(.green) - } else { - Text("Speichern") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(.blue) - } - } - .buttonStyle(SubtleButtonStyle()) - .animation(.easeInOut(duration: 0.2), value: saved) - } } .padding(16) + .animation(.easeInOut(duration: 0.2), value: saved) .onAppear { launchAtLoginService.refresh() refreshInstallState() @@ -370,26 +379,31 @@ struct AccessSettingsView: View { openAIAPIKey = "" } - private func save() { + /// Speichert den Key direkt nach Eingabe/Einfügen — kein separater Button. + private func saveAPIKey() { apiKeyErrorText = nil cleanupStatusText = nil cleanupErrorText = nil KeychainService.invalidateCache() + let trimmedAPIKey = openAIAPIKey.trimmingCharacters(in: .whitespacesAndNewlines) - if editingAPIKey || !appState.hasValue(for: .openAIAPIKey) { - guard !trimmedAPIKey.isEmpty else { - apiKeyErrorText = "Bitte trage deinen OpenAI API Key ein." - return - } - do { - try KeychainService.save(key: .openAIAPIKey, value: trimmedAPIKey) - openAIAPIKey = "" - editingAPIKey = false - } catch { - apiKeyErrorText = "OpenAI API Key konnte nicht gespeichert werden." - return - } + guard !trimmedAPIKey.isEmpty else { + apiKeyErrorText = "Bitte trage deinen OpenAI API Key ein." + return + } + guard trimmedAPIKey.range(of: Self.openAIAPIKeyPattern, options: .regularExpression) != nil else { + apiKeyErrorText = "Das sieht nicht nach einem OpenAI API Key aus (beginnt mit „sk-“)." + return + } + + do { + try KeychainService.save(key: .openAIAPIKey, value: trimmedAPIKey) + openAIAPIKey = "" + editingAPIKey = false + } catch { + apiKeyErrorText = "OpenAI API Key konnte nicht gespeichert werden." + return } KeychainService.invalidateCache() @@ -420,6 +434,7 @@ struct AccessSettingsView: View { openAIAPIKey = trimmedKey NSPasteboard.general.clearContents() apiKeyErrorText = nil + saveAPIKey() } private var installationHeadline: String {