Files
OSGKeyboard/OSGKeyboardMac/MacContentView.swift
T
Rocky cdf833935a feat: harden Flow cold-start/force-quit and polish macOS dictation UX
Fix cold-start overlay recursion that overflowed the main-thread stack when
recording began while the ready overlay was still up; also remove temporary
on-screen Flow DEBUG panels after the orange-mic investigation, and land the
macOS overlay/catalog/layout polish plus related Flow recovery hardening.
2026-07-10 12:39:41 +08:00

157 lines
5.8 KiB
Swift

// MacContentView.swift
// OSGKeyboard · Mac
//
// Compact menu-bar popover for quick dictation.
import SwiftUI
struct MacContentView: View {
@ObservedObject var viewModel: MacDictationViewModel
@Environment(\.themePalette) private var palette
private var lang: AppUILanguage { viewModel.config.uiLanguage }
var body: some View {
VStack(alignment: .leading, spacing: Spacing.sm) {
header
recordButton
Text(MacL10n.string(viewModel.hotkeyTrigger.hintKey, language: lang))
.font(TypeStyle.caption)
.foregroundStyle(palette.textTertiary)
.frame(maxWidth: .infinity, alignment: .center)
Text(viewModel.statusMessage)
.font(TypeStyle.caption)
.foregroundStyle(palette.textSecondary)
.contentTransition(.opacity)
if !viewModel.transcript.isEmpty {
ScrollView {
Text(viewModel.transcript)
.font(TypeStyle.footnote)
.foregroundStyle(palette.textPrimary)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: 120)
.padding(Spacing.xs)
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.small, style: .continuous))
.transition(.opacity.combined(with: .move(edge: .top)))
}
Divider().overlay(palette.divider)
statusRow
Divider().overlay(palette.divider)
footer
}
.padding(Spacing.md)
.background(palette.background)
.animation(Motion.soft, value: viewModel.transcript.isEmpty)
.animation(Motion.quick, value: viewModel.statusMessage)
}
private var statusRow: some View {
HStack(spacing: Spacing.sm) {
Label(
viewModel.isCloudMode
? MacL10n.string("mac.mode.cloud", language: lang)
: MacL10n.string("mac.mode.local", language: lang),
systemImage: viewModel.isCloudMode ? "cloud" : "cpu"
)
.foregroundStyle(palette.textSecondary)
Spacer()
translationMenu
Spacer()
Label(MacL10n.string("mac.connected", language: lang), systemImage: "link")
.foregroundStyle(palette.accent)
}
.font(TypeStyle.caption)
.labelStyle(.titleAndIcon)
}
private var translationMenu: some View {
Menu {
ForEach(TranslationLanguageCatalog.all) { language in
Button(MacTranslationDisplay.label(for: language.id, language: lang)) {
viewModel.config.translationTargetLocaleId = language.id
}
}
} label: {
HStack(spacing: 4) {
Image(systemName: "translate")
Text(MacTranslationDisplay.label(for: viewModel.config.translationTargetLocaleId, language: lang))
.lineLimit(1)
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 8, weight: .semibold))
}
.foregroundStyle(palette.textSecondary)
}
.menuStyle(.borderlessButton)
.fixedSize()
}
private var header: some View {
HStack(spacing: Spacing.xs) {
Image("OSGBrandMark")
.renderingMode(.template)
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
.foregroundStyle(palette.accent)
Text("OSGKeyboard")
.font(TypeStyle.bodyEmph)
.foregroundStyle(palette.textPrimary)
Spacer()
if viewModel.isRecording {
MiniWaveform(level: viewModel.audioLevel, barCount: 4)
.transition(.opacity.combined(with: .scale(scale: 0.7)))
}
}
.animation(Motion.quick, value: viewModel.isRecording)
}
private var recordButton: some View {
Button(action: viewModel.toggleRecording) {
HStack {
Image(systemName: viewModel.isRecording ? "stop.fill" : "mic.fill")
.contentTransition(.symbolEffect(.replace))
Text(
viewModel.isRecording
? MacL10n.string("mac.record.stop", language: lang)
: MacL10n.string("mac.record.start", language: lang)
)
.font(TypeStyle.bodyEmph)
.contentTransition(.opacity)
}
.frame(maxWidth: .infinity, minHeight: 40)
.background(
(viewModel.isRecording ? palette.recordRed : palette.accent),
in: RoundedRectangle(cornerRadius: Radius.small, style: .continuous)
)
.foregroundStyle(palette.textOnAccent)
}
.buttonStyle(.plain)
.disabled(viewModel.isProcessing)
.opacity(viewModel.isProcessing ? 0.55 : 1)
.animation(Motion.soft, value: viewModel.isRecording)
.animation(Motion.quick, value: viewModel.isProcessing)
}
private var footer: some View {
HStack {
Button(MacL10n.string("mac.openWindow", language: lang)) { MacMainWindow.open() }
.buttonStyle(.plain)
.font(TypeStyle.caption)
.foregroundStyle(palette.accent)
Spacer()
Button(MacL10n.string("mac.quit", language: lang)) { NSApplication.shared.terminate(nil) }
.buttonStyle(.plain)
.font(TypeStyle.caption)
.foregroundStyle(palette.textTertiary)
}
}
}