fix(ipad): ship iPad P0 layout/globe fixes, edit-last-input, drop clipboard commands

Adapt typing/voice surfaces for iPad width and height, add the system globe
key and last-input editing flow, harden host-only Rime deployment, and remove
clipboard voice commands. Bump build to 61.
This commit is contained in:
Rocky
2026-08-10 13:50:50 +08:00
parent 53abad2050
commit 2bc8c1b87d
85 changed files with 5703 additions and 3997 deletions
@@ -7,7 +7,7 @@
import Foundation
public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
public static let schemaVersion = 2
public static let schemaVersion = 3
public static let kvsKey = "speechHistory.v2"
public static let legacyKVSKey = "speechHistory.v1"
public static let maxEntries = 300
@@ -28,6 +28,8 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
public var entries: [SpeechHistoryEntry]
/// Entry IDs deleted on any device, with deletion timestamps.
public var deletedEntryIDs: [UUID: Date]
/// Recent idempotency keys for keyboard-originated history mutations.
public var appliedMutationIDs: [UUID]
/// When set, entries created at or before this instant are excluded.
public var clearedAt: Date?
@@ -36,12 +38,14 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
updatedAt: Date = Date(),
entries: [SpeechHistoryEntry] = [],
deletedEntryIDs: [UUID: Date] = [:],
appliedMutationIDs: [UUID] = [],
clearedAt: Date? = nil
) {
self.schemaVersion = schemaVersion
self.updatedAt = updatedAt
self.entries = entries
self.deletedEntryIDs = deletedEntryIDs
self.appliedMutationIDs = appliedMutationIDs
self.clearedAt = clearedAt
}
@@ -58,12 +62,16 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
} else {
deletedEntryIDs = [:]
}
appliedMutationIDs = try container.decodeIfPresent(
[UUID].self,
forKey: .appliedMutationIDs
) ?? []
clearedAt = try container.decodeIfPresent(Date.self, forKey: .clearedAt)
}
public static let empty = SyncedSpeechHistory(updatedAt: .distantPast)
/// Union entries by id (newer `createdAt` wins), apply tombstones and clear.
/// Union entries by id. Higher revision wins; timestamps break legacy ties.
public static func merge(local: SyncedSpeechHistory, remote: SyncedSpeechHistory) -> SyncedSpeechHistory {
let clearedAt = later(of: local.clearedAt, and: remote.clearedAt)
var deletedIDs = local.deletedEntryIDs
@@ -75,13 +83,18 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
}
}
deletedIDs = pruneTombstones(deletedIDs, clearedAt: clearedAt)
let appliedMutationIDs = Array(
Set(local.appliedMutationIDs + remote.appliedMutationIDs)
.sorted { $0.uuidString < $1.uuidString }
.prefix(256)
)
var byID: [UUID: SpeechHistoryEntry] = [:]
for entry in local.entries + remote.entries {
if deletedIDs[entry.id] != nil { continue }
if let clearedAt, entry.createdAt <= clearedAt { continue }
if let existing = byID[entry.id] {
byID[entry.id] = entry.createdAt >= existing.createdAt ? entry : existing
byID[entry.id] = preferred(entry, over: existing)
} else {
byID[entry.id] = entry
}
@@ -96,6 +109,7 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
updatedAt: max(local.updatedAt, remote.updatedAt),
entries: entries,
deletedEntryIDs: deletedIDs,
appliedMutationIDs: appliedMutationIDs,
clearedAt: clearedAt
)
}
@@ -143,6 +157,19 @@ public struct SyncedSpeechHistory: Codable, Equatable, Sendable {
return nil
}
}
private static func preferred(
_ candidate: SpeechHistoryEntry,
over existing: SpeechHistoryEntry
) -> SpeechHistoryEntry {
if candidate.revision != existing.revision {
return candidate.revision > existing.revision ? candidate : existing
}
if candidate.modifiedAt != existing.modifiedAt {
return candidate.modifiedAt > existing.modifiedAt ? candidate : existing
}
return candidate.createdAt >= existing.createdAt ? candidate : existing
}
}
extension SyncedSpeechHistory {