feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish

Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
Rocky
2026-08-13 01:00:51 +08:00
parent fd6e0d3e7e
commit 9f308fadd2
202 changed files with 10897 additions and 5962 deletions
+81
View File
@@ -125,6 +125,61 @@ final class CloudASRTests: XCTestCase {
XCTAssertEqual(config.asrModel, CloudASRModelCatalog.alibabaFunASRRealtime)
}
func testLegacyQwenASRConfigCopiesDedicatedKeyToBailian() throws {
clearQwenMigrationKeys()
defer { clearQwenMigrationKeys() }
let suite = "group.com.osgkeyboard.tests.qwen-asr-key.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
defer { defaults.removePersistentDomain(forName: suite) }
defaults.set(false, forKey: AppGroupConfiguration.Keys.settingsICloudSyncEnabled)
defaults.set("qwen", forKey: AppGroupConfiguration.Keys.asrProviderId)
try Keychain.setASRAPIKey("dedicated-credential", for: "qwen", useICloudSync: false)
_ = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertNotNil(Keychain.asrApiKey(for: "qwen", preferICloudSync: false))
XCTAssertNotNil(Keychain.asrApiKey(for: "bailian", preferICloudSync: false))
}
func testLegacyQwenASRConfigFallsBackToProviderKey() throws {
clearQwenMigrationKeys()
defer { clearQwenMigrationKeys() }
let suite = "group.com.osgkeyboard.tests.qwen-provider-key.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
defer { defaults.removePersistentDomain(forName: suite) }
defaults.set(false, forKey: AppGroupConfiguration.Keys.settingsICloudSyncEnabled)
defaults.set("qwen", forKey: AppGroupConfiguration.Keys.asrProviderId)
try Keychain.setAPIKey("provider-credential", for: "qwen", useICloudSync: false)
_ = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertNotNil(Keychain.apiKey(for: "qwen", preferICloudSync: false))
XCTAssertNotNil(Keychain.asrApiKey(for: "bailian", preferICloudSync: false))
}
func testLegacyQwenASRConfigDoesNotOverwriteDifferentBailianKey() throws {
clearQwenMigrationKeys()
defer { clearQwenMigrationKeys() }
let suite = "group.com.osgkeyboard.tests.qwen-key-conflict.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
defer { defaults.removePersistentDomain(forName: suite) }
defaults.set(false, forKey: AppGroupConfiguration.Keys.settingsICloudSyncEnabled)
defaults.set("qwen", forKey: AppGroupConfiguration.Keys.asrProviderId)
try Keychain.setASRAPIKey("qwen-credential", for: "qwen", useICloudSync: false)
try Keychain.setASRAPIKey("bailian-credential", for: "bailian", useICloudSync: false)
_ = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertTrue(
Keychain.asrApiKey(for: "bailian", preferICloudSync: false)
== "bailian-credential"
)
XCTAssertNotNil(Keychain.asrApiKey(for: "qwen", preferICloudSync: false))
}
func testVolcengineASRFieldsJSONParsing() {
let json = #"{"app_id":"app-1","access_token":"tok-2","resource_id":"res-3"}"#
let fields = VolcengineASRFields.parse(apiKey: json, resourceFallback: "")
@@ -202,6 +257,12 @@ final class CloudASRTests: XCTestCase {
XCTAssertEqual(parsed.accessToken, "tok-2")
}
func testVolcengineASRFieldsEmptyDefaultsToAPIKeyMode() {
let fields = VolcengineASRFields.parse(apiKey: "")
XCTAssertEqual(fields.authMode, .apiKey)
XCTAssertFalse(fields.hasUsableCredentials)
}
func testVolcengineASRFieldsEmptyAPIKeyModeIsNotUsable() {
let json = #"{"auth_mode":"api_key"}"#
let fields = VolcengineASRFields.parse(apiKey: json)
@@ -252,6 +313,20 @@ final class CloudASRTests: XCTestCase {
XCTAssertEqual(String(data: wav.dropFirst(8).prefix(4), encoding: .ascii), "WAVE")
}
func testOpenAIRealtimeProbeCancellationDoesNotUseBatchFallback() {
XCTAssertFalse(
OpenAIRealtimeASRClient.shouldFallbackToBatch(afterProbeError: CancellationError())
)
XCTAssertFalse(
OpenAIRealtimeASRClient.shouldFallbackToBatch(afterProbeError: URLError(.cancelled))
)
XCTAssertTrue(
OpenAIRealtimeASRClient.shouldFallbackToBatch(
afterProbeError: CloudASRError.transport("handshake failed")
)
)
}
func testVocabularyFingerprintChangesWhenDictionaryChanges() {
let emptyFP = PersonalDictionary.empty.vocabularySyncFingerprint()
let withTerm = PersonalDictionary(entries: [
@@ -259,4 +334,10 @@ final class CloudASRTests: XCTestCase {
])
XCTAssertNotEqual(emptyFP, withTerm.vocabularySyncFingerprint())
}
private func clearQwenMigrationKeys() {
try? Keychain.deleteAPIKey(for: "qwen", useICloudSync: true)
try? Keychain.deleteASRAPIKey(for: "qwen", useICloudSync: true)
try? Keychain.deleteASRAPIKey(for: "bailian", useICloudSync: true)
}
}