[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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
public struct KeyboardRootView: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
@ObservedObject var state: State
|
||||
|
||||
@@ -69,7 +70,7 @@ public struct KeyboardRootView: View {
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Palette.divider)
|
||||
.fill(palette.divider)
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
@@ -89,10 +90,10 @@ public struct KeyboardRootView: View {
|
||||
Button(action: state.openSettings) {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(Palette.textSecondary)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(Palette.surface, in: Circle())
|
||||
.overlay(Circle().stroke(Palette.divider, lineWidth: 0.5))
|
||||
.background(palette.surface, in: Circle())
|
||||
.overlay(Circle().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("Open OSGKeyboard settings"))
|
||||
@@ -134,12 +135,12 @@ public struct KeyboardRootView: View {
|
||||
Button(action: state.insertSpace) {
|
||||
Text("空格")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(Palette.textPrimary)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.frame(maxWidth: .infinity, minHeight: 42)
|
||||
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
|
||||
.stroke(Palette.divider, lineWidth: 0.5)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -195,6 +196,8 @@ extension KeyboardRootView {
|
||||
// MARK: - Transcript line
|
||||
|
||||
private struct TranscriptLine: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let phase: KeyboardViewController.State.Phase
|
||||
let transcript: String
|
||||
|
||||
@@ -204,38 +207,38 @@ private struct TranscriptLine: View {
|
||||
case .idle:
|
||||
Text("按住说话 · Hold to talk")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.textTertiary)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
case .requestingPermissions:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(Palette.textSecondary)
|
||||
ProgressView().controlSize(.mini).tint(palette.textSecondary)
|
||||
Text("准备中…")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.textSecondary)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
case .recording:
|
||||
Text(transcript.isEmpty ? " " : transcript)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.textPrimary)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.head)
|
||||
.frame(maxWidth: .infinity)
|
||||
case .processing:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(Palette.accent)
|
||||
ProgressView().controlSize(.mini).tint(palette.accent)
|
||||
Text("润色中 · Polishing")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.textSecondary)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
case .error(let msg):
|
||||
Text(msg)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.warning)
|
||||
.foregroundStyle(palette.warning)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
case .denied(let reason):
|
||||
Text(deniedMessage(for: reason))
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(Palette.warning)
|
||||
.foregroundStyle(palette.warning)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
@@ -255,6 +258,8 @@ private struct TranscriptLine: View {
|
||||
// MARK: - Toolbar icon button
|
||||
|
||||
private struct ToolbarIconButton: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let systemName: String
|
||||
let label: String
|
||||
let action: () -> Void
|
||||
@@ -263,12 +268,12 @@ private struct ToolbarIconButton: View {
|
||||
Button(action: action) {
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundStyle(Palette.textPrimary)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(Palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.background(palette.surfaceElevated, in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
|
||||
.stroke(Palette.divider, lineWidth: 0.5)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -279,6 +284,8 @@ private struct ToolbarIconButton: View {
|
||||
// MARK: - Status badge
|
||||
|
||||
private struct StatusBadge: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let phase: KeyboardViewController.State.Phase
|
||||
|
||||
var body: some View {
|
||||
@@ -289,13 +296,13 @@ private struct StatusBadge: View {
|
||||
case .requestingPermissions:
|
||||
EmptyView()
|
||||
case .recording:
|
||||
dot(color: Palette.recordRed, label: "REC")
|
||||
dot(color: palette.recordRed, label: "REC")
|
||||
case .processing:
|
||||
dot(color: Palette.accent, label: "···")
|
||||
dot(color: palette.accent, label: "···")
|
||||
case .error:
|
||||
dot(color: Palette.warning, label: "!")
|
||||
dot(color: palette.warning, label: "!")
|
||||
case .denied:
|
||||
dot(color: Palette.warning, label: "!")
|
||||
dot(color: palette.warning, label: "!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,18 +314,20 @@ private struct StatusBadge: View {
|
||||
.frame(width: 6, height: 6)
|
||||
Text(label)
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(Palette.textSecondary)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
.padding(.horizontal, Spacing.xs)
|
||||
.padding(.vertical, 3)
|
||||
.background(Palette.surface, in: Capsule())
|
||||
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
|
||||
.background(palette.surface, in: Capsule())
|
||||
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mode chip
|
||||
|
||||
private struct ModeChip: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let mode: KeyboardViewController.State.InputMode
|
||||
let onChange: (KeyboardViewController.State.InputMode) -> Void
|
||||
|
||||
@@ -343,11 +352,11 @@ private struct ModeChip: View {
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
}
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(mode == .off ? Palette.textTertiary : Palette.textPrimary)
|
||||
.foregroundStyle(mode == .off ? palette.textTertiary : palette.textPrimary)
|
||||
.padding(.horizontal, Spacing.xs + 2)
|
||||
.padding(.vertical, 4)
|
||||
.background(Palette.surfaceElevated, in: Capsule())
|
||||
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
|
||||
.background(palette.surfaceElevated, in: Capsule())
|
||||
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
.menuStyle(.button)
|
||||
}
|
||||
@@ -372,6 +381,8 @@ private struct ModeChip: View {
|
||||
// MARK: - Locale chip
|
||||
|
||||
private struct LocaleChip: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let localeId: String
|
||||
let onChange: (String) -> Void
|
||||
|
||||
@@ -405,11 +416,11 @@ private struct LocaleChip: View {
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
}
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(Palette.textPrimary)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.padding(.horizontal, Spacing.xs + 2)
|
||||
.padding(.vertical, 4)
|
||||
.background(Palette.surfaceElevated, in: Capsule())
|
||||
.overlay(Capsule().stroke(Palette.divider, lineWidth: 0.5))
|
||||
.background(palette.surfaceElevated, in: Capsule())
|
||||
.overlay(Capsule().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
.menuStyle(.button)
|
||||
}
|
||||
|
||||
@@ -10,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
|
||||
)
|
||||
|
||||
@@ -10,15 +10,17 @@ import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct WaveformView: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
let level: Double // 0...1, smoothed RMS
|
||||
let barCount: Int
|
||||
let color: Color
|
||||
let color: Color?
|
||||
let active: Bool // when false, bars collapse to a thin resting line
|
||||
|
||||
init(
|
||||
level: Double,
|
||||
barCount: Int = 18,
|
||||
color: Color = Palette.recordRed,
|
||||
color: Color? = nil,
|
||||
active: Bool = true
|
||||
) {
|
||||
self.level = max(0, min(1, level))
|
||||
@@ -27,12 +29,16 @@ struct WaveformView: View {
|
||||
self.active = active
|
||||
}
|
||||
|
||||
private var resolvedColor: Color {
|
||||
color ?? palette.recordRed
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
|
||||
HStack(alignment: .center, spacing: 3) {
|
||||
ForEach(0..<barCount, id: \.self) { i in
|
||||
Capsule()
|
||||
.fill(color)
|
||||
.fill(resolvedColor)
|
||||
.frame(width: 2.4, height: height(for: i, time: context.date.timeIntervalSinceReferenceDate))
|
||||
.opacity(active ? 1.0 : 0.45)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user