feat: complete Phase 4 batch F, keyboard i18n, and Pages app icon
Remove KeyboardL10n hard-coded fallbacks in favor of ExtL10n and extension Localizable.strings. Add Flow session expiry hints, Darwin cross-process notifications, session monitor on the keyboard, and app icon on GitHub Pages.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// FlowSessionDarwin.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Cross-process Darwin notification when the host app changes Flow session
|
||||
// state (start, extend, end). Keyboard extension listens without polling alone.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum FlowSessionDarwin {
|
||||
public static let notificationName = "com.osgkeyboard.flow.session.changed"
|
||||
|
||||
public static func postSessionChanged() {
|
||||
CFNotificationCenterPostNotification(
|
||||
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
CFNotificationName(notificationName as CFString),
|
||||
nil,
|
||||
nil,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Observes Flow session Darwin notifications on a background thread; invokes
|
||||
/// `handler` on the main actor.
|
||||
public final class FlowSessionDarwinObserver {
|
||||
private final class Box: @unchecked Sendable {
|
||||
let handler: @MainActor () -> Void
|
||||
init(handler: @escaping @MainActor () -> Void) { self.handler = handler }
|
||||
}
|
||||
|
||||
private let box: Box
|
||||
private let token: UnsafeMutableRawPointer
|
||||
|
||||
public init(handler: @escaping @MainActor () -> Void) {
|
||||
let box = Box(handler: handler)
|
||||
self.box = box
|
||||
self.token = Unmanaged.passRetained(box).toOpaque()
|
||||
|
||||
CFNotificationCenterAddObserver(
|
||||
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
token,
|
||||
{ _, observer, _, _, _ in
|
||||
guard let observer else { return }
|
||||
let box = Unmanaged<Box>.fromOpaque(observer).takeUnretainedValue()
|
||||
Task { @MainActor in box.handler() }
|
||||
},
|
||||
FlowSessionDarwin.notificationName as CFString,
|
||||
nil,
|
||||
.deliverImmediately
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
CFNotificationCenterRemoveObserver(
|
||||
CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
token,
|
||||
CFNotificationName(FlowSessionDarwin.notificationName as CFString),
|
||||
nil
|
||||
)
|
||||
Unmanaged<Box>.fromOpaque(token).release()
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,8 @@ public final class KeyboardState: ObservableObject {
|
||||
@Published public var onDeviceSupported: Bool = false
|
||||
/// Seconds remaining in the current utterance (Flow tap-to-talk).
|
||||
@Published public var utteranceRemainingSeconds: Int = Int(FlowSessionKeys.maxUtteranceDuration)
|
||||
/// Whether the host app's Flow voice session is currently valid.
|
||||
@Published public var flowSessionActive: Bool = false
|
||||
/// "local" → ASR only, no LLM. "cloud" → ASR + optional LLM polish.
|
||||
@Published public var engineMode: String = "cloud"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user