diff --git a/OSGKeyboard/Views/APISettingsCard.swift b/OSGKeyboard/Views/APISettingsCard.swift index 9f1b007..efb4678 100644 --- a/OSGKeyboard/Views/APISettingsCard.swift +++ b/OSGKeyboard/Views/APISettingsCard.swift @@ -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 } } diff --git a/OSGKeyboard/Views/AppGroupErrorView.swift b/OSGKeyboard/Views/AppGroupErrorView.swift index 06df649..fbadfbe 100644 --- a/OSGKeyboard/Views/AppGroupErrorView.swift +++ b/OSGKeyboard/Views/AppGroupErrorView.swift @@ -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) } diff --git a/OSGKeyboard/Views/HomeView.swift b/OSGKeyboard/Views/HomeView.swift index 2a9cde9..a76560c 100644 --- a/OSGKeyboard/Views/HomeView.swift +++ b/OSGKeyboard/Views/HomeView.swift @@ -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) } } } diff --git a/OSGKeyboard/Views/KeyboardPreviewSheet.swift b/OSGKeyboard/Views/KeyboardPreviewSheet.swift index 7386e83..e61ab40 100644 --- a/OSGKeyboard/Views/KeyboardPreviewSheet.swift +++ b/OSGKeyboard/Views/KeyboardPreviewSheet.swift @@ -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, diff --git a/OSGKeyboard/Views/KeyboardPreviewStub.swift b/OSGKeyboard/Views/KeyboardPreviewStub.swift index ee3a042..65da6ec 100644 --- a/OSGKeyboard/Views/KeyboardPreviewStub.swift +++ b/OSGKeyboard/Views/KeyboardPreviewStub.swift @@ -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) } diff --git a/OSGKeyboard/Views/OnboardingView.swift b/OSGKeyboard/Views/OnboardingView.swift index e3d3af4..f0be564 100644 --- a/OSGKeyboard/Views/OnboardingView.swift +++ b/OSGKeyboard/Views/OnboardingView.swift @@ -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) diff --git a/OSGKeyboard/Views/ProviderPickerSection.swift b/OSGKeyboard/Views/ProviderPickerSection.swift index 60a60a2..2d70460 100644 --- a/OSGKeyboard/Views/ProviderPickerSection.swift +++ b/OSGKeyboard/Views/ProviderPickerSection.swift @@ -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) diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift index 3f4d8b7..b192baf 100644 --- a/OSGKeyboard/Views/SettingsView.swift +++ b/OSGKeyboard/Views/SettingsView.swift @@ -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) } } } diff --git a/OSGKeyboardExt/Views/KeyboardRootView.swift b/OSGKeyboardExt/Views/KeyboardRootView.swift index 74e1b33..f9b4f80 100644 --- a/OSGKeyboardExt/Views/KeyboardRootView.swift +++ b/OSGKeyboardExt/Views/KeyboardRootView.swift @@ -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) } diff --git a/OSGKeyboardExt/Views/RecordButton.swift b/OSGKeyboardExt/Views/RecordButton.swift index 2e57659..9dad424 100644 --- a/OSGKeyboardExt/Views/RecordButton.swift +++ b/OSGKeyboardExt/Views/RecordButton.swift @@ -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 ) diff --git a/OSGKeyboardExt/Views/WaveformView.swift b/OSGKeyboardExt/Views/WaveformView.swift index c2d8a35..1bb25df 100644 --- a/OSGKeyboardExt/Views/WaveformView.swift +++ b/OSGKeyboardExt/Views/WaveformView.swift @@ -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..