9ef0196d52
- fix(keyboard): drop unusable extensionContext.open; report the real result of UIApplication.open and cancel the start watchdog immediately on failure with clear "open OSGKeyboard" guidance instead of a 30s spin - feat(capture): recover from audio route changes (AirPods/wired headset plug-unplug) and phone-call interruptions by rebuilding the engine/tap against the live hardware format - feat(home): add a "Start" button in the Home footer when a session is inactive and permissions are granted (no-jump manual restart) - feat(hostapps): expand the return whitelist to 46 apps (Signal, Zoom, Instagram, X, Douyin, Zhihu, Bilibili, Things, Firefox, ...) with en/zh-Hans names and synced LSApplicationQueriesSchemes - feat(island): use a "ready" checkmark instead of a mic glyph while idle and set a staleDate so an orphaned Live Activity fades after a force-quit - feat(coldstart): use the template OSG brand mark instead of a system waveform glyph in the cold-start overlay - perf(keyboard): rely on Darwin notifications as the primary session signal and drop the fallback poll from 1s to 3s - fix(keyboard): remove the redundant "Start" text button next to the inactive-session hint on the keyboard - docs: correct the per-take cap to 210s (3.5 min) and document that force-quitting no longer resurrects the session (tracker + READMEs)
68 lines
2.3 KiB
Swift
68 lines
2.3 KiB
Swift
// MainAppRoot.swift
|
||
// OSGKeyboard · Main App
|
||
//
|
||
// Host-app shell that owns `ProviderConfig` and `FlowSessionManager`.
|
||
// Only constructed when `AppGroup.isAvailable` so the error path never
|
||
// touches App Group–backed singletons.
|
||
|
||
import SwiftUI
|
||
import OSGKeyboardShared
|
||
|
||
struct MainAppRoot: View {
|
||
@Environment(\.scenePhase) private var scenePhase
|
||
|
||
@StateObject private var config = ProviderConfig.shared
|
||
@StateObject private var flowManager = FlowSessionManager()
|
||
|
||
var body: some View {
|
||
Group {
|
||
if config.hasCompletedOnboarding {
|
||
MainTabView()
|
||
} else {
|
||
OnboardingView(config: config)
|
||
}
|
||
}
|
||
.environment(\.locale, config.uiLanguage.swiftUILocale)
|
||
.environmentObject(flowManager)
|
||
.overlay {
|
||
if let context = flowManager.coldStartContext {
|
||
FlowColdStartOverlay(
|
||
context: context,
|
||
onReturnToHost: { flowManager.returnToPendingHostFromColdStart() },
|
||
onDismiss: { flowManager.dismissColdStartOverlay() }
|
||
)
|
||
.transition(.opacity)
|
||
}
|
||
}
|
||
.animation(.easeInOut(duration: 0.2), value: flowManager.coldStartContext != nil)
|
||
.onAppear {
|
||
flowManager.setAppForeground(scenePhase == .active)
|
||
flowManager.activateOnForeground()
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: .osgKeyboardOpenURL)) { notification in
|
||
guard let url = notification.userInfo?["url"] as? URL else { return }
|
||
handleIncomingURL(url)
|
||
}
|
||
.onChange(of: config.hasCompletedOnboarding) { _, done in
|
||
if done {
|
||
flowManager.activateOnForeground()
|
||
}
|
||
}
|
||
.onChange(of: scenePhase) { _, phase in
|
||
flowManager.handleScenePhase(phase)
|
||
guard phase == .active, config.hasCompletedOnboarding else { return }
|
||
flowManager.activateOnForeground()
|
||
}
|
||
}
|
||
|
||
private func handleIncomingURL(_ url: URL) {
|
||
guard url.scheme == "osgkeyboard" else { return }
|
||
switch url.host {
|
||
case "startflow":
|
||
flowManager.startSession(coldStart: true)
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
}
|