feat: keyboard bottom-row layout, handedness preference, and screen wake lock
Rework the action cluster to a mic-above-bottom-row layout, add left/right handedness setting that swaps delete and return, and keep the screen awake during Flow recording sessions.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// FlowUtteranceChunkConfig.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Chunking policy for pipelined Flow utterance ASR (up to 3 minutes).
|
||||
// Chunking policy for pipelined Flow utterance ASR (up to 3.5 minutes).
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// HandednessPreference.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Which hand the user holds the phone with — controls bottom-row key order
|
||||
// on the keyboard (delete ↔ return swap for right-handed use).
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum HandednessPreference: String, CaseIterable, Identifiable, Sendable, Codable {
|
||||
case left
|
||||
case right
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public var labelKey: String {
|
||||
switch self {
|
||||
case .left: return "settings.handedness.left"
|
||||
case .right: return "settings.handedness.right"
|
||||
}
|
||||
}
|
||||
|
||||
/// Right-handed preference places return on the left and delete on the right.
|
||||
public var swapsActionKeys: Bool { self == .right }
|
||||
|
||||
public static func fromStored(_ raw: String?) -> HandednessPreference {
|
||||
guard let raw, let value = HandednessPreference(rawValue: raw) else { return .left }
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
// "on" state during init, but new writes never touch the key.
|
||||
static let translationTargetLocaleId = "config.translationTargetLocaleId"
|
||||
static let polishScenarioId = "config.polishScenarioId"
|
||||
static let handednessPreference = "config.handednessPreference"
|
||||
}
|
||||
|
||||
@Published public var providerId: String {
|
||||
@@ -163,6 +164,14 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
}
|
||||
/// Which hand the user holds the phone with — mirrors to the keyboard
|
||||
/// extension so delete / return can swap on the bottom row.
|
||||
@Published public var handednessPreference: HandednessPreference {
|
||||
didSet {
|
||||
defaults.set(handednessPreference.rawValue, forKey: Key.handednessPreference)
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the pipeline should run translate-and-polish (not just
|
||||
/// polish). Cloud engine: any selected target locale. Local engine:
|
||||
@@ -299,6 +308,9 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
self.polishScenarioId = PolishScenarioCatalog.defaultId
|
||||
}
|
||||
}
|
||||
self.handednessPreference = HandednessPreference.fromStored(
|
||||
resolvedDefaults.string(forKey: Key.handednessPreference)
|
||||
)
|
||||
|
||||
// Cloud no longer exposes off/transcribe; migrate legacy values.
|
||||
if self.engineMode == "cloud", self.modeId != "polish" {
|
||||
@@ -350,6 +362,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
|
||||
model = preset.defaultModel
|
||||
systemPrompt = AppGroupStore.defaultSystemPrompt(for: "openai")
|
||||
polishScenarioId = PolishScenarioCatalog.defaultId
|
||||
handednessPreference = .left
|
||||
hasAcknowledgedCloudSharing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public struct AppGroupStore: @unchecked Sendable {
|
||||
// computed shim for source compatibility.
|
||||
static let translationTargetLocaleId = "config.translationTargetLocaleId"
|
||||
static let polishScenarioId = "config.polishScenarioId"
|
||||
static let handednessPreference = "config.handednessPreference"
|
||||
}
|
||||
|
||||
// MARK: - Reads
|
||||
@@ -133,6 +134,11 @@ public struct AppGroupStore: @unchecked Sendable {
|
||||
return PolishScenarioCatalog.resolve(stored ?? PolishScenarioCatalog.defaultId).id
|
||||
}
|
||||
|
||||
/// Bottom-row key order on the keyboard extension.
|
||||
public var handednessPreference: HandednessPreference {
|
||||
HandednessPreference.fromStored(defaults.string(forKey: Key.handednessPreference))
|
||||
}
|
||||
|
||||
// MARK: - Writes
|
||||
|
||||
public func setModeId(_ id: String) {
|
||||
@@ -184,6 +190,11 @@ public struct AppGroupStore: @unchecked Sendable {
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
|
||||
public func setHandednessPreference(_ preference: HandednessPreference) {
|
||||
defaults.set(preference.rawValue, forKey: Key.handednessPreference)
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
|
||||
/// Whether ASR output should be sent through the cloud LLM step.
|
||||
/// Cloud engine: always. Local engine: only when cloud polish is
|
||||
/// enabled (translation is a sub-option of that step).
|
||||
|
||||
@@ -24,8 +24,8 @@ public enum FlowSessionKeys {
|
||||
/// Default Flow session length when started from the keyboard.
|
||||
public static let defaultSessionDuration: TimeInterval = 480
|
||||
|
||||
/// Maximum duration for a single keyboard utterance (3 minutes).
|
||||
public static let maxUtteranceDuration: TimeInterval = 180
|
||||
/// Maximum duration for a single keyboard utterance (3.5 minutes).
|
||||
public static let maxUtteranceDuration: TimeInterval = 210
|
||||
|
||||
/// Host polls for pipelined ASR drain after mic stop. Pipelining usually
|
||||
/// finishes most chunks during recording; this is a soft deadline before
|
||||
|
||||
@@ -103,6 +103,8 @@ public final class KeyboardState: ObservableObject {
|
||||
/// v0.2.0: mirrored from App Group — local engine runs the cloud
|
||||
/// LLM step only when this is `true`.
|
||||
@Published public var localModeCloudPolishEnabled: Bool = false
|
||||
/// Mirrored from App Group — swaps delete / return on the bottom row.
|
||||
@Published public var handednessPreference: HandednessPreference = .left
|
||||
/// Whether translate-and-polish is actually armed for the current
|
||||
/// engine (local requires cloud polish + a target locale).
|
||||
public var isTranslationEffective: Bool {
|
||||
|
||||
Reference in New Issue
Block a user