feat: add streaming cloud ASR, polish routing, and settings card layout

Unify Bailian/Volcengine/OpenAI realtime streaming, ABE polish routing with
fun styles, and a shared card-page Settings hierarchy; bump to 1.1 (build 32).
This commit is contained in:
Rocky
2026-07-28 20:25:17 +08:00
parent 956331a2af
commit d656bac8c3
58 changed files with 4153 additions and 1396 deletions
@@ -0,0 +1,118 @@
// CardPageLayout.swift
// OSGKeyboard · Shared
//
// Shared structure for card-based pages: consistent page margins, section
// labels, and surface chrome while leaving each feature's content flexible.
import SwiftUI
public struct CardPageContent<Content: View>: View {
private let spacing: CGFloat
private let topPadding: CGFloat
private let bottomPadding: CGFloat
private let content: Content
public init(
spacing: CGFloat = Spacing.md,
topPadding: CGFloat = Spacing.md,
bottomPadding: CGFloat = Spacing.md,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.topPadding = topPadding
self.bottomPadding = bottomPadding
self.content = content()
}
public var body: some View {
VStack(alignment: .leading, spacing: spacing) {
content
}
.padding(.horizontal, Spacing.lg)
.padding(.top, topPadding)
.padding(.bottom, bottomPadding)
}
}
public struct CardSection<Content: View>: View {
private let title: Text
private let content: Content
public init(
_ title: LocalizedStringKey,
@ViewBuilder content: () -> Content
) {
self.title = Text(title)
self.content = content()
}
public init(
title: String,
@ViewBuilder content: () -> Content
) {
self.title = Text(verbatim: title)
self.content = content()
}
public var body: some View {
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
title
.cardSectionLabel()
content
}
}
}
public struct CardSectionLabelModifier: ViewModifier {
@Environment(\.themePalette) private var palette
public init() {}
public func body(content: Content) -> some View {
content
.font(TypeStyle.caption2)
.foregroundStyle(palette.textSecondary)
.textCase(.uppercase)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
public struct SurfaceCardModifier: ViewModifier {
@Environment(\.themePalette) private var palette
private let enabled: Bool
public init(enabled: Bool = true) {
self.enabled = enabled
}
public func body(content: Content) -> some View {
if enabled {
let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
content
.background(
palette.surface,
in: shape
)
// Clip child backgrounds as well as the card surface. Without
// this, a full-width child can visually square off a corner
// even though the shared background and border use Radius.xl.
.clipShape(shape)
.overlay(
shape.stroke(palette.divider, lineWidth: 0.5)
)
} else {
content
}
}
}
public extension View {
func cardSectionLabel() -> some View {
modifier(CardSectionLabelModifier())
}
func surfaceCard(enabled: Bool = true) -> some View {
modifier(SurfaceCardModifier(enabled: enabled))
}
}
@@ -0,0 +1,41 @@
// PolishStyleIconBadge.swift
// OSGKeyboard · Shared
//
// Circular SF Symbol badge for polish-style cards. Fixed footprint keeps icons
// visually consistent across built-in and user-defined styles on iOS and macOS.
import SwiftUI
public struct PolishStyleIconBadge: View {
@Environment(\.themePalette) private var palette
public let systemImage: String
public var isSelected: Bool
private let circleSize: CGFloat = 40
private let iconSize: CGFloat = 18
public init(pack: PolishStylePack, isSelected: Bool = false) {
self.systemImage = PolishStylePackCatalog.systemImage(for: pack.id)
self.isSelected = isSelected
}
public init(systemImage: String, isSelected: Bool = false) {
self.systemImage = systemImage
self.isSelected = isSelected
}
public var body: some View {
ZStack {
Circle()
.fill(isSelected ? palette.accentMuted : palette.surfaceMuted)
.frame(width: circleSize, height: circleSize)
Image(systemName: systemImage)
.font(.system(size: iconSize, weight: .medium))
.foregroundStyle(isSelected ? palette.accent : palette.textSecondary)
.symbolRenderingMode(.hierarchical)
}
.frame(width: circleSize, height: circleSize)
.accessibilityHidden(true)
}
}
@@ -25,13 +25,7 @@ public struct SupportDeveloperSection: View {
}
public var body: some View {
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
Text(SharedL10n.string("tip.title", language: language))
.font(TypeStyle.caption2)
.foregroundStyle(palette.textSecondary)
.textCase(.uppercase)
.frame(maxWidth: .infinity, alignment: .leading)
CardSection(title: SharedL10n.string("tip.title", language: language)) {
VStack(alignment: .leading, spacing: Spacing.sm) {
SupportDeveloperTipBody(language: language)
@@ -57,11 +51,7 @@ public struct SupportDeveloperSection: View {
}
.padding(Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
.stroke(palette.divider, lineWidth: 0.5)
)
.surfaceCard()
}
.onChange(of: tipManager.purchaseState) { _, newValue in
switch newValue {
@@ -15,7 +15,7 @@ public struct UsageSurfaceCard<Content: View>: View {
public init(
padding: CGFloat = Spacing.md,
cornerRadius: CGFloat = Radius.medium,
cornerRadius: CGFloat = Radius.xl,
@ViewBuilder content: @escaping () -> Content
) {
self.padding = padding
@@ -29,6 +29,7 @@ public struct UsageSurfaceCard<Content: View>: View {
content()
.padding(padding)
.background(palette.surface, in: shape)
.clipShape(shape)
.overlay(
shape.stroke(palette.divider, lineWidth: 0.5)
)