[JJC-20260618-005-C-2] Refactor: replace 166 Palette.xxx with @Environment(\.themePalette) (ARCH-A2)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user