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:
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user