Files
OSGKeyboard/OSGKeyboard/Views/FeaturePreviewHostDocument.swift
T

171 lines
6.1 KiB
Swift

// FeaturePreviewHostDocument.swift
// OSGKeyboard · Main App (DEBUG-only)
//
// Fills the empty band above the scripted keyboard demos so a full-screen
// App Store preview can be recorded from them. The What's New workflow crops
// to the keyboard chrome and must keep its blank backdrop, so every demo view
// only shows this when `--preview-fullscreen` is passed.
//
// Visual language deliberately matches `NotesHostDemoView` — the same
// Notes / Messages stand-in the extension-based recordings sit on top of.
#if DEBUG
import OSGKeyboardShared
import SwiftUI
enum FeaturePreviewFlags {
/// Opt into the full-screen host document above the keyboard chrome.
static var isFullscreen: Bool {
ProcessInfo.processInfo.arguments.contains("--preview-fullscreen")
}
/// Recording language from `--whats-new-lang=zh|en`; Chinese when absent.
static var language: WhatsNewDemoScenario.Language {
let prefix = "--whats-new-lang="
guard let argument = ProcessInfo.processInfo.arguments.first(
where: { $0.hasPrefix(prefix) }
) else {
return .zh
}
return WhatsNewDemoScenario.Language(
rawValue: String(argument.dropFirst(prefix.count))
) ?? .zh
}
/// Points the extension strings (`ExtL10n` reads the App Group setting)
/// at the recording language before the first frame renders.
@MainActor
static func applyRecordingLanguage() {
ProviderConfig.shared.uiLanguage = language == .en ? .english : .chinese
}
}
/// A Notes- or Messages-like document that reacts to the demo timeline.
struct FeaturePreviewHostDocument: View {
enum Kind {
case notes
case messages
}
let kind: Kind
let title: String
/// Recording language for the host chrome this view draws itself
/// (everything else is passed in already localized by the caller).
var language: WhatsNewDemoScenario.Language = .zh
/// Text the keyboard has produced so far. Empty renders the placeholder.
let text: String
/// Messages only — the message being replied to.
var incoming: String?
/// Draws the send-confirmed state for the AI clip's closing beat.
var isSent: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(TypeStyle.footnote)
.foregroundStyle(Color(uiColor: .secondaryLabel))
.padding(.horizontal, 4)
switch kind {
case .notes:
notesBody
case .messages:
messagesBody
}
}
.padding(.horizontal, 18)
.padding(.top, 56)
.padding(.bottom, 12)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var notesBody: some View {
VStack(alignment: .leading, spacing: 0) {
if text.isEmpty {
Text(placeholder)
.font(.system(size: 20))
.foregroundStyle(Color(uiColor: .tertiaryLabel))
} else {
HStack(alignment: .top, spacing: 0) {
Text(text)
.font(.system(size: 20))
.foregroundStyle(Color(uiColor: .label))
.multilineTextAlignment(.leading)
caret
}
}
Spacer(minLength: 0)
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
// Text lands in a stable place instead of jumping as it grows.
.animation(.easeOut(duration: 0.2), value: text)
}
private var messagesBody: some View {
VStack(alignment: .leading, spacing: 12) {
Spacer(minLength: 0)
if let incoming, !incoming.isEmpty {
bubble(incoming, outgoing: false)
}
if !text.isEmpty {
bubble(text, outgoing: true)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
if isSent {
Text(language == .en ? "Sent" : "已发送")
.font(TypeStyle.caption.weight(.regular))
.foregroundStyle(Color(uiColor: .tertiaryLabel))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.padding(14)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
.animation(.easeOut(duration: 0.24), value: text)
.animation(.easeOut(duration: 0.24), value: isSent)
}
private func bubble(_ content: String, outgoing: Bool) -> some View {
HStack {
if outgoing { Spacer(minLength: 40) }
Text(content)
.font(.system(size: 17))
.foregroundStyle(outgoing ? Color.white : Color(uiColor: .label))
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(
outgoing
? Color(red: 0.20, green: 0.66, blue: 0.38)
: Color(uiColor: .tertiarySystemFill)
)
)
if !outgoing { Spacer(minLength: 40) }
}
}
private var caret: some View {
RoundedRectangle(cornerRadius: 1)
.fill(Color.accentColor)
.frame(width: 2, height: 24)
.padding(.leading, 1)
}
private var placeholder: String {
switch kind {
case .notes: return language == .en ? "Start writing…" : "开始记录…"
case .messages: return ""
}
}
}
#endif