Files
OSGKeyboard/OSGKeyboard/Views/MainAppRoot.swift
T
Rocky c2f07bd8d2 feat(macos): add macOS menu-bar app and harden cross-device iCloud sync
Introduce a standalone macOS menu-bar app (OSGKeyboardMac) that reuses the
platform-agnostic OSGKeyboardShared core: record -> cloud/local ASR -> polish
-> insert. Local mode uses Qwen3-ASR via mlx-swift-asr (macOS 15+, Apple
Silicon); iOS targets stay zero-SPM.

Harden iCloud sync for multi-device correctness:
- Per-field settings merge (appSettings.v2) so concurrent edits no longer
  clobber each other's unrelated fields.
- Per-device usage statistics (G-Counter) that sum instead of max().
- Tombstoned dictionary/history merge so deletes propagate and entries can't
  resurrect.
- API keys replicate via iCloud Keychain, never iCloud KVS JSON; pulling a
  legacy blob without key fields no longer wipes local Keychain entries.
- Add a low-risk "Sync Now" action in Settings.

Fix Flow keyboard mic state: stay orange until the host publishes a real ready
contract, share a single MicVoiceAvailability gate, and self-heal stale
cross-process heartbeat jitter instead of getting stuck.

Extract shared storage (SpeechHistoryStore/UsageStatisticsStore,
ConfigurationStore) into OSGKeyboardShared and add tests for the new
sync/merge logic.
2026-07-08 18:13:56 +08:00

84 lines
3.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 Groupbacked 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() },
onRetry: { flowManager.retryColdStartReadiness() },
onOpenSettings: { flowManager.openColdStartPermissionSettings() }
)
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 0.2), value: flowManager.coldStartContext != nil)
.onAppear {
flowManager.setAppForeground(scenePhase == .active)
flowManager.activateOnForeground()
AppCloudSync.shared.startObservingExternalChanges()
// Registering here also flushes any URL buffered during a cold
// launch (the keyboard → app `startflow` handoff arrives via the
// scene delegate before this view is on screen).
AppOpenURLRouter.shared.register { url in
handleIncomingURL(url)
}
Task {
await AppCloudSync.shared.pullAllIfEnabled()
}
}
.onReceive(NotificationCenter.default.publisher(for: .settingsDidSyncFromCloud)) { _ in
config.reloadFromPersistedStorage()
}
.onChange(of: config.hasCompletedOnboarding) { _, done in
if done {
flowManager.activateOnForeground()
}
}
.onChange(of: scenePhase) { _, phase in
flowManager.handleScenePhase(phase)
guard phase == .active else { return }
if config.hasCompletedOnboarding {
flowManager.activateOnForeground()
}
Task {
await AppCloudSync.shared.pullAllIfEnabled()
}
}
}
private func handleIncomingURL(_ url: URL) {
guard url.scheme == "osgkeyboard" else { return }
switch url.host {
case "startflow":
flowManager.startSession(coldStart: true)
default:
break
}
}
}