Files
OSGKeyboard/OSGKeyboardShared/Services/WhatsNewDemoScenario.swift
T
Rocky 9f308fadd2 feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
2026-08-13 01:00:51 +08:00

112 lines
4.3 KiB
Swift

// WhatsNewDemoScenario.swift
// OSGKeyboard · Shared
//
// DEBUG-only bridge: the main app arms a scenario in the App Group, then the
// real keyboard extension plays a scripted UI timeline over a Notes-like host.
// Never read in Release.
import Foundation
public enum WhatsNewDemoScenario: String, Sendable {
case edit
case ai
case clipboard
public enum Keys {
public static let scenario = "debug.whatsNew.demoScenario"
public static let seedText = "debug.whatsNew.seedText"
public static let armedAt = "debug.whatsNew.armedAt"
/// `zh` / `en` — drives demo copy; UI strings follow AppGroup `uiLanguage`.
public static let language = "debug.whatsNew.language"
/// Set while the extension timeline is running (survives consume).
public static let playing = "debug.whatsNew.playing"
}
public enum Language: String, Sendable {
case zh
case en
}
/// How long an armed scenario stays valid (avoids sticky demos).
public static let armTTL: TimeInterval = 120
public static func arm(
_ scenario: WhatsNewDemoScenario,
seedText: String,
language: Language = .zh,
defaults: UserDefaults? = AppGroup.defaultsIfAvailable
) {
guard let defaults else { return }
// Don't stomp an in-flight timeline.
guard !isPlaying(defaults: defaults) else { return }
defaults.set(scenario.rawValue, forKey: Keys.scenario)
defaults.set(seedText, forKey: Keys.seedText)
defaults.set(language.rawValue, forKey: Keys.language)
defaults.set(Date().timeIntervalSince1970, forKey: Keys.armedAt)
defaults.synchronize()
}
/// Peek without clearing — clear only after the demo timeline finishes.
public static func peek(
defaults: UserDefaults? = AppGroup.defaultsIfAvailable
) -> (scenario: WhatsNewDemoScenario, seedText: String, language: Language)? {
guard let defaults else { return nil }
guard let raw = defaults.string(forKey: Keys.scenario),
let scenario = WhatsNewDemoScenario(rawValue: raw)
else { return nil }
let armedAt = defaults.double(forKey: Keys.armedAt)
guard armedAt > 0,
Date().timeIntervalSince1970 - armedAt < armTTL
else {
clear(defaults: defaults)
return nil
}
let language = Language(rawValue: defaults.string(forKey: Keys.language) ?? "") ?? .zh
let seed = defaults.string(forKey: Keys.seedText)
?? (language == .en
? "Meeting at 3pm tomorrow to discuss the plan"
: "明天下午三点开会讨论方案")
return (scenario, seed, language)
}
public static func consume(
defaults: UserDefaults? = AppGroup.defaultsIfAvailable
) -> (scenario: WhatsNewDemoScenario, seedText: String, language: Language)? {
guard let defaults else { return nil }
guard let armed = peek(defaults: defaults) else { return nil }
// Keep `playing` so host re-arm / pasteboard capture stay suppressed.
defaults.set(true, forKey: Keys.playing)
defaults.removeObject(forKey: Keys.scenario)
defaults.removeObject(forKey: Keys.seedText)
defaults.removeObject(forKey: Keys.armedAt)
// Keep language for the in-flight timeline; cleared in finishPlaying.
defaults.synchronize()
return armed
}
public static func isPlaying(
defaults: UserDefaults? = AppGroup.defaultsIfAvailable
) -> Bool {
defaults?.bool(forKey: Keys.playing) == true
}
public static func finishPlaying(
defaults: UserDefaults? = AppGroup.defaultsIfAvailable
) {
guard let defaults else { return }
defaults.removeObject(forKey: Keys.playing)
defaults.removeObject(forKey: Keys.language)
defaults.synchronize()
}
public static func clear(defaults: UserDefaults? = AppGroup.defaultsIfAvailable) {
guard let defaults else { return }
defaults.removeObject(forKey: Keys.scenario)
defaults.removeObject(forKey: Keys.seedText)
defaults.removeObject(forKey: Keys.armedAt)
defaults.removeObject(forKey: Keys.language)
defaults.removeObject(forKey: Keys.playing)
defaults.synchronize()
}
}