Files
ten31-transcripts/Ten31Transcripts/UI/SettingsView.swift
T
Grant Gilliam d9e8c6ceab Open Settings in its own roomy window, not the cramped popover
Settings was a NavigationLink pushed inside the 320pt menu-bar popover, so the
grouped form was cramped and most controls sat below a non-obvious scroll (and
showed a confusing "< Settings" back arrow). Add SettingsWindow (same standalone
NSWindow pattern as the Editor/Templates windows) and open it from the menu-bar
"Settings…" button. Drop the now-unused NavigationStack and the 320pt cap so the
form uses real window width with normal macOS spacing; window is resizable.
2026-06-08 13:57:24 -05:00

112 lines
4.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import AppKit
/// Settings panel (pushed from the menu-bar panel).
struct SettingsView: View {
@EnvironmentObject private var settings: AppSettings
var body: some View {
Form {
Section("Your name") {
TextField("Your name", text: $settings.selfName)
.textFieldStyle(.roundedBorder)
if isDefaultName {
Label("Still set to the default. Enter your real name so your own voice is labeled correctly — and so the AI never gives your name to someone else.",
systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.orange)
} else {
Text("Labels your microphone channel as you in every transcript, and reserves this name so its never assigned to another speaker.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
Section("SparkControl backend") {
TextField("Base URL", text: $settings.backendBaseURL)
.textFieldStyle(.roundedBorder)
Toggle("Skip TLS verification (self-signed cert)",
isOn: $settings.skipTLSVerification)
}
Section("Call detection") {
Toggle("Auto-record when a call is detected", isOn: $settings.autoRecordOnDetection)
Text("Detects Zoom, Teams, Signal, and Google Meet (any browser).")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Transcription") {
Toggle("Auto-send recordings to backend", isOn: $settings.autoSendOnStop)
Toggle("Reconcile speakers (merge splits + name from content)", isOn: $settings.reconcileSpeakers)
Toggle("Build readable recap (topics + highlights)", isOn: $settings.recapEnabled)
HStack {
Picker("Default recap template", selection: $settings.defaultTemplateId) {
ForEach(settings.recapTemplates) { Text($0.name).tag($0.id) }
}
Button("Manage…") { TemplatesWindow.shared.show(settings: settings) }
}
Text("Auto-send transcribes on stop; the recap writes transcript.md + recap.html. Templates define the takeaways categories per meeting type.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Output") {
HStack {
Text(settings.outputFolderPath)
.lineLimit(1)
.truncationMode(.middle)
.foregroundStyle(.secondary)
Spacer()
Button("Choose…", action: chooseFolder)
}
}
Section("Adapters") {
Text("Screen-reading for active-speaker cues. Turn one off to record that app audio-only — transcription still runs, but speakers arent identified from the screen.")
.font(.caption)
.foregroundStyle(.secondary)
ForEach(AppSettings.adapterKeys, id: \.key) { adapter in
Toggle(adapter.label, isOn: binding(for: adapter.key))
}
}
}
.formStyle(.grouped)
.frame(minWidth: 460, idealWidth: 520, maxWidth: .infinity,
minHeight: 520, idealHeight: 660, maxHeight: .infinity)
.navigationTitle("Settings")
}
/// True while the user still has the placeholder name drives the inline nudge.
private var isDefaultName: Bool {
let n = settings.selfName.trimmingCharacters(in: .whitespacesAndNewlines)
return n.isEmpty || n.caseInsensitiveCompare("Me") == .orderedSame
}
private func binding(for key: String) -> Binding<Bool> {
Binding(
get: { settings.adapterEnabled[key] ?? true },
set: { settings.adapterEnabled[key] = $0 }
)
}
private func chooseFolder() {
let panel = NSOpenPanel()
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.allowsMultipleSelection = false
panel.prompt = "Choose"
panel.directoryURL = settings.outputFolderURL
// The app is a menu-bar accessory and this is invoked from the transient
// MenuBarExtra(.window) popover. Use the async begin(...) API rather than
// runModal() a nested modal loop can let the popover dismiss the panel
// out from under it. Activate first so the panel comes to the front.
NSApp.activate(ignoringOtherApps: true)
panel.begin { response in
guard response == .OK, let url = panel.url else { return }
settings.outputFolderPath = url.path
}
}
}