8ce84d2339
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
136 lines
5.0 KiB
Swift
136 lines
5.0 KiB
Swift
// DictationBridge.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Lightweight App Group bridge for host-app dictation handoff:
|
|
// keyboard extension -> open host app for recording
|
|
// host app -> writes final transcript
|
|
// keyboard extension -> consumes pending transcript and inserts text
|
|
//
|
|
// STATUS (v0.1.2): Retained. Consumed by `KeyboardViewController` for
|
|
// the "one-shot" host-app dictation path (where the keyboard extension
|
|
// launches the host app, the user records there, and the resulting
|
|
// text is consumed back by the extension). The *continuous* path goes
|
|
// through `FlowSessionBridge` + `FlowSessionManager` instead.
|
|
|
|
import Foundation
|
|
|
|
public enum DictationBridge {
|
|
public enum Status: String, Sendable, Equatable {
|
|
case idle
|
|
case requested
|
|
case recording
|
|
case transcribing
|
|
case done
|
|
case cancelled
|
|
case error
|
|
}
|
|
|
|
private enum Key {
|
|
static let pendingText = "dictation.pendingText"
|
|
static let polishWarning = "dictation.polishWarning"
|
|
static let updatedAt = "dictation.updatedAt"
|
|
static let status = "dictation.status"
|
|
static let statusUpdatedAt = "dictation.statusUpdatedAt"
|
|
static let statusMessage = "dictation.statusMessage"
|
|
}
|
|
|
|
private static func resolvedDefaults(_ defaults: UserDefaults?) -> UserDefaults {
|
|
if let defaults {
|
|
return defaults
|
|
}
|
|
return AppGroup.isAvailable ? AppGroup.defaults : .standard
|
|
}
|
|
|
|
public static func setStatus(
|
|
_ status: Status,
|
|
message: String? = nil,
|
|
defaults: UserDefaults? = nil
|
|
) {
|
|
let store = resolvedDefaults(defaults)
|
|
store.set(status.rawValue, forKey: Key.status)
|
|
store.set(Date().timeIntervalSince1970, forKey: Key.statusUpdatedAt)
|
|
if let message, !message.isEmpty {
|
|
store.set(message, forKey: Key.statusMessage)
|
|
} else {
|
|
store.removeObject(forKey: Key.statusMessage)
|
|
}
|
|
}
|
|
|
|
public static func currentStatus(
|
|
defaults: UserDefaults? = nil
|
|
) -> (status: Status, message: String?, updatedAt: TimeInterval) {
|
|
let store = resolvedDefaults(defaults)
|
|
let raw = store.string(forKey: Key.status) ?? Status.idle.rawValue
|
|
let status = Status(rawValue: raw) ?? .idle
|
|
let message = store.string(forKey: Key.statusMessage)
|
|
let updatedAt = store.double(forKey: Key.statusUpdatedAt)
|
|
return (status, message, updatedAt)
|
|
}
|
|
|
|
public static func markRequested(defaults: UserDefaults? = nil) {
|
|
let store = resolvedDefaults(defaults)
|
|
store.removeObject(forKey: Key.pendingText)
|
|
setStatus(.requested, defaults: store)
|
|
}
|
|
|
|
/// Store a transcript for the keyboard extension to consume.
|
|
public static func storePendingTranscript(
|
|
_ text: String,
|
|
polishWarning: String? = nil,
|
|
defaults: UserDefaults? = nil
|
|
) {
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return }
|
|
let store = resolvedDefaults(defaults)
|
|
store.set(trimmed, forKey: Key.pendingText)
|
|
store.set(Date().timeIntervalSince1970, forKey: Key.updatedAt)
|
|
if let polishWarning, !polishWarning.isEmpty {
|
|
store.set(polishWarning, forKey: Key.polishWarning)
|
|
} else {
|
|
store.removeObject(forKey: Key.polishWarning)
|
|
}
|
|
setStatus(.done, defaults: store)
|
|
}
|
|
|
|
/// Returns and clears the pending transcript if present.
|
|
public static func consumePendingTranscript(
|
|
maxAge: TimeInterval = 180,
|
|
defaults: UserDefaults? = nil
|
|
) -> String? {
|
|
consumePendingDelivery(maxAge: maxAge, defaults: defaults)?.text
|
|
}
|
|
|
|
/// Returns and clears the pending delivery (text + optional polish
|
|
/// warning) if present.
|
|
public static func consumePendingDelivery(
|
|
maxAge: TimeInterval = 180,
|
|
defaults: UserDefaults? = nil
|
|
) -> TranscriptionDelivery? {
|
|
let store = resolvedDefaults(defaults)
|
|
guard let text = store.string(forKey: Key.pendingText) else {
|
|
return nil
|
|
}
|
|
if maxAge > 0 {
|
|
let ts = store.double(forKey: Key.updatedAt)
|
|
if ts > 0, Date().timeIntervalSince1970 - ts > maxAge {
|
|
clear(defaults: store)
|
|
return nil
|
|
}
|
|
}
|
|
let warning = store.string(forKey: Key.polishWarning)
|
|
store.removeObject(forKey: Key.pendingText)
|
|
store.removeObject(forKey: Key.polishWarning)
|
|
store.removeObject(forKey: Key.updatedAt)
|
|
setStatus(.idle, defaults: store)
|
|
return TranscriptionDelivery(text: text, polishWarning: warning)
|
|
}
|
|
|
|
public static func clear(defaults: UserDefaults? = nil) {
|
|
let store = resolvedDefaults(defaults)
|
|
store.removeObject(forKey: Key.pendingText)
|
|
store.removeObject(forKey: Key.polishWarning)
|
|
store.removeObject(forKey: Key.updatedAt)
|
|
setStatus(.idle, defaults: store)
|
|
}
|
|
}
|