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