[JJC-20260618-005-C-2] Refactor: replace 166 Palette.xxx with @Environment(\.themePalette) (ARCH-A2)

This commit is contained in:
2026-06-18 12:02:58 +08:00
parent ccf2ea3f90
commit 38ad66f07d
11 changed files with 212 additions and 168 deletions
+20 -18
View File
@@ -8,6 +8,8 @@ import SwiftUI
import OSGKeyboardShared
struct APISettingsCard: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
@State private var showKey: Bool = false
@State private var testStatus: TestStatus = .idle
@@ -28,9 +30,9 @@ struct APISettingsCard: View {
keyboard: .URL,
autocap: false
)
Divider().background(Palette.divider)
Divider().background(palette.divider)
keyField
Divider().background(Palette.divider)
Divider().background(palette.divider)
field(
title: "Model",
placeholder: "gpt-4o-mini",
@@ -39,7 +41,7 @@ struct APISettingsCard: View {
autocap: false
)
if let url = LLMProvider.provider(id: config.providerId).apiKeyURL {
Divider().background(Palette.divider)
Divider().background(palette.divider)
// Use a Button + UIApplication.open instead of SwiftUI
// `Link`. SwiftUI `Link` has a hit-test bug on iOS 18 that
// makes its tappable area eat gestures from the adjacent
@@ -49,12 +51,12 @@ struct APISettingsCard: View {
} label: {
HStack {
Image(systemName: "key.fill")
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
Text("Get an API key")
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Spacer()
Image(systemName: "arrow.up.right.square")
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
.padding(.horizontal, Spacing.md)
.padding(.vertical, Spacing.sm)
@@ -62,13 +64,13 @@ struct APISettingsCard: View {
}
.buttonStyle(.plain)
}
Divider().background(Palette.divider)
Divider().background(palette.divider)
testConnectionRow
}
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
@@ -79,11 +81,11 @@ struct APISettingsCard: View {
HStack {
Text("API Key")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
Spacer()
Button(action: { showKey.toggle() }) {
Image(systemName: showKey ? "eye.slash.fill" : "eye.fill")
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
.buttonStyle(.plain)
.accessibilityLabel(Text(showKey ? "Hide key" : "Show key"))
@@ -98,7 +100,7 @@ struct APISettingsCard: View {
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
}
.padding(.horizontal, Spacing.md)
.padding(.vertical, Spacing.sm)
@@ -117,13 +119,13 @@ struct APISettingsCard: View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
TextField(placeholder, text: text)
.keyboardType(keyboard)
.autocorrectionDisabled(true)
.textInputAutocapitalization(autocap ? .sentences : .never)
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.submitLabel(.done)
.onSubmit { /* no-op: prevent the keyboard from "submitting"
and dismissing the sheet on iOS 18 */ }
@@ -139,7 +141,7 @@ struct APISettingsCard: View {
HStack {
Text("Connection")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
Spacer()
Button(action: runTest) {
HStack(spacing: 6) {
@@ -187,9 +189,9 @@ struct APISettingsCard: View {
private var testTint: Color {
switch testStatus {
case .idle, .running: return Palette.accent
case .success: return Palette.success
case .failure: return Palette.danger
case .idle, .running: return palette.accent
case .success: return palette.success
case .failure: return palette.danger
}
}
+5 -3
View File
@@ -14,24 +14,26 @@ import SwiftUI
import OSGKeyboardShared
struct AppGroupErrorView: View {
@Environment(\.themePalette) private var palette: ThemePalette
var body: some View {
VStack(spacing: Spacing.lg) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 48))
.foregroundStyle(Palette.danger)
.foregroundStyle(palette.danger)
Text("App Group 未配置")
.font(TypeStyle.title2)
Text("OSGKeyboard 需要 App Group 才能在键盘扩展和主 App 之间共享配置。")
.font(TypeStyle.body)
.multilineTextAlignment(.center)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
VStack(alignment: .leading, spacing: Spacing.sm) {
Label("在 Apple Developer 后台创建 group.com.osgkeyboard.shared", systemImage: "1.circle")
Label("主 App 和键盘扩展都启用该 App Group", systemImage: "2.circle")
Label("重新生成 provisioning profile 并下载", systemImage: "3.circle")
}
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
}
.padding(Spacing.lg)
}
+17 -15
View File
@@ -9,13 +9,15 @@ import OSGKeyboardShared
import OSGKeyboardExt
struct HomeView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config = ProviderConfig.shared
@State private var showSettings = false
@State private var showKeyboardPreview = false
var body: some View {
ZStack {
Palette.background.ignoresSafeArea()
palette.background.ignoresSafeArea()
VStack(spacing: 0) {
statusHeader
.padding(.top, Spacing.xl)
@@ -41,18 +43,18 @@ struct HomeView: View {
VStack(spacing: Spacing.xs) {
HStack(spacing: 6) {
Circle()
.fill(config.isConfigured ? Palette.success : Palette.warning)
.fill(config.isConfigured ? palette.success : palette.warning)
.frame(width: 8, height: 8)
Text(config.isConfigured ? "Ready" : "Setup incomplete")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
Text("OSGKeyboard")
.font(TypeStyle.largeTitle)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text(providerLine)
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
}
}
@@ -71,27 +73,27 @@ struct HomeView: View {
} label: {
ZStack {
Circle()
.fill(Palette.accentMuted)
.fill(palette.accentMuted)
.frame(width: 220, height: 220)
.blur(radius: 40)
Circle()
.fill(LinearGradient(
colors: [Palette.surfaceElevated, Palette.surface],
colors: [palette.surfaceElevated, palette.surface],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 160, height: 160)
.overlay(
Circle().stroke(Palette.accent.opacity(0.35), lineWidth: 1.5)
Circle().stroke(palette.accent.opacity(0.35), lineWidth: 1.5)
)
.shadow(color: Palette.accent.opacity(0.18), radius: 24, y: 8)
.shadow(color: palette.accent.opacity(0.18), radius: 24, y: 8)
VStack(spacing: 6) {
Image(systemName: "waveform")
.font(.system(size: 36, weight: .light))
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
Text("Tap to configure")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
}
}
@@ -139,17 +141,17 @@ struct HomeView: View {
} label: {
Text("GitHub")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.padding(.horizontal, Spacing.sm)
.padding(.vertical, 6)
.background(Palette.surface, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
.buttonStyle(.plain)
Spacer()
Text("v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1.0")")
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
}
}
}
+10 -8
View File
@@ -11,6 +11,8 @@ import SwiftUI
import OSGKeyboardShared
struct KeyboardPreviewSheet: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.dismiss) private var dismiss
// We mirror only the fields the stand-in actually needs, instead of
@@ -24,15 +26,15 @@ struct KeyboardPreviewSheet: View {
var body: some View {
ZStack {
Palette.background.ignoresSafeArea()
palette.background.ignoresSafeArea()
VStack(spacing: 0) {
VStack(spacing: Spacing.md) {
Text("Keyboard Preview")
.font(TypeStyle.title2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text("Tap the disc to cycle states. The real keyboard uses the same layout.")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Spacing.lg)
mockTextField.padding(.horizontal, Spacing.md)
@@ -47,23 +49,23 @@ struct KeyboardPreviewSheet: View {
private var mockTextField: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
Text("Type here…")
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
Spacer()
}
.padding(Spacing.md)
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.medium))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.medium))
.overlay(
RoundedRectangle(cornerRadius: Radius.medium)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
private var keyboardBlock: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Palette.divider)
.fill(palette.divider)
.frame(height: 0.5)
KeyboardPreviewStub(
phase: stubPhase,
+34 -33
View File
@@ -11,6 +11,7 @@ import SwiftUI
import OSGKeyboardShared
struct KeyboardPreviewStub: View {
@Environment(\.themePalette) private var palette: ThemePalette
enum Phase { case idle, recording, processing }
@@ -20,7 +21,7 @@ struct KeyboardPreviewStub: View {
var body: some View {
ZStack(alignment: .top) {
Palette.background
palette.background
VStack(spacing: 0) {
topBar.frame(height: 32)
centreArea.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -43,10 +44,10 @@ struct KeyboardPreviewStub: View {
Button(action: {}) {
Image(systemName: "gearshape.fill")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.frame(width: 28, height: 28)
.background(Palette.surface, in: Circle())
.overlay(Circle().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Circle())
.overlay(Circle().stroke(palette.divider, lineWidth: 0.5))
}
.buttonStyle(.plain)
}
@@ -60,10 +61,10 @@ struct KeyboardPreviewStub: View {
Image(systemName: "chevron.down").font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.padding(.horizontal, Spacing.xs + 2).padding(.vertical, 4)
.background(Palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
private var localeChip: some View {
@@ -73,10 +74,10 @@ struct KeyboardPreviewStub: View {
Image(systemName: "chevron.down").font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.padding(.horizontal, Spacing.xs + 2).padding(.vertical, 4)
.background(Palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
private var statusBadge: some View {
@@ -86,20 +87,20 @@ struct KeyboardPreviewStub: View {
EmptyView()
case .recording:
HStack(spacing: 4) {
Circle().fill(Palette.recordRed).frame(width: 6, height: 6)
Text("REC").font(TypeStyle.caption2).foregroundStyle(Palette.textSecondary)
Circle().fill(palette.recordRed).frame(width: 6, height: 6)
Text("REC").font(TypeStyle.caption2).foregroundStyle(palette.textSecondary)
}
.padding(.horizontal, Spacing.xs).padding(.vertical, 3)
.background(Palette.surface, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
case .processing:
HStack(spacing: 4) {
Circle().fill(Palette.accent).frame(width: 6, height: 6)
Text("···").font(TypeStyle.caption2).foregroundStyle(Palette.textSecondary)
Circle().fill(palette.accent).frame(width: 6, height: 6)
Text("···").font(TypeStyle.caption2).foregroundStyle(palette.textSecondary)
}
.padding(.horizontal, Spacing.xs).padding(.vertical, 3)
.background(Palette.surface, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
}
}
@@ -120,20 +121,20 @@ struct KeyboardPreviewStub: View {
case .idle:
Text("按住说话 · Hold to talk")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
case .recording:
Text(transcript.isEmpty ? " " : transcript)
.font(TypeStyle.caption)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
.truncationMode(.head)
.frame(maxWidth: .infinity)
case .processing:
HStack(spacing: 6) {
ProgressView().controlSize(.mini).tint(Palette.accent)
ProgressView().controlSize(.mini).tint(palette.accent)
Text("润色中 · Polishing")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
}
}
@@ -144,11 +145,11 @@ struct KeyboardPreviewStub: View {
ZStack {
if phase == .recording {
Circle()
.stroke(Palette.recordRed.opacity(0.35), lineWidth: 2)
.stroke(palette.recordRed.opacity(0.35), lineWidth: 2)
.frame(width: 110, height: 110)
.opacity(0.6)
Circle()
.fill(RadialGradient(colors: [Palette.recordRed.opacity(0.55), .clear], center: .center, startRadius: 30, endRadius: 70))
.fill(RadialGradient(colors: [palette.recordRed.opacity(0.55), .clear], center: .center, startRadius: 30, endRadius: 70))
.frame(width: 160, height: 160)
.blur(radius: 12)
.opacity(0.4 + level * 0.6)
@@ -168,7 +169,7 @@ struct KeyboardPreviewStub: View {
HStack(spacing: 3) {
ForEach(0..<12, id: \.self) { i in
Capsule()
.fill(Palette.recordRed)
.fill(palette.recordRed)
.frame(width: 2, height: 8 + CGFloat(level * 30) * (i.isMultiple(of: 2) ? 1 : 0.6))
}
}
@@ -183,9 +184,9 @@ struct KeyboardPreviewStub: View {
private var discGradient: LinearGradient {
switch phase {
case .recording:
return LinearGradient(colors: [Palette.recordRed.opacity(0.95), Palette.recordRed.opacity(0.75)], startPoint: .top, endPoint: .bottom)
return LinearGradient(colors: [palette.recordRed.opacity(0.95), palette.recordRed.opacity(0.75)], startPoint: .top, endPoint: .bottom)
case .processing:
return LinearGradient(colors: [Palette.surfaceElevated, Palette.surface], startPoint: .top, endPoint: .bottom)
return LinearGradient(colors: [palette.surfaceElevated, palette.surface], startPoint: .top, endPoint: .bottom)
case .idle:
return LinearGradient(colors: [Color(white: 0.22), Color(white: 0.10)], startPoint: .top, endPoint: .bottom)
}
@@ -201,10 +202,10 @@ struct KeyboardPreviewStub: View {
Button(action: {}) {
Text("空格")
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.frame(maxWidth: .infinity, minHeight: 42)
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous).stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous).stroke(palette.divider, lineWidth: 0.5))
}
.buttonStyle(.plain)
Spacer(minLength: 0)
@@ -217,10 +218,10 @@ struct KeyboardPreviewStub: View {
Button(action: {}) {
Image(systemName: systemName)
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.frame(width: 40, height: 40)
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous).stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous).stroke(palette.divider, lineWidth: 0.5))
}
.buttonStyle(.plain)
}
+37 -27
View File
@@ -15,12 +15,14 @@ import SwiftUI
import OSGKeyboardShared
struct OnboardingView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
@State private var page: Int = 0
var body: some View {
ZStack {
Palette.background.ignoresSafeArea()
palette.background.ignoresSafeArea()
VStack(spacing: 0) {
TabView(selection: $page) {
WelcomePage().tag(0)
@@ -43,7 +45,7 @@ struct OnboardingView: View {
HStack(spacing: 6) {
ForEach(0..<3, id: \.self) { i in
Capsule()
.fill(i == page ? Palette.accent : Color.white.opacity(0.18))
.fill(i == page ? palette.accent : Color.white.opacity(0.18))
.frame(width: i == page ? 18 : 6, height: 6)
.animation(Motion.quick, value: page)
}
@@ -58,12 +60,12 @@ struct OnboardingView: View {
Text("Back")
.font(TypeStyle.headline)
.frame(maxWidth: .infinity, minHeight: 50)
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(Palette.dividerStrong, lineWidth: 0.5)
.stroke(palette.dividerStrong, lineWidth: 0.5)
)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
}
.buttonStyle(.plain)
}
@@ -77,11 +79,11 @@ struct OnboardingView: View {
.font(TypeStyle.headline)
.frame(maxWidth: .infinity, minHeight: 50)
.background(
(page == 2 && !config.isConfigured) ? Palette.surfaceElevated : Palette.accent,
(page == 2 && !config.isConfigured) ? palette.surfaceElevated : palette.accent,
in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
)
.foregroundStyle(
(page == 2 && !config.isConfigured) ? Palette.textSecondary : Palette.textOnAccent
(page == 2 && !config.isConfigured) ? palette.textSecondary : palette.textOnAccent
)
}
.buttonStyle(.plain)
@@ -93,29 +95,31 @@ struct OnboardingView: View {
// MARK: - Page 1: Welcome
private struct WelcomePage: View {
@Environment(\.themePalette) private var palette: ThemePalette
var body: some View {
VStack(spacing: Spacing.xl) {
Spacer()
ZStack {
Circle()
.fill(Palette.accentMuted)
.fill(palette.accentMuted)
.frame(width: 180, height: 180)
.blur(radius: 30)
Image(systemName: "mic.circle.fill")
.font(.system(size: 96, weight: .light))
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
}
VStack(spacing: Spacing.sm) {
Text("OSGKeyboard")
.font(TypeStyle.title)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text("按住说话,松开即得润色文字。")
.font(TypeStyle.body)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
Text("Hold to talk. Release for polished text, in any app.")
.font(TypeStyle.footnote)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, Spacing.xl)
@@ -127,6 +131,8 @@ private struct WelcomePage: View {
}
private struct PrivacyFootnote: View {
@Environment(\.themePalette) private var palette: ThemePalette
var body: some View {
VStack(alignment: .leading, spacing: 8) {
footnoteRow(icon: "lock.fill",
@@ -140,10 +146,10 @@ private struct PrivacyFootnote: View {
body: "WeChat, Notes, Mail, ChatGPT, Claude, Cursor — anywhere a keyboard appears.")
}
.padding(Spacing.md)
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
.padding(.horizontal, Spacing.md)
}
@@ -152,16 +158,16 @@ private struct PrivacyFootnote: View {
HStack(alignment: .top, spacing: Spacing.xs) {
Image(systemName: icon)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
.frame(width: 24, height: 24)
.background(Palette.accentMuted, in: Circle())
.background(palette.accentMuted, in: Circle())
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(TypeStyle.caption)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text(body)
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
}
}
@@ -170,19 +176,21 @@ private struct PrivacyFootnote: View {
// MARK: - Page 2: Enable keyboard
private struct EnableKeyboardPage: View {
@Environment(\.themePalette) private var palette: ThemePalette
var body: some View {
VStack(spacing: Spacing.xl) {
Spacer()
Image(systemName: "keyboard.fill")
.font(.system(size: 64, weight: .light))
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
VStack(spacing: Spacing.sm) {
Text("启用 OSGKeyboard")
.font(TypeStyle.title2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text("Enable OSGKeyboard")
.font(TypeStyle.body)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
}
VStack(alignment: .leading, spacing: Spacing.sm) {
step(num: 1, text: "Settings → General → Keyboard → Keyboards")
@@ -211,11 +219,11 @@ private struct EnableKeyboardPage: View {
Text("\(num)")
.font(TypeStyle.caption2)
.frame(width: 22, height: 22)
.background(Palette.accent, in: Circle())
.foregroundStyle(Palette.textOnAccent)
.background(palette.accent, in: Circle())
.foregroundStyle(palette.textOnAccent)
Text(text)
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.fixedSize(horizontal: false, vertical: true)
}
}
@@ -224,6 +232,8 @@ private struct EnableKeyboardPage: View {
// MARK: - Page 3: API setup
private struct APISetupPage: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
var body: some View {
@@ -232,13 +242,13 @@ private struct APISetupPage: View {
VStack(alignment: .leading, spacing: Spacing.xxs) {
Text("配置 AI 提供商")
.font(TypeStyle.title2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Text("Configure your AI provider")
.font(TypeStyle.body)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
Text("OSGKeyboard only calls the AI to polish your text. No audio leaves your device.")
.font(TypeStyle.footnote)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.padding(.top, Spacing.xxs)
}
.padding(.horizontal, Spacing.md)
+10 -8
View File
@@ -9,6 +9,8 @@ import SwiftUI
import OSGKeyboardShared
struct ProviderPickerSection: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
var body: some View {
@@ -21,14 +23,14 @@ struct ProviderPickerSection: View {
}
.buttonStyle(.plain)
if index < LLMProvider.presets.count - 1 {
Divider().background(Palette.divider)
Divider().background(palette.divider)
}
}
}
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
@@ -44,27 +46,27 @@ struct ProviderPickerSection: View {
// Provider mark a coloured dot with first letter
ZStack {
Circle()
.fill(selected ? Palette.accent : Palette.surfaceElevated)
.fill(selected ? palette.accent : palette.surfaceElevated)
.frame(width: 36, height: 36)
Text(String(provider.name.prefix(1)))
.font(TypeStyle.bodyEmph)
.foregroundStyle(selected ? Palette.textOnAccent : Palette.textPrimary)
.foregroundStyle(selected ? palette.textOnAccent : palette.textPrimary)
}
VStack(alignment: .leading, spacing: 2) {
Text(provider.name)
.font(TypeStyle.bodyEmph)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
if let blurb = provider.blurb {
Text(blurb)
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
}
}
Spacer()
if selected {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18, weight: .medium))
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
}
}
.padding(.horizontal, Spacing.md)
+18 -14
View File
@@ -8,6 +8,8 @@ import SwiftUI
import OSGKeyboardShared
struct SettingsView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config = ProviderConfig.shared
@Environment(\.dismiss) private var dismiss
@State private var showResetConfirm = false
@@ -15,7 +17,7 @@ struct SettingsView: View {
var body: some View {
NavigationStack {
ZStack {
Palette.background.ignoresSafeArea()
palette.background.ignoresSafeArea()
ScrollView {
VStack(spacing: Spacing.md) {
providerSection
@@ -34,7 +36,7 @@ struct SettingsView: View {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { dismiss() }
.font(TypeStyle.headline)
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
}
}
}
@@ -84,7 +86,7 @@ struct SettingsView: View {
set: { config.modeId = $0 }
)
)
Divider().background(Palette.divider)
Divider().background(palette.divider)
PickerRow(
title: "ASR locale",
options: localeOptions,
@@ -94,10 +96,10 @@ struct SettingsView: View {
)
)
}
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
}
@@ -130,7 +132,7 @@ struct SettingsView: View {
Spacer()
Button("Reset") { config.systemPrompt = config.defaultSystemPrompt }
.font(TypeStyle.caption2)
.foregroundStyle(Palette.accent)
.foregroundStyle(palette.accent)
}
VStack(alignment: .leading, spacing: Spacing.xs) {
TextEditor(text: $config.systemPrompt)
@@ -138,10 +140,10 @@ struct SettingsView: View {
.scrollContentBackground(.hidden)
.frame(minHeight: 140)
.padding(Spacing.xs)
.background(Palette.surface, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
.cardSurface()
@@ -156,7 +158,7 @@ struct SettingsView: View {
} label: {
Text("Reset all settings")
.font(TypeStyle.caption)
.foregroundStyle(Palette.danger)
.foregroundStyle(palette.danger)
.frame(maxWidth: .infinity, minHeight: 40)
}
.buttonStyle(.plain)
@@ -168,12 +170,12 @@ struct SettingsView: View {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.textCase(.uppercase)
if let subtitle {
Text(subtitle)
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -183,6 +185,8 @@ struct SettingsView: View {
// MARK: - Picker row
private struct PickerRow: View {
@Environment(\.themePalette) private var palette: ThemePalette
let title: String
let options: [(id: String, label: String)]
@Binding var selection: String
@@ -191,7 +195,7 @@ private struct PickerRow: View {
HStack {
Text(title)
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
Spacer()
Menu {
ForEach(options, id: \.id) { o in
@@ -209,10 +213,10 @@ private struct PickerRow: View {
HStack(spacing: 4) {
Text(currentLabel)
.font(TypeStyle.body)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
}
}
}
+42 -31
View File
@@ -22,6 +22,7 @@ import SwiftUI
import OSGKeyboardShared
public struct KeyboardRootView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var state: State
@@ -69,7 +70,7 @@ public struct KeyboardRootView: View {
}
.overlay(alignment: .top) {
Rectangle()
.fill(Palette.divider)
.fill(palette.divider)
.frame(height: 0.5)
}
}
@@ -89,10 +90,10 @@ public struct KeyboardRootView: View {
Button(action: state.openSettings) {
Image(systemName: "gearshape.fill")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
.frame(width: 28, height: 28)
.background(Palette.surface, in: Circle())
.overlay(Circle().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Circle())
.overlay(Circle().stroke(palette.divider, lineWidth: 0.5))
}
.buttonStyle(.plain)
.accessibilityLabel(Text("Open OSGKeyboard settings"))
@@ -134,12 +135,12 @@ public struct KeyboardRootView: View {
Button(action: state.insertSpace) {
Text("空格")
.font(TypeStyle.body)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.frame(maxWidth: .infinity, minHeight: 42)
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
.buttonStyle(.plain)
@@ -195,6 +196,8 @@ extension KeyboardRootView {
// MARK: - Transcript line
private struct TranscriptLine: View {
@Environment(\.themePalette) private var palette: ThemePalette
let phase: KeyboardViewController.State.Phase
let transcript: String
@@ -204,38 +207,38 @@ private struct TranscriptLine: View {
case .idle:
Text("按住说话 · Hold to talk")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textTertiary)
.foregroundStyle(palette.textTertiary)
case .requestingPermissions:
HStack(spacing: 6) {
ProgressView().controlSize(.mini).tint(Palette.textSecondary)
ProgressView().controlSize(.mini).tint(palette.textSecondary)
Text("准备中…")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
case .recording:
Text(transcript.isEmpty ? " " : transcript)
.font(TypeStyle.caption)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
.truncationMode(.head)
.frame(maxWidth: .infinity)
case .processing:
HStack(spacing: 6) {
ProgressView().controlSize(.mini).tint(Palette.accent)
ProgressView().controlSize(.mini).tint(palette.accent)
Text("润色中 · Polishing")
.font(TypeStyle.caption)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
case .error(let msg):
Text(msg)
.font(TypeStyle.caption)
.foregroundStyle(Palette.warning)
.foregroundStyle(palette.warning)
.lineLimit(1)
.truncationMode(.tail)
case .denied(let reason):
Text(deniedMessage(for: reason))
.font(TypeStyle.caption)
.foregroundStyle(Palette.warning)
.foregroundStyle(palette.warning)
.lineLimit(1)
.truncationMode(.tail)
}
@@ -255,6 +258,8 @@ private struct TranscriptLine: View {
// MARK: - Toolbar icon button
private struct ToolbarIconButton: View {
@Environment(\.themePalette) private var palette: ThemePalette
let systemName: String
let label: String
let action: () -> Void
@@ -263,12 +268,12 @@ private struct ToolbarIconButton: View {
Button(action: action) {
Image(systemName: systemName)
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.frame(width: 40, height: 40)
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
.stroke(Palette.divider, lineWidth: 0.5)
.stroke(palette.divider, lineWidth: 0.5)
)
}
.buttonStyle(.plain)
@@ -279,6 +284,8 @@ private struct ToolbarIconButton: View {
// MARK: - Status badge
private struct StatusBadge: View {
@Environment(\.themePalette) private var palette: ThemePalette
let phase: KeyboardViewController.State.Phase
var body: some View {
@@ -289,13 +296,13 @@ private struct StatusBadge: View {
case .requestingPermissions:
EmptyView()
case .recording:
dot(color: Palette.recordRed, label: "REC")
dot(color: palette.recordRed, label: "REC")
case .processing:
dot(color: Palette.accent, label: "···")
dot(color: palette.accent, label: "···")
case .error:
dot(color: Palette.warning, label: "!")
dot(color: palette.warning, label: "!")
case .denied:
dot(color: Palette.warning, label: "!")
dot(color: palette.warning, label: "!")
}
}
}
@@ -307,18 +314,20 @@ private struct StatusBadge: View {
.frame(width: 6, height: 6)
Text(label)
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textSecondary)
.foregroundStyle(palette.textSecondary)
}
.padding(.horizontal, Spacing.xs)
.padding(.vertical, 3)
.background(Palette.surface, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surface, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
}
// MARK: - Mode chip
private struct ModeChip: View {
@Environment(\.themePalette) private var palette: ThemePalette
let mode: KeyboardViewController.State.InputMode
let onChange: (KeyboardViewController.State.InputMode) -> Void
@@ -343,11 +352,11 @@ private struct ModeChip: View {
.font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(mode == .off ? Palette.textTertiary : Palette.textPrimary)
.foregroundStyle(mode == .off ? palette.textTertiary : palette.textPrimary)
.padding(.horizontal, Spacing.xs + 2)
.padding(.vertical, 4)
.background(Palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
.menuStyle(.button)
}
@@ -372,6 +381,8 @@ private struct ModeChip: View {
// MARK: - Locale chip
private struct LocaleChip: View {
@Environment(\.themePalette) private var palette: ThemePalette
let localeId: String
let onChange: (String) -> Void
@@ -405,11 +416,11 @@ private struct LocaleChip: View {
.font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
.padding(.horizontal, Spacing.xs + 2)
.padding(.vertical, 4)
.background(Palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
.background(palette.surfaceElevated, in: Capsule())
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
}
.menuStyle(.button)
}
+10 -8
View File
@@ -10,6 +10,8 @@ import SwiftUI
import OSGKeyboardShared
struct RecordButton: View {
@Environment(\.themePalette) private var palette: ThemePalette
enum Phase: Equatable {
case idle
case recording
@@ -44,7 +46,7 @@ struct RecordButton: View {
ZStack {
// Outer breathing ring (recording only)
Circle()
.stroke(Palette.recordRed.opacity(0.35), lineWidth: 2)
.stroke(palette.recordRed.opacity(0.35), lineWidth: 2)
.frame(width: 150, height: 150)
.scaleEffect(breath ? 1.18 : 0.95)
.opacity(phase == .recording ? 1 : 0)
@@ -54,7 +56,7 @@ struct RecordButton: View {
Circle()
.fill(
RadialGradient(
colors: [Palette.recordRed.opacity(0.55), .clear],
colors: [palette.recordRed.opacity(0.55), .clear],
center: .center,
startRadius: 50,
endRadius: 100
@@ -88,7 +90,7 @@ struct RecordButton: View {
case .idle:
Image(systemName: "mic.fill")
.font(.system(size: 38, weight: .medium))
.foregroundStyle(Palette.textPrimary)
.foregroundStyle(palette.textPrimary)
case .recording:
WaveformView(level: level, active: true)
.frame(width: 80, height: 44)
@@ -96,12 +98,12 @@ struct RecordButton: View {
case .processing:
ProgressView()
.progressViewStyle(.circular)
.tint(Palette.textPrimary)
.tint(palette.textPrimary)
.scaleEffect(1.2)
case .error:
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 30, weight: .medium))
.foregroundStyle(Palette.warning)
.foregroundStyle(palette.warning)
}
}
}
@@ -160,19 +162,19 @@ struct RecordButton: View {
switch phase {
case .recording:
return LinearGradient(
colors: [Palette.recordRed.opacity(0.95), Palette.recordRed.opacity(0.75)],
colors: [palette.recordRed.opacity(0.95), palette.recordRed.opacity(0.75)],
startPoint: .top,
endPoint: .bottom
)
case .processing:
return LinearGradient(
colors: [Palette.surfaceElevated, Palette.surface],
colors: [palette.surfaceElevated, palette.surface],
startPoint: .top,
endPoint: .bottom
)
case .error:
return LinearGradient(
colors: [Palette.warning.opacity(0.85), Palette.warning.opacity(0.55)],
colors: [palette.warning.opacity(0.85), palette.warning.opacity(0.55)],
startPoint: .top,
endPoint: .bottom
)
+9 -3
View File
@@ -10,15 +10,17 @@ import SwiftUI
import OSGKeyboardShared
struct WaveformView: View {
@Environment(\.themePalette) private var palette: ThemePalette
let level: Double // 0...1, smoothed RMS
let barCount: Int
let color: Color
let color: Color?
let active: Bool // when false, bars collapse to a thin resting line
init(
level: Double,
barCount: Int = 18,
color: Color = Palette.recordRed,
color: Color? = nil,
active: Bool = true
) {
self.level = max(0, min(1, level))
@@ -27,12 +29,16 @@ struct WaveformView: View {
self.active = active
}
private var resolvedColor: Color {
color ?? palette.recordRed
}
var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
HStack(alignment: .center, spacing: 3) {
ForEach(0..<barCount, id: \.self) { i in
Capsule()
.fill(color)
.fill(resolvedColor)
.frame(width: 2.4, height: height(for: i, time: context.date.timeIntervalSinceReferenceDate))
.opacity(active ? 1.0 : 0.45)
}