fix(输入法): 优化双端联想排序并消除文字刷新闪烁

这个提交包含在:
sydxky 于 2026-11-14 18:39:54 +08:00 提交
1 父节点 bce6f8083b
当前提交 2d2f6890f5
31 个文件已更改
+504 -124

没有与搜索条件相匹配的文件

+31 -25
查看文件
@@ -1,6 +1,6 @@
{ {
"project": "蟹蟹输入 RTOS / Android手表适配改版", "project": "蟹蟹输入 RTOS / Android手表适配改版",
"version": "1.2.2", "version": "1.2.3",
"license": "MIT", "license": "MIT",
"android": true, "android": true,
"independent_history": true, "independent_history": true,
@@ -9,7 +9,7 @@
".gitattributes": "8e689518e5810501eb34583fa369475902805d8dd6c3ea78699469de4fa22932", ".gitattributes": "8e689518e5810501eb34583fa369475902805d8dd6c3ea78699469de4fa22932",
".gitignore": "cf35fe4d5c1c2ca6ae6bd343642fee3d04cd8d3799510a02ca94df73a2d4daad", ".gitignore": "cf35fe4d5c1c2ca6ae6bd343642fee3d04cd8d3799510a02ca94df73a2d4daad",
"android/.gitignore": "9907f78ab6ad68ac162f15ed5554c0ea9bafecf6eb25f2388ba0c11d40fab35d", "android/.gitignore": "9907f78ab6ad68ac162f15ed5554c0ea9bafecf6eb25f2388ba0c11d40fab35d",
"android/app/build.gradle": "c87ab8f2b5fd4dee8faa613d9f82fbae92d3a4496d9a54eae6980d2380546784", "android/app/build.gradle": "57f1fbc4c9b4bf2b6e85db2977e81b79544a18d8c8c28c26ef9f2aaab25d641a",
"android/app/proguard-rules.pro": "41e72ccc70b31f2da13fc597d5196590ec25e0422c2766e1555f3348d64ac6ed", "android/app/proguard-rules.pro": "41e72ccc70b31f2da13fc597d5196590ec25e0422c2766e1555f3348d64ac6ed",
"android/app/proguard-test.pro": "dc5f87f1ec90eb24823174f80d19e93d3e69919af70891529e4c54a744988aa4", "android/app/proguard-test.pro": "dc5f87f1ec90eb24823174f80d19e93d3e69919af70891529e4c54a744988aa4",
"android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java": "d5a7284b6693ee116a9e5450b4c0e38df2a71812c79664ee04311d8088db33ac", "android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java": "d5a7284b6693ee116a9e5450b4c0e38df2a71812c79664ee04311d8088db33ac",
@@ -24,35 +24,39 @@
"android/gradle.properties": "1a62f4c0a2a08044c706c378451342a11b307842b6b2ad434a86d6f970acc201", "android/gradle.properties": "1a62f4c0a2a08044c706c378451342a11b307842b6b2ad434a86d6f970acc201",
"android/gradlew": "80a4c62723bb67cbabe92c37606d380089c647184afa1bf2b8c136f0e15d2cd6", "android/gradlew": "80a4c62723bb67cbabe92c37606d380089c647184afa1bf2b8c136f0e15d2cd6",
"android/gradlew.bat": "0e82fa239f8f4dca4efc83863c16c3b143cb6faab6f50cbefab4ee0bd03e5b5f", "android/gradlew.bat": "0e82fa239f8f4dca4efc83863c16c3b143cb6faab6f50cbefab4ee0bd03e5b5f",
"android/host/build.gradle": "56d136fa6600a94cbcb3eaca1c9a8ce68451b3500aef386af2d5783307f808f5", "android/host/build.gradle": "8ddfb0f5f4b25e64c8c7d2b4da057445c47939b3ed4b56fceacf7f0ae966c722",
"android/host/src/main/AndroidManifest.xml": "2013ca2761bc091216dca86d6b14987147e169902c4e784a2a047d473a7c6c95", "android/host/src/main/AndroidManifest.xml": "2013ca2761bc091216dca86d6b14987147e169902c4e784a2a047d473a7c6c95",
"android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java": "0b73b7b5b6286e0a00636a13ec14b81efe37898d68d8f89cd74db266aa34da83", "android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java": "0b73b7b5b6286e0a00636a13ec14b81efe37898d68d8f89cd74db266aa34da83",
"android/keyboard/build.gradle": "e0c80feef0b7434ed4fa9889c5d2fbd373aa1634f9ef63229e98e7aed4df8d36", "android/keyboard/build.gradle": "512edae398ac76078488f9d2439e23032029b44b4d1bbbbe266d9e2d11effedc",
"android/keyboard/consumer-rules.pro": "74ec127457bb863b30cc41be93b5f6b1eb003783addb2cda36b3fa1cf7e80432", "android/keyboard/consumer-rules.pro": "74ec127457bb863b30cc41be93b5f6b1eb003783addb2cda36b3fa1cf7e80432",
"android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java": "73343a906f5ab36f465d587d8d47f3391e4513435ce59e4a4ffc1208445868c3",
"android/keyboard/src/main/AndroidManifest.xml": "343d5be1ac17cdd45d1a2f2ddc616c509290e655869125a9422a96d88aa66066", "android/keyboard/src/main/AndroidManifest.xml": "343d5be1ac17cdd45d1a2f2ddc616c509290e655869125a9422a96d88aa66066",
"android/keyboard/src/main/assets/dictionary-manifest.json": "2b3d4d5564164700b46729394bfbd94349ee4a2cc949f608ec87446a7c613da3", "android/keyboard/src/main/assets/dictionary-manifest.json": "aab6bf005ec08396e877163c4edb835299946b99362df97ab32ded80eb3e78a4",
"android/keyboard/src/main/assets/licenses/CrabInput-LICENSE": "027e84ad11fd2992040da0aad2ece7a74fdf61540dc2d74cc8a281fd76eb942c", "android/keyboard/src/main/assets/licenses/CrabInput-LICENSE": "027e84ad11fd2992040da0aad2ece7a74fdf61540dc2d74cc8a281fd76eb942c",
"android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", "android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab",
"android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", "android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775",
"android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt": "cf234c4188d773406f640bf89be680d9288f40ff96901c70d643fcea96ad46df", "android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt": "cf234c4188d773406f640bf89be680d9288f40ff96901c70d643fcea96ad46df",
"android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", "android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37",
"android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java": "9b14e96b13150b5ced15188d0eb1b6e09939f8c2c4affc8aa52147aebacf0415", "android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java": "ce0ed27cb8d69d14595f6e3cbc0ac27bfac3245b8fb7f1562540054a98c81e37",
"android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java": "a5071ddec3cee9899b80181ef1f35c6896cdd1455df37839b600a33c29ea640e", "android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java": "a5071ddec3cee9899b80181ef1f35c6896cdd1455df37839b600a33c29ea640e",
"android/keyboard/src/main/java/com/sydxky/crabinput/CandidateRanking.java": "dbd1e7cbda98225eb2f9b1361508a8f22f403c6a9a8690cc64dec34ffd0c78fc",
"android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java": "1f03cda3833ba282d12eaf66ddcbb5f4354bb8b99a1d2c9de1b4f430c9effcac", "android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java": "1f03cda3833ba282d12eaf66ddcbb5f4354bb8b99a1d2c9de1b4f430c9effcac",
"android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "014dcf83e2d1982299558204893cbd444db424ada4c2b2216585a46e7eaab47d", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "8427a7d3dbee68031ee5964ad7f22cf64d94cf70049b7e05ae2ec2aca63fade8",
"android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java": "d11bc8a3ce09980eee77a4258a858ff0c38292ce55900f00d29afb20f2fd7715", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java": "e5253607c117d18084ca1167868b3ef7ef0f28529700b70507c70af531d17291",
"android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "75fcfd7d96b8d34445d233144063893d331abe0cc1fb0b9debf9866a461e4d22", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "e9c8b43608a634c35cd942672d6da7f96e710ee35e4d3050ad110eff7a7d0c69",
"android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java": "9c21767b579f6f0436175d29bb2c2244958e214d76a1b6e43ccf986dee4c7f02", "android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java": "a0e0909184bdb97b5fd8773d6f32b1ae9dee8c96ce589c37d6cabdbcfbb63cbd",
"android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java": "7c0447f5eebea944df38179aafd789c4f2eae25ba53aff5ba9e92a2d46044c64", "android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java": "7c0447f5eebea944df38179aafd789c4f2eae25ba53aff5ba9e92a2d46044c64",
"android/keyboard/src/test/java/com/sydxky/crabinput/CandidateRankingTest.java": "3e163c8137829de2f26fa737b4a983f0bb4544c0598f2b635e3ddc55631bcb74",
"android/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "android/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af",
"android/NOTICE.md": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", "android/NOTICE.md": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab",
"android/README.md": "f559f361489b20cc33cdbbb7e9930a9b13c2f0f8afc57508e7f9040b3ab0b76c", "android/README.md": "b20dba67d7e314a4db1d9124a7dd0f230c3dd05f6a904fceb83cc4220df980ec",
"android/scripts/build-dictionary.py": "a8c29b57e42e53c5cc1e5ca6471db2ee19892db424701b1ae1cd523d2dfd0e62", "android/scripts/build-dictionary.py": "4643f5422f2383a56d8c0653e47e3fc6b02481f065efd924958569fb48435969",
"android/scripts/capture-device.py": "34023ff4fb0ff80a8811b1222aad3b58c70b628333edb56efffa3e6adaf05ace", "android/scripts/capture-device.py": "34023ff4fb0ff80a8811b1222aad3b58c70b628333edb56efffa3e6adaf05ace",
"android/scripts/validate-device.py": "f07227f15c8779666cc59ad0f598f286714e5251d59ec506b594348cf3622fb9", "android/scripts/validate-device.py": "c520bbeed36106dd1cc3c237d1f94b4adb933884ea47582bb4278128650f3d5f",
"android/settings.gradle": "73a5f1e8c25c44b0c5f983ed822c4256dea33b14b8d53115ed3be118aeaaee1d", "android/settings.gradle": "73a5f1e8c25c44b0c5f983ed822c4256dea33b14b8d53115ed3be118aeaaee1d",
"android/VALIDATION_1.2.0.md": "7ce04837f631f9e491ecfb96e8e056c4b01c4aa2f24a061d540c5280201848e6", "android/VALIDATION_1.2.0.md": "7ce04837f631f9e491ecfb96e8e056c4b01c4aa2f24a061d540c5280201848e6",
"android/VALIDATION_1.2.2.md": "6d3ebe3f107099516d2dbcae025e41d516ba8d4de4a40fc9af3342ba9ac9d345", "android/VALIDATION_1.2.2.md": "6d3ebe3f107099516d2dbcae025e41d516ba8d4de4a40fc9af3342ba9ac9d345",
"android/VALIDATION_1.2.3.md": "603ed3903589f4718892ee4211d59c563c638ea3547c28df27f5eef04651bd33",
"build-rtos-game.ps1": "a0cf9d6fa5ba1ecc22e768373dcca2f87e1e58fc44f0a612b59c7215fb064df8", "build-rtos-game.ps1": "a0cf9d6fa5ba1ecc22e768373dcca2f87e1e58fc44f0a612b59c7215fb064df8",
"com.sydxky.rtos.imefixture/build.ps1": "37cd2f3af83d9c1cb2f1d855d0d95cf2e822ac9e743c8370237c48c4a4d8c0a0", "com.sydxky.rtos.imefixture/build.ps1": "37cd2f3af83d9c1cb2f1d855d0d95cf2e822ac9e743c8370237c48c4a4d8c0a0",
"com.sydxky.rtos.imefixture/glyphix.config.js": "d53b4b0e8597d9f7fb5fd3d53063d285b245669021527a1bfc3bc054c5122514", "com.sydxky.rtos.imefixture/glyphix.config.js": "d53b4b0e8597d9f7fb5fd3d53063d285b245669021527a1bfc3bc054c5122514",
@@ -67,10 +71,10 @@
"com.sydxky.rtos.inputmethod/KEYBOARD_14.md": "27ae1fa73dd8fa8fce70743fa1e418b4a4ff71ba610e735f009344896adfa754", "com.sydxky.rtos.inputmethod/KEYBOARD_14.md": "27ae1fa73dd8fa8fce70743fa1e418b4a4ff71ba610e735f009344896adfa754",
"com.sydxky.rtos.inputmethod/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "com.sydxky.rtos.inputmethod/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af",
"com.sydxky.rtos.inputmethod/NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", "com.sydxky.rtos.inputmethod/NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999",
"com.sydxky.rtos.inputmethod/PRIVACY.md": "c3062d8434db2fad3b6824cf2c41561a0bf6ae05e62f05d7c6316efd64d74af9", "com.sydxky.rtos.inputmethod/PRIVACY.md": "19049b0bb30add6fa5071ec16e9494f835e04ee1dad83108bfec34c71e16d679",
"com.sydxky.rtos.inputmethod/README.md": "a22425e93ad54ffa426a35883035bc70284e7f066b7c8eb452d403f0dec9d35b", "com.sydxky.rtos.inputmethod/README.md": "837b66afd2b838a54c3940e1829300213eb6bbcbce7506fde90548deae5b2d8f",
"com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704", "com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704",
"com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "83eea8c7adae0254eb69170dec905c285c09c43ffcf10e8e553beb108acf43b3", "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "bf8110a215357fdf1020bf633cff9c7bb7ac155510b75e884b63e0698ecfef84",
"com.sydxky.rtos.inputmethod/scripts/install.mjs": "436a5560a54a56fbf923a370b7ec40b9d9670b1a8d3ef5d9b10e3603d71779ba", "com.sydxky.rtos.inputmethod/scripts/install.mjs": "436a5560a54a56fbf923a370b7ec40b9d9670b1a8d3ef5d9b10e3603d71779ba",
"com.sydxky.rtos.inputmethod/src/app.js": "e51c7e0a67563bd64b6bc04aedde22842295824432942bc8f1ab0d4731cc3ed3", "com.sydxky.rtos.inputmethod/src/app.js": "e51c7e0a67563bd64b6bc04aedde22842295824432942bc8f1ab0d4731cc3ed3",
"com.sydxky.rtos.inputmethod/src/assets/crab-keyboard-license.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", "com.sydxky.rtos.inputmethod/src/assets/crab-keyboard-license.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775",
@@ -85,16 +89,17 @@
"com.sydxky.rtos.inputmethod/src/assets/ime-icons/keyboard-box-line.png": "bf4b45e2e54b4fd7e901068df1de865334f7e4616583dc5358d35d14890948af", "com.sydxky.rtos.inputmethod/src/assets/ime-icons/keyboard-box-line.png": "bf4b45e2e54b4fd7e901068df1de865334f7e4616583dc5358d35d14890948af",
"com.sydxky.rtos.inputmethod/src/assets/ime-icons/LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", "com.sydxky.rtos.inputmethod/src/assets/ime-icons/LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
"com.sydxky.rtos.inputmethod/src/assets/jieba-license.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", "com.sydxky.rtos.inputmethod/src/assets/jieba-license.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37",
"com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "80e70d69561e424f4e42d5b3f33880bfa87df4d04119eb4f49b46b2dde4dce3d", "com.sydxky.rtos.inputmethod/src/common/association-seeds.json": "181aa6f3bd77cf2f1002e405cbbfd5240f3c75a560f1a79c42cca63b70f05b58",
"com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "528f6c94ccee5e0a226d8420d8fc8df8d221047f1c3259d61e80e4e98652b744",
"com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "93525dffabcca0620acc36defd077ab6677b1032ef67faf695f3fbf3116b8a64", "com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "93525dffabcca0620acc36defd077ab6677b1032ef67faf695f3fbf3116b8a64",
"com.sydxky.rtos.inputmethod/src/common/ime-theme.js": "cd4fb2392585c1c93cab1077d37fcced34ecb67449becfffbc0be3a3833f8538", "com.sydxky.rtos.inputmethod/src/common/ime-theme.js": "2e10201035adc278d32d4eb9cb42411e7e79e36631de9fe4cab0799ab790ac2e",
"com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "a845000679353d243381c91d65108d895f87ae931bb02a4bee3f1ea3451974ee", "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "d5a8e6ccf6622d77e94d4bb9b81e4faca403030648bb6e0d7957ce0593b5ceec",
"com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca", "com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca",
"com.sydxky.rtos.inputmethod/src/manifest.json": "065e5fb6e1e5898195fd117bcf905f002ee54ffd1d72e02dc0f9e4c50924bfab", "com.sydxky.rtos.inputmethod/src/manifest.json": "08dfbe892bcaad5e6d4cd176c5fea6178749557d2b45868991c3ea7a39e1696d",
"com.sydxky.rtos.inputmethod/tests/client.test.mjs": "a1c524b3d4ff5c49cfd599d2b1111e585a67dfdccf9612fd3f4a312fc1841076", "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "0a85252cd2548223f99e6582e3075dab1fe1a2ec88f3bb6d16593c2914878666",
"com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "86408d7629f189a11327ee801e75e472414d3a7979db60c12772debcf447ce23", "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "50f416bbbcaae1b867e3b88331ee2c27a95ad7315754d103e6684a223717a40a",
"com.sydxky.rtos.inputmethod/tests/layout.py": "9b1fc238cae2f39e5385611cb3d5d636e40941cd4566c51d1407cfb72d6dbcff", "com.sydxky.rtos.inputmethod/tests/layout.py": "9b1fc238cae2f39e5385611cb3d5d636e40941cd4566c51d1407cfb72d6dbcff",
"com.sydxky.rtos.inputmethod/tests/native.py": "307d14506af92388c044e30e8095ac4640b710d988ec10d31f3eecd1c9a9a093", "com.sydxky.rtos.inputmethod/tests/native.py": "31eaf7a0b898d575866f0df7bd54a0ffe2c6f96ec915633eb63f52359441bfe5",
"com.sydxky.rtos.inputmethod/tests/visual.py": "25bb14687278b6961e1e4957dafdb28b95d1056fdbe58e6506bca853cd5f1fec", "com.sydxky.rtos.inputmethod/tests/visual.py": "25bb14687278b6961e1e4957dafdb28b95d1056fdbe58e6506bca853cd5f1fec",
"com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775",
"com.sydxky.rtos.inputmethod/third_party/crabKeyboard/lookupDictV5.original.js": "915605439ebc404d808ccce6cb18abfff3981f1cdf53a78e9a743c78c4a6ddc9", "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/lookupDictV5.original.js": "915605439ebc404d808ccce6cb18abfff3981f1cdf53a78e9a743c78c4a6ddc9",
@@ -165,6 +170,7 @@
"com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md": "3b7346853bf6679c4efeda0568caece9ecb28dfde6347c96a67dc245a1e08ab9", "com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md": "3b7346853bf6679c4efeda0568caece9ecb28dfde6347c96a67dc245a1e08ab9",
"com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md": "db7cf247579c3f6be73eaecf75fc9553214df8120ee25dc7d51c402a34ea89d6", "com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md": "db7cf247579c3f6be73eaecf75fc9553214df8120ee25dc7d51c402a34ea89d6",
"com.sydxky.rtos.inputmethod/VALIDATION_1.2.2.md": "21792c32b065505b2b81ebbbcf7127958fbbb0d791fa121b84cd4e384ac05221", "com.sydxky.rtos.inputmethod/VALIDATION_1.2.2.md": "21792c32b065505b2b81ebbbcf7127958fbbb0d791fa121b84cd4e384ac05221",
"com.sydxky.rtos.inputmethod/VALIDATION_1.2.3.md": "2311037bd6901906318dd67882e5adc5055e45086a69408f66fdf8b050f39be7",
"docs/screenshots/android-candidates-16.png": "d244a3fb41300a7e8c478930307cfb3d4e781f016be53a9e93615bb79717ae73", "docs/screenshots/android-candidates-16.png": "d244a3fb41300a7e8c478930307cfb3d4e781f016be53a9e93615bb79717ae73",
"docs/screenshots/android-english.png": "adbc1ede8f2103a6435aa04efe604737895365ff3f53e75fcb9511b9a1f4f551", "docs/screenshots/android-english.png": "adbc1ede8f2103a6435aa04efe604737895365ff3f53e75fcb9511b9a1f4f551",
"docs/screenshots/android-fourteen.png": "88b06bdd1e6a6dc6f45722e01ce7930c94845062b8bf237aad5c5d086f37641c", "docs/screenshots/android-fourteen.png": "88b06bdd1e6a6dc6f45722e01ce7930c94845062b8bf237aad5c5d086f37641c",
@@ -183,11 +189,11 @@
"docs/screenshots/rtos-round-en.png": "b94f17f121d165c33f84b0eaa4c1ca48b2dbf9890bd1ab77147965fc138de6c2", "docs/screenshots/rtos-round-en.png": "b94f17f121d165c33f84b0eaa4c1ca48b2dbf9890bd1ab77147965fc138de6c2",
"docs/screenshots/rtos-round-num.png": "d7fcf451229861fb564b45b6ada03501aed57cf9ca5c058833b9b65dbace06eb", "docs/screenshots/rtos-round-num.png": "d7fcf451229861fb564b45b6ada03501aed57cf9ca5c058833b9b65dbace06eb",
"docs/screenshots/rtos-round-phrase-candidates.png": "507d09901681bbeae4da39fa0a700663d89b7149f7fab01dd603630c932b4dc4", "docs/screenshots/rtos-round-phrase-candidates.png": "507d09901681bbeae4da39fa0a700663d89b7149f7fab01dd603630c932b4dc4",
"docs/SCREENSHOTS.md": "b2b1af7717990312fe6aa56d5a75653a533bc695c5f048888dce64fb0d576c56", "docs/SCREENSHOTS.md": "e027e27a0838b83bda0369c1c3c986ec3635be2d6c8550ec11b8196424f0762d",
"docs/UI_PREVIEW.png": "a8fe477bfbc7efba2affdaa7f7314c773c123191a1fc0d04c2eae294b4aaa492", "docs/UI_PREVIEW.png": "a8fe477bfbc7efba2affdaa7f7314c773c123191a1fc0d04c2eae294b4aaa492",
"LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af",
"NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", "NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999",
"README.md": "32458e78cde7c95ffd91acde24c15f3d41a5b116b5d52682c4b883c3a0b7f176", "README.md": "cc049c63848a649e5ba976045fb3a424c7839e3e22f7b11763d5e2c44356a9e1",
"tests/emulator-validate.py": "2a549e3c7bc177b942826425e548f68db39a32d8a55ade080f0d8ab3e9c00060" "tests/emulator-validate.py": "2a549e3c7bc177b942826425e548f68db39a32d8a55ade080f0d8ab3e9c00060"
} }
} }
+13 -9
查看文件
@@ -4,13 +4,17 @@
**MIT 开源,允许商用、修改、再分发和闭源集成。** 使用时请保留 [LICENSE](LICENSE) 和 [第三方许可](NOTICE.md)。厂商 SDK、运行时与设备固件不包含在本项目授权中。 **MIT 开源,允许商用、修改、再分发和闭源集成。** 使用时请保留 [LICENSE](LICENSE) 和 [第三方许可](NOTICE.md)。厂商 SDK、运行时与设备固件不包含在本项目授权中。
![1.2.2 实际模拟器与手表界面](docs/UI_PREVIEW.png) ![历史界面,版本标记见截图说明](docs/UI_PREVIEW.png)
## 1.2.2 间距修复 ## 1.2.3 联想与稳定刷新
RTOS 去掉文字与键盘之间自动居中产生的空隙;Android 英文、数字、符号模式去掉未使用的候选栏空位。圆屏同步修正按键边角和工具页末排边界。四配置构建、普通圆/方24个原生视觉状态、Android新间距检查通过;Android整组测试6/9通过,三项受共享设备切换干扰,按用户要求不重跑。QuickApp原生视觉未完成,RTOS真机未验收。 - 全拼、九宫、14键支持未完成拼音补全,按完整匹配、补全、较短前缀分层,结合词频及当前上下文排序;原始候选完整保留。
- 输入词语后提供离线续写,例如“你好→世界”;使用光标前最多4个连续汉字,最多8个联想,不保存上下文,密码不参与。
- Android复用文字与按键控件并合并同帧刷新;RTOS查询期间保留候选,旧候选在新结果到达前不能提交。
[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.2.md) · [本版Android验收](android/VALIDATION_1.2.2.md) RTOS 53项引擎、42项UX及四配置构建通过。Android 7项Java测试及OWW261后台原生组件3项通过,包括24帧非空绘制、控件复用、补全/续写和密码隔离。本版未重跑前台交互;RTOS原生Inspector被其他任务占用,未接管。截图为明确标记的历史1.2.2界面,不冒充本版视觉验收。
[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.3.md) · [本版Android验收](android/VALIDATION_1.2.3.md)
## 1.2.0 已有功能 ## 1.2.0 已有功能
@@ -25,8 +29,8 @@ RTOS 去掉文字与键盘之间自动居中产生的空隙;Android 英文、
| 平台 | 接入与产物 | 已完成的验证 | | 平台 | 接入与产物 | 已完成的验证 |
| --- | --- | --- | | --- | --- | --- |
| Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp 四配置构建;普通圆/方 24 个原生视觉状态及间距/边界检查 | | Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp四配置构建;53项引擎和42项UX脚本断言;原生视觉未重跑 |
| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261 新间距测试通过;整组6/9,三项受共享设备切换干扰;系统默认输入法保持不变 | | Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261后台原生组件3项通过;24帧非空绘制及控件复用;前台交互未重跑 |
Android 版本作为**应用内公开组件**,由每个应用自行接入;组件不注册系统输入法。RTOS 页面也需宿主接入,系统级注册依赖厂商支持。 Android 版本作为**应用内公开组件**,由每个应用自行接入;组件不注册系统输入法。RTOS 页面也需宿主接入,系统级注册依赖厂商支持。
@@ -34,7 +38,7 @@ Android 版本作为**应用内公开组件**,由每个应用自行接入;
## 下载、源码与反馈 ## 下载、源码与反馈
- [v1.2.2 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.2):AAR、演示 APK、两屏 RTOS QuickApp、验收截图及 SHA-256。 - [v1.2.3 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.3):AAR、演示 APK、两屏 RTOS QuickApp、验收截图及 SHA-256。
- [Android 接入文档](android/README.md) · [RTOS 接入文档](com.sydxky.rtos.inputmethod/README.md) - [Android 接入文档](android/README.md) · [RTOS 接入文档](com.sydxky.rtos.inputmethod/README.md)
- [实际界面截图](docs/SCREENSHOTS.md) · [RTOS 验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md) · [Android 真机验收](android/VALIDATION_1.2.0.md) - [实际界面截图](docs/SCREENSHOTS.md) · [RTOS 验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md) · [Android 真机验收](android/VALIDATION_1.2.0.md)
- [已有问题](https://git2.sydxky.cn/sydxky/crab-input-rtos/issues) · [免注册提交问题与建议](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos) - [已有问题](https://git2.sydxky.cn/sydxky/crab-input-rtos/issues) · [免注册提交问题与建议](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)
@@ -75,7 +79,7 @@ CrabKeyboard.Binding keyboard = CrabKeyboard.attach(this, editText);
& .\com.sydxky.rtos.inputmethod\build.ps1 & .\com.sydxky.rtos.inputmethod\build.ps1
``` ```
脚本生成词库分片,执行 44 项引擎、32 项 UX 断言,并构建普通/QuickApp 四配置。每屏 QuickApp 都须保留 `bundle.pkg`、`icon.png`、`manifest.json` 三个文件。 脚本生成词库分片,执行 53 项引擎、42 项 UX 断言,并构建普通/QuickApp 四配置。每屏 QuickApp 都须保留 `bundle.pkg`、`icon.png`、`manifest.json` 三个文件。
复现原生验收时先构建最小参考页并启动一次圆屏模拟器,获得厂商 launcher/global 资源后退出;不要同时占用 Inspector 14201: 复现原生验收时先构建最小参考页并启动一次圆屏模拟器,获得厂商 launcher/global 资源后退出;不要同时占用 Inspector 14201:
@@ -88,4 +92,4 @@ python .\com.sydxky.rtos.inputmethod\tests\native.py --work work/native --output
python .\com.sydxky.rtos.inputmethod\tests\visual.py --work work/visual --output work/evidence/visual python .\com.sydxky.rtos.inputmethod\tests\visual.py --work work/visual --output work/evidence/visual
``` ```
视觉脚本依赖 Pillow。RTOS 运行时内存配置保持 8192 KiB。本版本已完成上述间距与构建检查;未完成的交互、QuickApp原生视觉、RTOS真机、长期性能/续航和商店审核范围见本版验收记录。 视觉脚本依赖 Pillow。RTOS 运行时内存配置保持 8192 KiB。本版本已完成上述算法、组件回归与构建检查;未完成的交互、QuickApp原生视觉、RTOS真机、长期性能/续航和商店审核范围见本版验收记录。
+5 -3
查看文件
@@ -1,19 +1,21 @@
# 蟹蟹输入 Android应用内键盘组件 # 蟹蟹输入 Android应用内键盘组件
基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.2 / 10202。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.3 / 10203。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。
应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。 应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。
完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。 完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。
1.2.3 增加未完成拼音补全、上下文续写及词频排序;复用预览、按键及候选控件,按帧合并刷新。联想仅在内存中使用当前输入框光标前的中文,不持久化全文,密码内容不参与。见[本版验收](./VALIDATION_1.2.3.md)。
1.2.2 去掉英文、数字、符号模式未使用的候选栏空位,文字与键盘紧密相接。本版真机间距检查通过;完整测试的限制见[验收记录](./VALIDATION_1.2.2.md)。 1.2.2 去掉英文、数字、符号模式未使用的候选栏空位,文字与键盘紧密相接。本版真机间距检查通过;完整测试的限制见[验收记录](./VALIDATION_1.2.2.md)。
## 一分钟接入 ## 一分钟接入
将发行版`crab-keyboard-1.2.2.aar`放入宿主`app/libs/`: 将发行版`crab-keyboard-1.2.3.aar`放入宿主`app/libs/`:
```groovy ```groovy
dependencies { implementation files('libs/crab-keyboard-1.2.2.aar') } dependencies { implementation files('libs/crab-keyboard-1.2.3.aar') }
``` ```
```java ```java
+9
查看文件
@@ -0,0 +1,9 @@
# Android 1.2.3 联想和刷新验收
2026-11-14,版本1.2.3/10203,最低API27。原始词库无损回读104,837组索引、163,238候选,SQLite schema2新增28,977个续写上下文/52,443候选对,完整性检查通过。新缓存文件名与旧schema隔离,避免升级后误用旧数据库。
7项Java单元测试通过。正式Release APK/AAR、宿主与测试APK构建通过;签名及最终哈希随发行清单。lint均为0错误,演示应用1项备份声明警告,组件2项仅测试依赖版本更新提示。Android复用预览、按键和候选控件,按Choreographer帧合并刷新;提交时合并编辑,候选替换后重置页码。联想只使用当前光标前最多4个连续汉字,不保存正文,密码不参与。
OWW261 Android11/API30上运行3项后台原生组件测试通过:24帧真实原生measure/layout/draw均绘出文字,预览/按键实例保持一致;niha补全你好并接续世界且仅提交一次;密码内容不作为联想上下文。测试在离屏原生View上执行,没有打开Activity、切换前台或修改默认系统输入法;这是组件回归,不是全套前台手势/视觉验收。测试使用组件debug变体,15个运行时class成员与发布AAR逐字节一致;不以此替代发布APK端到端验收。
本版未重跑原9项前台交互,RTOS真机、另一款方表、长时性能/功耗与商店审核未验收。之前1.2.2的6/9结果及截图仅保留作历史资料,不作为本版通过项。
+2 -2
查看文件
@@ -12,8 +12,8 @@ android {
applicationId 'com.sydxky.crabinput' applicationId 'com.sydxky.crabinput'
minSdk 27 minSdk 27
targetSdk 35 targetSdk 35
versionCode 10202 versionCode 10203
versionName '1.2.2' versionName '1.2.3'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
signingConfigs { signingConfigs {
+1 -1
查看文件
@@ -2,7 +2,7 @@ plugins { id 'com.android.application' }
android { android {
namespace 'com.sydxky.crabinput.host' namespace 'com.sydxky.crabinput.host'
compileSdk 37 compileSdk 37
defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10202; versionName '1.2.2' } defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10203; versionName '1.2.3' }
compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 }
} }
dependencies { implementation project(':keyboard') } dependencies { implementation project(':keyboard') }
+6 -2
查看文件
@@ -2,8 +2,12 @@ plugins { id 'com.android.library' }
android { android {
namespace 'com.sydxky.crabinput.keyboard' namespace 'com.sydxky.crabinput.keyboard'
compileSdk 37 compileSdk 37
defaultConfig { minSdk 27; consumerProguardFiles 'consumer-rules.pro' } defaultConfig { minSdk 27; consumerProguardFiles 'consumer-rules.pro'; testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' }
compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 }
lint { abortOnError true } lint { abortOnError true }
} }
dependencies { testImplementation 'junit:junit:4.13.2' } dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
}
@@ -0,0 +1,80 @@
package com.sydxky.crabinput;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.*;
/** Native offscreen regression: no Activity, foreground switch or system IME. */
public class SurfaceRegressionTest {
private Context context(){return new ContextThemeWrapper(InstrumentationRegistry.getInstrumentation().getTargetContext(),android.R.style.Theme_Material_Light_NoActionBar);}
private void ui(Runnable action){InstrumentationRegistry.getInstrumentation().runOnMainSync(action);}
private View find(View root,String description){
if(description.contentEquals(root.getContentDescription()==null?"":root.getContentDescription()))return root;
if(root instanceof ViewGroup)for(int i=0;i<((ViewGroup)root).getChildCount();i++){View found=find(((ViewGroup)root).getChildAt(i),description);if(found!=null)return found;}
return null;
}
@Test public void textAndKeysSurviveEveryNativeFrame(){
ui(()->{
KeyboardState state=new KeyboardState();state.mode="py";state.preview="你好";
KeyboardSurface surface=new KeyboardSurface(context(),state,new KeyboardSurface.Actions(){public void action(String v){}public void choose(Candidate v){}public void repeatDelete(boolean v){}});
View preview=null,key=null;
for(int frame=0;frame<24;frame++){
state.preview="你好"+frame;state.composition=frame%2==0?"niha":"nihao";
state.busy=frame%2==0;state.candidates=Arrays.asList(new Candidate(frame%2==0?"你好":"您好",5));
// Real native measure/layout/draw, alternating dimensions also
// exercises the resize path without attaching a screen window.
int width=466+frame%2;
surface.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(466,View.MeasureSpec.EXACTLY));surface.layout(0,0,width,466);
View now=find(surface,"编辑光标"),q=find(surface,"q");assertNotNull(now);assertNotNull(q);
if(preview!=null){assertSame("Preview is never detached and recreated",preview,now);assertSame("Keys survive async candidate changes",key,q);}
preview=now;key=q;assertSame(surface,now.getParent());assertEquals("你好"+frame,((TextView)now).getText().toString());
Bitmap bitmap=Bitmap.createBitmap(width,466,Bitmap.Config.ARGB_8888);surface.draw(new Canvas(bitmap));int ink=0;
for(int y=now.getTop();y<now.getBottom();y++)for(int x=now.getLeft();x<now.getRight();x++){int color=bitmap.getPixel(x,y);if(Color.red(color)<120&&Color.green(color)<120&&Color.blue(color)<120)ink++;}
assertTrue("Every frame paints the entered text",ink>20);bitmap.recycle();
}
});
}
private KeyboardController session(EditText editor,int type){
EditorInfo info=new EditorInfo();info.inputType=type;
BaseInputConnection input=new BaseInputConnection(editor,true){@Override public Editable getEditable(){return editor.getText();}};
return new KeyboardController(context(),input,info,()->{});
}
private void awaitQuery(KeyboardController controller){
long until=SystemClock.uptimeMillis()+8000;boolean[] busy={true};
do{SystemClock.sleep(40);ui(()->busy[0]=controller.state.busy);}while(busy[0]&&SystemClock.uptimeMillis()<until);
assertFalse("Offline query completes",busy[0]);
}
@Test public void completionAndContinuationCommitExactlyOnce(){
KeyboardController[] value=new KeyboardController[1];EditText[] editor=new EditText[1];
ui(()->{editor[0]=new EditText(context());value[0]=session(editor[0],InputType.TYPE_CLASS_TEXT);value[0].action("mode:py");for(char ch:"niha".toCharArray())value[0].action("key:"+ch);});
KeyboardController controller=value[0];
try{
awaitQuery(controller);ui(()->{assertEquals("你好",controller.state.candidates.get(0).word);controller.choose(controller.state.candidates.get(0));});
awaitQuery(controller);ui(()->{assertEquals("你好",editor[0].getText().toString());assertTrue(controller.state.prediction);assertEquals("世界",controller.state.candidates.get(0).word);controller.choose(controller.state.candidates.get(0));});
awaitQuery(controller);ui(()->assertEquals("你好世界",editor[0].getText().toString()));
}finally{ui(controller::close);}
}
@Test public void passwordDoesNotProvidePredictionContext(){
ui(()->{
EditText editor=new EditText(context());editor.setText("你好");editor.setSelection(editor.length());
KeyboardController controller=session(editor,InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);
try{controller.action("space");assertTrue(controller.state.secret);assertFalse(controller.state.prediction);assertTrue(controller.state.candidates.isEmpty());assertEquals("",controller.state.preview);}
finally{controller.close();}
});
}
}
@@ -1,9 +1,12 @@
{ {
"schema_version": 2,
"association_prefixes": 28977,
"association_candidates": 52443,
"keys": 104837, "keys": 104837,
"candidates": 163238, "candidates": 163238,
"longest_word": 4, "longest_word": 4,
"sha256": "fa01b1ab0264d8b271a32338b93a64b8670a71d57264c8586c89ae3cf3ddd8a2", "sha256": "6575841fb5d6232d1624bd91c19e8f39b030eca4a4e09a6bcff21e12b12b27d4",
"bytes": 12644352, "bytes": 13688832,
"round_trip": true, "round_trip": true,
"source_files": { "source_files": {
"key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8", "key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8",
@@ -3,5 +3,10 @@ package com.sydxky.crabinput;
public final class Candidate { public final class Candidate {
public final String word; public final String word;
public final int consume; public final int consume;
public Candidate(String word, int consume) { this.word=word; this.consume=consume; } final int frequency;
final boolean completion;
public Candidate(String word, int consume) { this(word,consume,0,false); }
Candidate(String word,int consume,int frequency,boolean completion) {
this.word=word;this.consume=consume;this.frequency=frequency;this.completion=completion;
}
} }
@@ -0,0 +1,24 @@
package com.sydxky.crabinput;
import java.util.List;
/** Shared policy for complete matches, unfinished syllables and prefix fallbacks. */
final class CandidateRanking {
private static int category(Candidate item,int length) {
return item.completion?1:item.consume==length?0:2;
}
private static int contextRank(Candidate item,List<Candidate> predictions) {
for(int i=0;i<predictions.size();i++)if(predictions.get(i).word.equals(item.word))return 8-i;
return 0;
}
static void sort(List<Candidate> items,int length,List<Candidate> predictions) {
items.sort((a,b)->{
int order=Integer.compare(category(a,length),category(b,length));
if(order==0)order=Integer.compare(b.consume,a.consume);
if(order==0)order=Integer.compare(contextRank(b,predictions),contextRank(a,predictions));
if(order==0)order=Integer.compare(b.frequency,a.frequency);
return order;
});
}
private CandidateRanking(){}
}
@@ -27,7 +27,7 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable
private Future<?> pending; private Future<?> pending;
private int revision; private int revision;
private boolean active=true,repeat,closed; private boolean active=true,repeat,closed;
private final Runnable lookup=()->query(revision,state.composition,state.mode); private final Runnable lookup=()->query(revision,state.composition,state.mode,contextText());
private final Runnable deleteAgain=new Runnable(){@Override public void run(){if(repeat&&active){delete();main.postDelayed(this,115);}}}; private final Runnable deleteAgain=new Runnable(){@Override public void run(){if(repeat&&active){delete();main.postDelayed(this,115);}}};
@android.annotation.SuppressLint("WearPasswordInput") // Detect host password editors, no account login. @android.annotation.SuppressLint("WearPasswordInput") // Detect host password editors, no account login.
KeyboardController(Context context,InputConnection input,EditorInfo info,Runnable dismiss){ KeyboardController(Context context,InputConnection input,EditorInfo info,Runnable dismiss){
@@ -44,26 +44,34 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable
} }
private void message(String text){state.message=text;android.widget.Toast.makeText(context,text,android.widget.Toast.LENGTH_SHORT).show();refresh();} private void message(String text){state.message=text;android.widget.Toast.makeText(context,text,android.widget.Toast.LENGTH_SHORT).show();refresh();}
private boolean chinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");} private boolean chinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");}
private String contextText(){if(state.secret)return "";CharSequence text=input.getTextBeforeCursor(12,0);return text==null?"":text.toString();}
private void clearCandidates(){state.candidates=new ArrayList<>();state.pages=new ArrayList<>();}
private void schedule(){ private void schedule(){
invalidateLookup();state.page=0;state.candidates=new ArrayList<>();state.pages=new ArrayList<>(); invalidateLookup();state.page=0;
if(!state.composition.isEmpty()&&active){state.busy=true;main.postDelayed(lookup,40);}refresh(); boolean prediction=state.composition.isEmpty();
if(!active||!chinese()||prediction&&(state.secret||!contextText().matches("(?s).*[\\u3400-\\u9fff]$"))){clearCandidates();state.prediction=false;refresh();return;}
if(prediction!=state.prediction)clearCandidates();
state.prediction=prediction;state.busy=true;main.postDelayed(lookup,40);refresh();
} }
private void query(int owner,String composition,String mode){ private void query(int owner,String composition,String mode,String context){
pending=worker.submit(()->{ pending=worker.submit(()->{
try {List<Candidate> result=lexicon.lookup(composition,mode);main.post(()->{if(!active||revision!=owner)return;state.candidates=result;state.pages=CandidatePager.pages(result);state.busy=false;state.message=result.isEmpty()?"暂无匹配,请退格修改":"离线输入";refresh();});} try {List<Candidate> result=composition.isEmpty()?lexicon.predict(context):lexicon.lookup(composition,mode,context);main.post(()->{if(!active||revision!=owner)return;state.candidates=result;state.pages=CandidatePager.pages(result);state.page=0;state.busy=false;state.message=result.isEmpty()&&!composition.isEmpty()?"暂无匹配,请退格修改":state.prediction?"联想":"离线输入";refresh();});}
catch(Exception ignored){main.post(()->{if(active&&revision==owner){state.busy=false;state.message="词库暂不可用,可切换英文";refresh();}});} catch(Exception ignored){main.post(()->{if(active&&revision==owner){clearCandidates();state.busy=false;state.message="词库暂不可用,可切换英文";refresh();}});}
}); });
} }
private void commit(String text){if(active)input.commitText(text,1);refresh();} private void commit(String text){if(!active)return;input.commitText(text,1);if(chinese())schedule();else refresh();}
@Override public void choose(Candidate item){ @Override public void choose(Candidate item){
if(!active||state.busy||state.composition.isEmpty()||!state.candidates.contains(item))return; if(!active||state.busy||state.composition.isEmpty()&&(!state.prediction||item.consume!=0)||!state.candidates.contains(item))return;
String remaining=state.composition.substring(Math.min(item.consume,state.composition.length())); String remaining=state.composition.substring(Math.min(item.consume,state.composition.length()));
invalidateLookup();commit(item.word);state.composition=remaining;state.expanded=false;schedule(); invalidateLookup();input.beginBatchEdit();
try{input.commitText(item.word,1);state.composition=remaining;state.expanded=false;}
finally{input.endBatchEdit();}
schedule();
} }
private void delete(){ private void delete(){
if(!active)return; if(!active)return;
if(!state.composition.isEmpty()){state.composition=state.composition.substring(0,state.composition.length()-1);schedule();return;} if(!state.composition.isEmpty()){state.composition=state.composition.substring(0,state.composition.length()-1);schedule();return;}
CharSequence selection=input.getSelectedText(0);if(selection!=null&&selection.length()>0)input.commitText("",1);else input.deleteSurroundingTextInCodePoints(1,0);refresh(); CharSequence selection=input.getSelectedText(0);if(selection!=null&&selection.length()>0)input.commitText("",1);else input.deleteSurroundingTextInCodePoints(1,0);schedule();
} }
@Override public void repeatDelete(boolean start){boolean previous=repeat;main.removeCallbacks(deleteAgain);repeat=start&&active;if(repeat){delete();main.postDelayed(deleteAgain,450);}else if(previous)refresh();} @Override public void repeatDelete(boolean start){boolean previous=repeat;main.removeCallbacks(deleteAgain);repeat=start&&active;if(repeat){delete();main.postDelayed(deleteAgain,450);}else if(previous)refresh();}
@Override public void action(String value){ @Override public void action(String value){
@@ -88,8 +96,8 @@ final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable
case "tools":state.expanded=false;state.tools=true;break; case "tools":state.expanded=false;state.tools=true;break;
case "clear-composition":state.composition="";schedule();return; case "clear-composition":state.composition="";schedule();return;
case "hide":dismiss.run();return; case "hide":dismiss.run();return;
case "left":case "right":if(state.composition.isEmpty()){int key=value.equals("left")?KeyEvent.KEYCODE_DPAD_LEFT:KeyEvent.KEYCODE_DPAD_RIGHT;input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,key));input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,key));}break; case "left":case "right":if(state.composition.isEmpty()){int key=value.equals("left")?KeyEvent.KEYCODE_DPAD_LEFT:KeyEvent.KEYCODE_DPAD_RIGHT;input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,key));input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,key));schedule();return;}break;
case "raw":if(state.mode.equals("py")&&!state.composition.isEmpty()){String text=state.composition;state.composition="";commit(text);schedule();}break; case "raw":if(state.mode.equals("py")&&!state.composition.isEmpty()){String text=state.composition;state.composition="";commit(text);}return;
case "newline":if(!state.secret&&(info.inputType&InputType.TYPE_TEXT_FLAG_MULTI_LINE)!=0)commit("\n");break; case "newline":if(!state.secret&&(info.inputType&InputType.TYPE_TEXT_FLAG_MULTI_LINE)!=0)commit("\n");break;
case "done": case "done":
if(!state.composition.isEmpty()){message("请先选择候选词");state.expanded=false;break;} if(!state.composition.isEmpty()){message("请先选择候选词");state.expanded=false;break;}
@@ -5,7 +5,7 @@ import java.util.List;
final class KeyboardState { final class KeyboardState {
String mode="t9",composition="",preview="",message="离线输入"; String mode="t9",composition="",preview="",message="离线输入";
boolean expanded,tools,upper,secret,busy; boolean expanded,tools,upper,secret,busy,prediction;
int page,symbolPage; int page,symbolPage;
List<Candidate> candidates=new ArrayList<>(); List<Candidate> candidates=new ArrayList<>();
List<List<CandidatePager.Row>> pages=new ArrayList<>(); List<List<CandidatePager.Row>> pages=new ArrayList<>();
@@ -16,6 +16,10 @@ import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** Native accessible buttons laid out within the actual round screen chord. */ /** Native accessible buttons laid out within the actual round screen chord. */
@android.annotation.SuppressLint("ViewConstructor") // Created by the IME, never inflated from XML. @android.annotation.SuppressLint("ViewConstructor") // Created by the IME, never inflated from XML.
@@ -26,7 +30,11 @@ final class KeyboardSurface extends ViewGroup {
private final Actions actions; private final Actions actions;
private final boolean round; private final boolean round;
private final List<Placement> placements=new ArrayList<>(); private final List<Placement> placements=new ArrayList<>();
private final Map<String,View> children=new LinkedHashMap<>();
private final Set<String> used=new HashSet<>();
private boolean refreshPosted;
private int builtWidth,builtHeight; private int builtWidth,builtHeight;
private final Runnable render=()->{refreshPosted=false;if(builtWidth>0)build(builtWidth,builtHeight);requestLayout();invalidate();};
private float sx=1,sy=1; private float sx=1,sy=1;
private static final class Placement { private static final class Placement {
final View view;final Rect bounds; final View view;final Rect bounds;
@@ -41,7 +49,7 @@ final class KeyboardSurface extends ViewGroup {
boolean watch=round||(dm.heightPixels/(float)dm.widthPixels<1.35f); boolean watch=round||(dm.heightPixels/(float)dm.widthPixels<1.35f);
return watch?dm.heightPixels:Math.min(dm.widthPixels,Math.round(dm.heightPixels*.46f)); return watch?dm.heightPixels:Math.min(dm.widthPixels,Math.round(dm.heightPixels*.46f));
} }
void refresh() { if(builtWidth>0)build(builtWidth,builtHeight);requestLayout();invalidate(); } void refresh() {if(!refreshPosted){refreshPosted=true;postOnAnimation(render);}}
private Rect bounds(float x,float y,float width,float height) { private Rect bounds(float x,float y,float width,float height) {
return new Rect(Math.round(x*sx),Math.round(y*sy),Math.round((x+width)*sx),Math.round((y+height)*sy)); return new Rect(Math.round(x*sx),Math.round(y*sy),Math.round((x+width)*sx),Math.round((y+height)*sy));
} }
@@ -53,25 +61,29 @@ final class KeyboardSurface extends ViewGroup {
return Math.max(8,(builtWidth/2f-half)/sx+2); return Math.max(8,(builtWidth/2f-half)/sx+2);
} }
private void place(View view,float x,float y,float width,float height) { private void place(View view,float x,float y,float width,float height) {
addView(view);placements.add(new Placement(view,bounds(x,y,width,height))); if(view.getParent()==null)addView(view);placements.add(new Placement(view,bounds(x,y,width,height)));
} }
private TextView label(String text,float font,int color) { private TextView label(String key,String text,float font,int color) {
TextView view=new TextView(getContext());view.setText(text);view.setTextColor(color);view.setGravity(Gravity.CENTER); used.add(key);TextView view=(TextView)children.get(key);
if(view==null){view=new TextView(getContext());children.put(key,view);}
if(!view.getText().toString().equals(text))view.setText(text);view.setTextColor(color);view.setGravity(Gravity.CENTER);
view.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy));view.setIncludeFontPadding(false);return view; view.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy));view.setIncludeFontPadding(false);return view;
} }
private Button button(String text,String description,float font,boolean primary,Runnable action) { private Button button(String key,String text,String description,float font,boolean primary,Runnable action) {
Button button=new Button(getContext());button.setText(text);button.setContentDescription(description);button.setAllCaps(false); used.add(key);Button button=(Button)children.get(key);boolean created=button==null;
if(created){button=new Button(getContext());children.put(key,button);}
if(!button.getText().toString().equals(text))button.setText(text);button.setContentDescription(description);button.setAllCaps(false);
button.setGravity(Gravity.CENTER);button.setIncludeFontPadding(false);button.setMinHeight(0);button.setMinimumHeight(0);button.setMinWidth(0);button.setMinimumWidth(0); button.setGravity(Gravity.CENTER);button.setIncludeFontPadding(false);button.setMinHeight(0);button.setMinimumHeight(0);button.setMinWidth(0);button.setMinimumWidth(0);
button.setPadding(Math.round(3*sx),0,Math.round(3*sx),0);button.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy)); button.setPadding(Math.round(3*sx),0,Math.round(3*sx),0);button.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy));
button.setTextColor(primary?Color.WHITE:INK);button.setStateListAnimator(null); button.setTextColor(primary?Color.WHITE:INK);button.setStateListAnimator(null);
GradientDrawable shape=new GradientDrawable();shape.setColor(primary?BLUE:Color.WHITE);shape.setCornerRadius(10*Math.min(sx,sy)); if(created){GradientDrawable shape=new GradientDrawable();shape.setColor(primary?BLUE:Color.WHITE);shape.setCornerRadius(10*Math.min(sx,sy));button.setBackground(new RippleDrawable(ColorStateList.valueOf(0x332867D9),shape,null));}
button.setBackground(new RippleDrawable(ColorStateList.valueOf(0x332867D9),shape,null));button.setOnClickListener(v->action.run());return button; button.setOnClickListener(v->action.run());return button;
} }
// Native Button click remains available to accessibility; touch deletion starts on down. // Native Button click remains available to accessibility; touch deletion starts on down.
@android.annotation.SuppressLint("ClickableViewAccessibility") @android.annotation.SuppressLint("ClickableViewAccessibility")
private void action(String text,String value,float x,float y,float width,float height,float font,boolean primary) { private void action(String text,String value,float x,float y,float width,float height,float font,boolean primary) {
String name=value.equals("delete")?"删除":value.equals("done")?"完成":value.equals("hide")?"收起键盘":value.equals("expand")?"更多候选":value.equals("back")?"返回键盘":value.equals("previous")?"上一页":value.equals("next")?"下一页":text; String name=value.equals("delete")?"删除":value.equals("done")?"完成":value.equals("hide")?"收起键盘":value.equals("expand")?"更多候选":value.equals("back")?"返回键盘":value.equals("previous")?"上一页":value.equals("next")?"下一页":text;
Button button=button(text,name,font,primary,()->actions.action(value)); Button button=button("action:"+value,text,name,font,primary,()->actions.action(value));
if(value.equals("delete"))button.setOnTouchListener((v,event)->{ if(value.equals("delete"))button.setOnTouchListener((v,event)->{
if(event.getActionMasked()==MotionEvent.ACTION_DOWN)actions.repeatDelete(true); if(event.getActionMasked()==MotionEvent.ACTION_DOWN)actions.repeatDelete(true);
if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL)actions.repeatDelete(false); if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL)actions.repeatDelete(false);
@@ -84,31 +96,33 @@ final class KeyboardSurface extends ViewGroup {
for(int i=0;i<captions.length;i++)action(captions[i],values[i],inset+i*cell+2,y,cell-4,height,font,false); for(int i=0;i<captions.length;i++)action(captions[i],values[i],inset+i*cell+2,y,cell-4,height,font,false);
} }
private void build(int width,int height) { private void build(int width,int height) {
builtWidth=width;builtHeight=height;sx=width/410f;sy=height/410f;removeAllViews();placements.clear(); builtWidth=width;builtHeight=height;sx=width/410f;sy=height/410f;placements.clear();used.clear();
try{
float top=round?28:7; float top=round?28:7;
float headerInset=inset(top,23); float headerInset=inset(top,23);
action("⌄","hide",headerInset,top,32,23,23,false); action("⌄","hide",headerInset,top,32,23,23,false);
String title=state.expanded?"完整候选":state.tools?"编辑工具":"蟹蟹输入"; String title=state.expanded?"完整候选":state.tools?"编辑工具":"蟹蟹输入";
place(label(title,14,MUTED),headerInset+34,top,410-2*headerInset-68,23); place(label("title",title,14,MUTED),headerInset+34,top,410-2*headerInset-68,23);
action("编辑","tools",410-headerInset-32,top,32,23,10,false); action("编辑","tools",410-headerInset-32,top,32,23,10,false);
float previewY=top+29; float previewY=top+29;
String preview=state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview; String preview=state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview;
TextView previewView=label(preview,21,INK);previewView.setMaxLines(1);previewView.setEllipsize(android.text.TextUtils.TruncateAt.START); TextView previewView=label("preview",preview,21,INK);previewView.setMaxLines(1);previewView.setEllipsize(android.text.TextUtils.TruncateAt.START);
previewView.setOnClickListener(v->actions.action("tools"));previewView.setContentDescription("编辑光标");
float pi=inset(previewY,30);place(previewView,pi,previewY,410-2*pi,30); float pi=inset(previewY,30);place(previewView,pi,previewY,410-2*pi,30);
if(state.expanded) { buildCandidates(previewY+38);return; } if(state.expanded) { buildCandidates(previewY+38);return; }
if(state.tools) { buildTools(previewY+40);return; } if(state.tools) { buildTools(previewY+40);return; }
float compositionY=previewY+32; float compositionY=previewY+32;
String composition=state.composition; String composition=state.composition;
if(state.mode.equals("14")) { StringBuilder display=new StringBuilder();for(char ch:composition.substring(Math.max(0,composition.length()-6)).toCharArray())display.append(Lexicon.GROUP14["0123456789abcd".indexOf(ch)].toUpperCase(Locale.ROOT)).append(' ');composition=display.toString(); } if(state.mode.equals("14")) { StringBuilder display=new StringBuilder();for(char ch:composition.substring(Math.max(0,composition.length()-6)).toCharArray())display.append(Lexicon.GROUP14["0123456789abcd".indexOf(ch)].toUpperCase(Locale.ROOT)).append(' ');composition=display.toString(); }
TextView compose=label(composition.isEmpty()?modeName():composition,15,BLUE);compose.setMaxLines(1); TextView compose=label("composition",composition.isEmpty()?modeName():composition,15,BLUE);compose.setMaxLines(1);
float ci=inset(compositionY,23);place(compose,ci,compositionY,410-2*ci,23); float ci=inset(compositionY,23);place(compose,ci,compositionY,410-2*ci,23);
float candidateY=compositionY+26; float candidateY=compositionY+26;
if(isChinese()) { if(isChinese()) {
float left=inset(candidateY,32),available=410-2*left; float left=inset(candidateY,32),available=410-2*left;
int count=Math.min(3,state.candidates.size());float cell=(available-37)/Math.max(1,count); int count=Math.min(3,state.candidates.size());float cell=(available-37)/Math.max(1,count);
for(int i=0;i<count;i++){Candidate item=state.candidates.get(i);Button b=button(item.word,item.word,22,false,()->actions.choose(item));b.setMaxLines(1);b.setAutoSizeTextTypeUniformWithConfiguration(10,22,1,TypedValue.COMPLEX_UNIT_PX);place(b,left+i*cell+2,candidateY,cell-4,32);} for(int i=0;i<count;i++){Candidate item=state.candidates.get(i);Button b=button("candidate:"+i,item.word,item.word,22,false,()->actions.choose(item));b.setEnabled(!state.busy);b.setMaxLines(1);b.setAutoSizeTextTypeUniformWithConfiguration(10,22,1,TypedValue.COMPLEX_UNIT_PX);place(b,left+i*cell+2,candidateY,cell-4,32);}
if(count>0)action("⌄","expand",410-left-35,candidateY,33,32,24,false); if(count>0)action("⌄","expand",410-left-35,candidateY,33,32,24,false);
else place(label(state.busy?"正在查词…":state.message,12,MUTED),left,candidateY,available,32); else place(label("candidate-status",state.busy?"正在查词…":state.message,12,MUTED),left,candidateY,available,32);
} }
// A direct-entry mode has no candidate strip; attach its keys to the // A direct-entry mode has no candidate strip; attach its keys to the
// composition caption instead of reserving an invisible suggestion row. // composition caption instead of reserving an invisible suggestion row.
@@ -128,19 +142,24 @@ final class KeyboardSurface extends ViewGroup {
float modeY=keyY+captions.length*(keyHeight+4)+3; float modeY=keyY+captions.length*(keyHeight+4)+3;
row(new String[]{"九宫","26键","14键",state.upper?"ABC":"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13); row(new String[]{"九宫","26键","14键",state.upper?"ABC":"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13);
bottom(modeY+34,false); bottom(modeY+34,false);
}finally{
java.util.Iterator<Map.Entry<String,View>> iterator=children.entrySet().iterator();
while(iterator.hasNext()){Map.Entry<String,View> entry=iterator.next();if(!used.contains(entry.getKey())){removeView(entry.getValue());iterator.remove();}}
}
} }
private void buildCandidates(float y) { private void buildCandidates(float y) {
int count=state.pages.size();float safe=inset(y,18); int count=state.pages.size();float safe=inset(y,18);
place(label(state.candidates.size()+" 项 · "+(state.page+1)+" / "+Math.max(1,count),12,MUTED),safe,y,410-2*safe,18); place(label("page-info",state.candidates.size()+" 项 · "+(state.page+1)+" / "+Math.max(1,count),12,MUTED),safe,y,410-2*safe,18);
float gridY=y+25;int unit=43; float gridY=y+25;int unit=43;
int rowIndex=0;
if(count>0)for(CandidatePager.Row row:state.pages.get(state.page)) { if(count>0)for(CandidatePager.Row row:state.pages.get(state.page)) {
float rowHeight=row.units*unit-4,left=inset(gridY,rowHeight),cell=(410-2*left)/4;int slot=0; float rowHeight=row.units*unit-4,left=inset(gridY,rowHeight),cell=(410-2*left)/4;int slot=0;
for(CandidatePager.Cell entry:row.cells) { for(CandidatePager.Cell entry:row.cells) {
Candidate item=entry.candidate;Button button=button(item.word,item.word,entry.span==1?25:entry.span==2?23:20,false,()->actions.choose(item)); Candidate item=entry.candidate;Button button=button("grid:"+rowIndex+":"+slot,item.word,item.word,entry.span==1?25:entry.span==2?23:20,false,()->actions.choose(item));button.setEnabled(!state.busy);
button.setMaxLines(row.units*2);button.setEllipsize(null); button.setMaxLines(row.units*2);button.setEllipsize(null);
place(button,left+slot*cell+2,gridY,entry.span*cell-4,rowHeight);slot+=entry.span; place(button,left+slot*cell+2,gridY,entry.span*cell-4,rowHeight);slot+=entry.span;
} }
gridY+=row.units*unit; gridY+=row.units*unit;rowIndex++;
} }
float pagerY=y+25+4*unit+3; float pagerY=y+25+4*unit+3;
row(new String[]{"‹","›"},new String[]{"previous","next"},pagerY,29,27); row(new String[]{"‹","›"},new String[]{"previous","next"},pagerY,29,27);
@@ -150,7 +169,7 @@ final class KeyboardSurface extends ViewGroup {
row(new String[]{"← 光标","光标 →"},new String[]{"left","right"},y+7,37,18); row(new String[]{"← 光标","光标 →"},new String[]{"left","right"},y+7,37,18);
row(new String[]{"换行","拼音上屏"},new String[]{"newline","raw"},y+53,37,18); row(new String[]{"换行","拼音上屏"},new String[]{"newline","raw"},y+53,37,18);
row(new String[]{"清除拼音","空格"},new String[]{"clear-composition","space"},y+99,37,18); row(new String[]{"清除拼音","空格"},new String[]{"clear-composition","space"},y+99,37,18);
place(label(state.secret?"私密输入 · 不学习、不保存":"完全离线 · 不采集输入内容",12,MUTED),45,y+150,320,26); place(label("privacy",state.secret?"私密输入 · 不学习、不保存":"完全离线 · 不采集输入内容",12,MUTED),45,y+150,320,26);
bottom(y+190,true); bottom(y+190,true);
} }
private void bottom(float y,boolean back) { private void bottom(float y,boolean back) {
@@ -158,8 +177,6 @@ final class KeyboardSurface extends ViewGroup {
action(back?"键盘":"空格",back?"back":"space",left+2,y,cell-4,34,17,false); action(back?"键盘":"空格",back?"back":"space",left+2,y,cell-4,34,17,false);
action("⌫","delete",left+cell+2,y,cell-4,34,25,false); action("⌫","delete",left+cell+2,y,cell-4,34,25,false);
action("✓","done",left+2*cell+2,y,cell-4,34,23,true); action("✓","done",left+2*cell+2,y,cell-4,34,23,true);
// Preview taps open cursor tools without taking an extra keyboard key.
for(Placement placement:placements)if(placement.view instanceof TextView&&!(placement.view instanceof Button)&&placement.view.getContentDescription()==null){if(((TextView)placement.view).getText().toString().equals(state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview)){placement.view.setOnClickListener(v->actions.action("tools"));placement.view.setContentDescription("编辑光标");}}
} }
private boolean isChinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");} private boolean isChinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");}
private String modeName(){return state.mode.equals("py")?"26键全拼 · v 代表 ü":state.mode.equals("t9")?"九宫拼音":state.mode.equals("14")?"14键拼音":state.mode.equals("en")?"英文":state.mode.equals("sym")?"符号 · "+(state.symbolPage+1)+"/3 · 再点符号翻页":"数字";} private String modeName(){return state.mode.equals("py")?"26键全拼 · v 代表 ü":state.mode.equals("t9")?"九宫拼音":state.mode.equals("14")?"14键拼音":state.mode.equals("en")?"英文":state.mode.equals("sym")?"符号 · "+(state.symbolPage+1)+"/3 · 再点符号翻页":"数字";}
@@ -173,5 +190,5 @@ final class KeyboardSurface extends ViewGroup {
} }
@Override protected void onLayout(boolean changed,int l,int t,int r,int b){for(Placement item:placements)item.view.layout(item.bounds.left,item.bounds.top,item.bounds.right,item.bounds.bottom);} @Override protected void onLayout(boolean changed,int l,int t,int r,int b){for(Placement item:placements)item.view.layout(item.bounds.left,item.bounds.top,item.bounds.right,item.bounds.bottom);}
@Override public boolean dispatchTouchEvent(MotionEvent event){if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL)actions.repeatDelete(false);return super.dispatchTouchEvent(event);} @Override public boolean dispatchTouchEvent(MotionEvent event){if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL)actions.repeatDelete(false);return super.dispatchTouchEvent(event);}
@Override protected void onDetachedFromWindow(){actions.repeatDelete(false);super.onDetachedFromWindow();} @Override protected void onDetachedFromWindow(){removeCallbacks(render);refreshPosted=false;actions.repeatDelete(false);super.onDetachedFromWindow();}
} }
@@ -29,9 +29,9 @@ public final class Lexicon implements AutoCloseable {
} }
private void open() throws Exception { private void open() throws Exception {
if(db!=null)return; if(db!=null)return;
File directory=context.getNoBackupFilesDir();File target=new File(directory,"crab-v5-1.db"); File directory=context.getNoBackupFilesDir();File target=new File(directory,"crab-v5-2.db");
if(!target.isFile()) { if(!target.isFile()) {
File temporary=new File(directory,"crab-v5-1.tmp"); File temporary=new File(directory,"crab-v5-2.tmp");
try(InputStream input=context.getAssets().open("dictionary.db");FileOutputStream output=new FileOutputStream(temporary)) { try(InputStream input=context.getAssets().open("dictionary.db");FileOutputStream output=new FileOutputStream(temporary)) {
byte[] buffer=new byte[32768];int length;while((length=input.read(buffer))!=-1)output.write(buffer,0,length);output.getFD().sync(); byte[] buffer=new byte[32768];int length;while((length=input.read(buffer))!=-1)output.write(buffer,0,length);output.getFD().sync();
} }
@@ -40,6 +40,21 @@ public final class Lexicon implements AutoCloseable {
db=SQLiteDatabase.openDatabase(target.getPath(),null,SQLiteDatabase.OPEN_READONLY|SQLiteDatabase.NO_LOCALIZED_COLLATORS); db=SQLiteDatabase.openDatabase(target.getPath(),null,SQLiteDatabase.OPEN_READONLY|SQLiteDatabase.NO_LOCALIZED_COLLATORS);
} }
public List<Candidate> lookup(String input,String mode) throws Exception { public List<Candidate> lookup(String input,String mode) throws Exception {
return lookup(input,mode,"");
}
public List<Candidate> predict(String context) throws Exception {
List<Candidate> result=new ArrayList<>();
java.util.regex.Matcher match=java.util.regex.Pattern.compile("[\\u3400-\\u9fff]{1,4}$").matcher(context==null?"":context);
if(!match.find())return result;
open();String tail=match.group();Set<String> seen=new HashSet<>();
for(int length=tail.length();length>0&&result.size()<8;length--){
try(Cursor cursor=db.rawQuery("SELECT word,frequency FROM associations WHERE prefix=? ORDER BY frequency DESC,word LIMIT 8",new String[]{tail.substring(tail.length()-length)})){
while(cursor.moveToNext()&&result.size()<8){String word=cursor.getString(0);if(seen.add(word))result.add(new Candidate(word,0,cursor.getInt(1),false));}
}
}
return result;
}
public List<Candidate> lookup(String input,String mode,String context) throws Exception {
List<Candidate> result=new ArrayList<>(); List<Candidate> result=new ArrayList<>();
String pattern=mode.equals("14")?"[0-9a-d]{1,32}":mode.equals("t9")?"[2-9]{1,32}":"[a-z]{1,32}"; String pattern=mode.equals("14")?"[0-9a-d]{1,32}":mode.equals("t9")?"[2-9]{1,32}":"[a-z]{1,32}";
if(!input.matches(pattern))return result;open(); if(!input.matches(pattern))return result;open();
@@ -48,13 +63,25 @@ public final class Lexicon implements AutoCloseable {
for(int i=0;i<input.length();i++){if(i>0)marks.append(',');marks.append('?');prefixes[i]=input.substring(0,i+1);} for(int i=0;i<input.length();i++){if(i>0)marks.append(',');marks.append('?');prefixes[i]=input.substring(0,i+1);}
String order=mode.equals("py")?"length(e.pinyin) DESC,e.pinyin,w.ordinal":"length(e.pinyin) DESC,w.frequency DESC,e.pinyin,w.ordinal"; String order=mode.equals("py")?"length(e.pinyin) DESC,e.pinyin,w.ordinal":"length(e.pinyin) DESC,w.frequency DESC,e.pinyin,w.ordinal";
Set<String> seen=new HashSet<>(); Set<String> seen=new HashSet<>();
try(Cursor cursor=db.rawQuery("SELECT w.word,length(e.pinyin) FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+" IN ("+marks+") ORDER BY "+order,prefixes)) { try(Cursor cursor=db.rawQuery("SELECT w.word,length(e.pinyin),w.frequency FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+" IN ("+marks+") ORDER BY "+order,prefixes)) {
while(cursor.moveToNext()) { while(cursor.moveToNext()) {
if(Thread.currentThread().isInterrupted())return new ArrayList<>(); if(Thread.currentThread().isInterrupted())return new ArrayList<>();
String word=cursor.getString(0);int consume=cursor.getInt(1); String word=cursor.getString(0);int consume=cursor.getInt(1);
if(seen.add(word+"@"+consume))result.add(new Candidate(word,consume)); if(seen.add(word+"@"+consume))result.add(new Candidate(word,consume,cursor.getInt(2),false));
} }
} }
if(input.length()>=2){
String sql="SELECT w.word,w.frequency FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+">? AND e."+column+"<? AND length(e.pinyin)<=? ORDER BY w.frequency DESC,length(e.pinyin),e.pinyin,w.ordinal LIMIT 128";
try(Cursor cursor=db.rawQuery(sql,new String[]{input,input+'\uffff',Integer.toString(input.length()+4)})){
int added=0;
while(cursor.moveToNext()&&added<12){
if(Thread.currentThread().isInterrupted())return new ArrayList<>();
String word=cursor.getString(0);
if(seen.add(word+"@"+input.length())){result.add(new Candidate(word,input.length(),cursor.getInt(1),true));added++;}
}
}
}
CandidateRanking.sort(result,input.length(),predict(context));
return result; return result;
} }
@Override public void close() { if(db!=null){db.close();db=null;} } @Override public void close() { if(db!=null){db.close();db=null;} }
@@ -0,0 +1,29 @@
package com.sydxky.crabinput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import static org.junit.Assert.*;
public class CandidateRankingTest {
@Test public void incompleteSyllablesBeatShorterFallbackWithoutLosingThem(){
Candidate shorter=new Candidate("你",2,234587,false),complete=new Candidate("你好",4,725,true);
List<Candidate> items=new ArrayList<>(Arrays.asList(shorter,complete));
CandidateRanking.sort(items,4,Collections.emptyList());
assertSame(complete,items.get(0));assertTrue(items.contains(shorter));
}
@Test public void exactSpellingBeatsCompletionsAndUsesWordFrequency(){
Candidate common=new Candidate("世界",6,34387,false),rare=new Candidate("使节",6,497,false),longer=new Candidate("世界观",6,100000,true);
List<Candidate> items=new ArrayList<>(Arrays.asList(rare,longer,common));
CandidateRanking.sort(items,6,Collections.emptyList());
assertEquals(Arrays.asList(common,rare,longer),items);
}
@Test public void matchingContextDisambiguatesWithoutInjectingCandidates(){
Candidate plain=new Candidate("背景",7,34488,false),contextual=new Candidate("北京",7,3342,false);
List<Candidate> items=new ArrayList<>(Arrays.asList(plain,contextual));
CandidateRanking.sort(items,7,Arrays.asList(new Candidate("北京",0),new Candidate("不存在",0)));
assertSame(contextual,items.get(0));assertEquals(2,items.size());
}
}
+8 -3
查看文件
@@ -1,5 +1,5 @@
"""Losslessly build the Android SQLite asset from the licensed original V5 trie.""" """Losslessly build the Android SQLite asset from the licensed original V5 trie."""
import argparse,hashlib,json,pathlib,sqlite3 import argparse,hashlib,importlib.util,json,pathlib,sqlite3
APP=pathlib.Path(__file__).resolve().parents[1] APP=pathlib.Path(__file__).resolve().parents[1]
def build(rtos): def build(rtos):
@@ -22,6 +22,9 @@ def build(rtos):
for line in frequency_path.read_text(encoding='utf-8').splitlines(): for line in frequency_path.read_text(encoding='utf-8').splitlines():
parts=line.split(' ') parts=line.split(' ')
if len(parts)>=2 and parts[1].isdigit():frequencies[parts[0]]=int(parts[1]) if len(parts)>=2 and parts[1].isdigit():frequencies[parts[0]]=int(parts[1])
spec=importlib.util.spec_from_file_location('crab_dictionary',rtos/'scripts/convert-dictionary.py')
converter=importlib.util.module_from_spec(spec);spec.loader.exec_module(converter)
associations=converter.associations(entries,frequencies)
groups9=['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];groups14=['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m'] groups9=['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];groups14=['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m']
map9={ch:str(i+2)for i,group in enumerate(groups9)for ch in group} map9={ch:str(i+2)for i,group in enumerate(groups9)for ch in group}
map14={ch:'0123456789abcd'[i]for i,group in enumerate(groups14)for ch in group} map14={ch:'0123456789abcd'[i]for i,group in enumerate(groups14)for ch in group}
@@ -33,13 +36,15 @@ def build(rtos):
for index,(key,words)in enumerate(sorted(entries.items()),1): for index,(key,words)in enumerate(sorted(entries.items()),1):
db.execute('INSERT INTO entries VALUES(?,?,?,?)',(index,key,''.join(map9[ch]for ch in key),''.join(map14[ch]for ch in key))) db.execute('INSERT INTO entries VALUES(?,?,?,?)',(index,key,''.join(map9[ch]for ch in key),''.join(map14[ch]for ch in key)))
db.executemany('INSERT INTO words VALUES(?,?,?,?)',[(index,ordinal,word,frequencies.get(word,0))for ordinal,word in enumerate(words)]) db.executemany('INSERT INTO words VALUES(?,?,?,?)',[(index,ordinal,word,frequencies.get(word,0))for ordinal,word in enumerate(words)])
db.executescript('CREATE INDEX entries_code9 ON entries(code9);CREATE INDEX entries_code14 ON entries(code14);ANALYZE;PRAGMA user_version=1;') db.execute('CREATE TABLE associations(prefix TEXT NOT NULL,word TEXT NOT NULL,frequency INTEGER NOT NULL,PRIMARY KEY(prefix,word)) WITHOUT ROWID')
db.executemany('INSERT INTO associations VALUES(?,?,?)',[(prefix,word,frequency)for prefix,rows in associations.items()for word,frequency in rows])
db.executescript('CREATE INDEX entries_code9 ON entries(code9);CREATE INDEX entries_code14 ON entries(code14);ANALYZE;PRAGMA user_version=2;')
db.commit() db.commit()
restored={} restored={}
for key,word in db.execute('SELECT e.pinyin,w.word FROM entries e JOIN words w ON w.entry=e.id ORDER BY e.pinyin,w.ordinal'):restored.setdefault(key,[]).append(word) for key,word in db.execute('SELECT e.pinyin,w.word FROM entries e JOIN words w ON w.entry=e.id ORDER BY e.pinyin,w.ordinal'):restored.setdefault(key,[]).append(word)
assert restored==entries,'Full dictionary round-trip mismatch' assert restored==entries,'Full dictionary round-trip mismatch'
assert db.execute('PRAGMA integrity_check').fetchone()[0]=='ok';db.close();temporary.replace(target) assert db.execute('PRAGMA integrity_check').fetchone()[0]=='ok';db.close();temporary.replace(target)
report={'keys':len(entries),'candidates':sum(map(len,entries.values())),'longest_word':max(len(word)for words in entries.values()for word in words),'sha256':hashlib.sha256(target.read_bytes()).hexdigest(),'bytes':target.stat().st_size,'round_trip':True,'source_files':{name:hashlib.sha256(data).hexdigest()for name,data in sorted(files.items())},'jieba_sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()} report={'schema_version':2,'association_prefixes':len(associations),'association_candidates':sum(map(len,associations.values())),'keys':len(entries),'candidates':sum(map(len,entries.values())),'longest_word':max(len(word)for words in entries.values()for word in words),'sha256':hashlib.sha256(target.read_bytes()).hexdigest(),'bytes':target.stat().st_size,'round_trip':True,'source_files':{name:hashlib.sha256(data).hexdigest()for name,data in sorted(files.items())},'jieba_sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()}
(APP/'keyboard/src/main/assets/dictionary-manifest.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') (APP/'keyboard/src/main/assets/dictionary-manifest.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps({k:v for k,v in report.items()if k!='source_files'},ensure_ascii=False)) print(json.dumps({k:v for k,v in report.items()if k!='source_files'},ensure_ascii=False))
+13 -8
查看文件
@@ -4,6 +4,7 @@ APP=pathlib.Path(__file__).resolve().parents[1]
def main(): def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--serial',required=True);parser.add_argument('--output',type=pathlib.Path,required=True) parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--serial',required=True);parser.add_argument('--output',type=pathlib.Path,required=True)
parser.add_argument('--background-regression',action='store_true',help='Run native offscreen component checks without launching an Activity or switching the foreground')
args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True) args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True)
def adb(*command,binary=False,timeout=30,retries=3): def adb(*command,binary=False,timeout=30,retries=3):
last=None last=None
@@ -14,26 +15,30 @@ def main():
if command[0]!='install'and'device' not in last.lower()and'closed' not in last.lower():break if command[0]!='install'and'device' not in last.lower()and'closed' not in last.lower():break
print('Device connection interrupted; retrying this command.',flush=True);time.sleep(1) print('Device connection interrupted; retrying this command.',flush=True);time.sleep(1)
raise RuntimeError(last) raise RuntimeError(last)
apk=APP/'app/build/outputs/apk/release/app-release.apk';test=APP/'app/build/outputs/apk/androidTest/release/app-release-androidTest.apk' apk=APP/'app/build/outputs/apk/release/app-release.apk';test=APP/('keyboard/build/outputs/apk/androidTest/debug/keyboard-debug-androidTest.apk'if args.background_regression else'app/build/outputs/apk/androidTest/release/app-release-androidTest.apk')
assert apk.is_file()and test.is_file() assert apk.is_file()and test.is_file()
model=adb('shell','getprop','ro.product.model');sdk=adb('shell','getprop','ro.build.version.sdk') model=adb('shell','getprop','ro.product.model');sdk=adb('shell','getprop','ro.build.version.sdk')
original=args.output/'original-ime.txt' original=args.output/'original-ime.txt'
if not original.exists():original.write_text(adb('shell','settings','get','secure','default_input_method')+'\n',encoding='utf-8') if not original.exists():original.write_text(adb('shell','settings','get','secure','default_input_method')+'\n',encoding='utf-8')
host=APP/'host/build/outputs/apk/debug/host-debug.apk' host=APP/'host/build/outputs/apk/debug/host-debug.apk'
for path in [apk,test,host]:print(adb('install','-r',str(path),timeout=90),flush=True) for path in ([test]if args.background_regression else[apk,test,host]):print(adb('install','-r',str(path),timeout=90),flush=True)
before=adb('shell','settings','get','secure','default_input_method') before=adb('shell','settings','get','secure','default_input_method')
print('Default system IME retained; testing the embedded keyboard only.',flush=True) print('Default system IME retained; testing the embedded keyboard only.',flush=True)
adb('shell','input','keyevent','224');adb('shell','wm','dismiss-keyguard') if not args.background_regression:adb('shell','input','keyevent','224');adb('shell','wm','dismiss-keyguard')
# The instrumentation is bounded short acceptance, not a stress/soak run. # The instrumentation is bounded short acceptance, not a stress/soak run.
command=['adb','-s',args.serial,'shell','am','instrument','-w','-r','com.sydxky.crabinput.test/androidx.test.runner.AndroidJUnitRunner'] command=['adb','-s',args.serial,'shell','am','instrument','-w','-r']
if args.background_regression:command+=['-e','class','com.sydxky.crabinput.SurfaceRegressionTest']
command+=[('com.sydxky.crabinput.keyboard.test'if args.background_regression else'com.sydxky.crabinput.test')+'/androidx.test.runner.AndroidJUnitRunner']
result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=240) result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=240)
output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True) output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True)
passed=result.returncode==0 and 'OK (9 tests)'in output and 'FAILURES!!!'not in output expected=3 if args.background_regression else 9
report={'model':model,'sdk':sdk,'version':'1.2.2','apk_sha256':hashlib.sha256(apk.read_bytes()).hexdigest(),'instrumentation_tests':9,'embedded_component':True,'system_ime_unchanged':adb('shell','settings','get','secure','default_input_method')==before,'passed':passed} passed=result.returncode==0 and f'OK ({expected} tests)'in output and 'FAILURES!!!'not in output
report={'model':model,'sdk':sdk,'version':'1.2.3','release_apk_sha256':hashlib.sha256(apk.read_bytes()).hexdigest(),'instrumentation_tests':expected,'verification_mode':'native_offscreen_component'if args.background_regression else'interactive','interactive_tests_run':not args.background_regression,'component_variant':'debug'if args.background_regression else'release','embedded_component':True,'system_ime_unchanged':adb('shell','settings','get','secure','default_input_method')==before,'passed':passed}
(args.output/'device-validation.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') (args.output/'device-validation.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted' assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted'
adb('shell','am','start','-n','com.sydxky.crabinput/.MainActivity');time.sleep(.5) if not args.background_regression:
(args.output/'home.png').write_bytes(adb('exec-out','screencap','-p',binary=True)) adb('shell','am','start','-n','com.sydxky.crabinput/.MainActivity');time.sleep(.5)
(args.output/'home.png').write_bytes(adb('exec-out','screencap','-p',binary=True))
print(json.dumps(report),flush=True) print(json.dumps(report),flush=True)
if __name__=='__main__':main() if __name__=='__main__':main()
+2
查看文件
@@ -4,6 +4,8 @@
正在编辑的文本仅在本次页面的内存中。主动“保存草稿”后,普通文本保存于本应用本地存储,供下次启动恢复;可清空全文后重新保存以删除旧草稿,卸载应用也会清除该存储。私密输入不提供保存、不参与学习,切换其他应用或销毁页面时清空编辑状态。 正在编辑的文本仅在本次页面的内存中。主动“保存草稿”后,普通文本保存于本应用本地存储,供下次启动恢复;可清空全文后重新保存以删除旧草稿,卸载应用也会清除该存储。私密输入不提供保存、不参与学习,切换其他应用或销毁页面时清空编辑状态。
离线上下文联想仅使用本次编辑光标前最多4个连续汉字,不跨标点或非中文字符,不保存联想上下文;密码/私密输入不参与。续写索引来自已附许可的现有词库与本项目自编的少量常用短语,不增加网络请求。
候选学习默认关闭。开启后仅在本应用保存最多 100 组编码、每组三个主动选择的真实候选;不会保存全文。在编辑页可关闭学习或“清除学习”。关闭学习不会自动删除旧记录,可通过清除按钮删除。 候选学习默认关闭。开启后仅在本应用保存最多 100 组编码、每组三个主动选择的真实候选;不会保存全文。在编辑页可关闭学习或“清除学习”。关闭学习不会自动删除旧记录,可通过清除按钮删除。
嵌入其他 Glyphix 应用后,键盘数据保存在该宿主自己的隔离空间。完成输入会将文本回填给宿主;宿主后续的网络行为适用其自身说明。腕上浏览的兼容模式会在明确告知后把网页输入交给服务器,详见其独立隐私政策。 嵌入其他 Glyphix 应用后,键盘数据保存在该宿主自己的隔离空间。完成输入会将文本回填给宿主;宿主后续的网络行为适用其自身说明。腕上浏览的兼容模式会在明确告知后把网页输入交给服务器,详见其独立隐私政策。
+4 -2
查看文件
@@ -2,7 +2,9 @@
**蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:<https://git2.sydxky.cn/sydxky/crab-input-rtos>;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。 **蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:<https://git2.sydxky.cn/sydxky/crab-input-rtos>;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。
独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.2 / 10202`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.3 / 10203`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。
1.2.3 新增未完成拼音补全、离线上下文续写和词频排序;查询期间保留候选,避免先清空再重画。验证范围见[本版验收](./VALIDATION_1.2.3.md)。
1.2.2 修复文字与键盘之间的多余空隙,圆屏按键上移后同步检查安全边界;本版验证范围见[间距修复验收](./VALIDATION_1.2.2.md)。 1.2.2 修复文字与键盘之间的多余空隙,圆屏按键上移后同步检查安全边界;本版验证范围见[间距修复验收](./VALIDATION_1.2.2.md)。
@@ -18,7 +20,7 @@
- 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。 - 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。
- 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。 - 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。
- 第一行显示三个候选。点击右侧下箭头按每页四项查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。拼音候选保留每组原始顺序;九宫和14键歧义通过本地 jieba 0.42.1 词频排序,学习偏好可优先排序。 - 第一行显示三个候选。点击右侧下箭头按4×4自适应分页查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。完整拼音、九宫和14键先匹配完整编码,再提供未完成拼音补全和较短前缀;通过本地 jieba 0.42.1 词频及当前光标前的中文上下文排序,学习偏好可优先排序。
- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。 - 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。
- 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。 - 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。
- 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。 - 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。
@@ -0,0 +1,7 @@
# RTOS 1.2.3 联想和刷新验收
2026-11-14,版本1.2.3/10203。完整匹配、未完成拼音补全、较短前缀分层,结合词频与光标前中文上下文排序。续写最多8项、上下文最多4个连续汉字;预测分片最大7163字节,沿用4分片/90000字符缓存预算,原始104,837组索引及163,238候选保留。隐私输入不提供上下文,原本可选的主动候选学习保持不变。
53项引擎和42项UX脚本断言通过,包含三模式补全、你好后续写世界、空格及换行结束联想且不自动采纳、候选加载期间不清空、不允许旧候选误提交、隐藏后的异步隔离和密码上下文隔离。厂商gx v0.10.1/3ed38835、8192KiB内存,普通/QuickApp圆方四配置构建通过。
本次原生模拟器启动被已占用的Inspector14201拒绝,未接管或终止他人模拟器,未把历史24视觉状态/84交互结果记为本版结果。RTOS真机、原生视觉、长期功耗和商店审核尚未验收。
@@ -7,6 +7,40 @@ T9 = {letter: str(i + 2) for i, group in enumerate(GROUPS) for letter in group}
GROUPS14 = ['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m'] GROUPS14 = ['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m']
KEY14 = {letter: '0123456789abcd'[i] for i, group in enumerate(GROUPS14) for letter in group} KEY14 = {letter: '0123456789abcd'[i] for i, group in enumerate(GROUPS14) for letter in group}
def predictive_bucket(value):
result=0
for ch in value:result=(result*31+ord(ch))&0xffff
return format(result%256,'02x')
def associations(entries,frequencies):
"""Derive bounded word continuations from licensed phrases; no user corpus."""
result={}
for word in {word for words in entries.values()for word in words}:
frequency=frequencies.get(word,0)
if frequency<20 or not re.fullmatch(r'[\u3400-\u9fff]{2,12}',word):continue
for cut in range(1,min(4,len(word)-1)+1):
prefix,suffix=word[:cut],word[cut:]
result.setdefault(prefix,{})[suffix]=round(frequency/(1+.25*len(suffix)))
# Small, original conversational defaults supplement dictionary word splits.
seeds=json.loads((APP/'src/common/association-seeds.json').read_text(encoding='utf-8'))
for prefix,words in seeds.items():
for rank,word in enumerate(words):result.setdefault(prefix,{})[word]=200000-rank*10000
return {prefix:sorted(words.items(),key=lambda x:(-x[1],x[0]))[:8]for prefix,words in sorted(result.items())}
def completions(entries,frequencies):
result={}
for key,words in entries.items():
for mode,code in [('py',key),('t9',''.join(T9[ch]for ch in key)),('14',''.join(KEY14[ch]for ch in key))]:
for length in range(2,min(3 if mode!='14'else 2,len(code)-1)+1):
if len(code)-length>4:continue
target=result.setdefault(mode+':'+code[:length],{})
for ordinal,word in enumerate(words):
frequency=frequencies.get(word,0)
if frequency<20:continue
value=[word,key,frequency,ordinal]
if word not in target or target[word][2]<frequency:target[word]=value
return {prefix:sorted(words.values(),key=lambda x:(-x[2],len(x[1]),x[3],x[0]))[:12]for prefix,words in sorted(result.items())}
def from94(value): def from94(value):
number = 0 number = 0
for ch in value: for ch in value:
@@ -57,6 +91,11 @@ def convert(source, target):
output=target/(name+'.json') output=target/(name+'.json')
data=json.dumps({'entries':content,'frequencies':weights}, ensure_ascii=False, separators=(',', ':')).encode('utf-8') data=json.dumps({'entries':content,'frequencies':weights}, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
if not output.exists() or output.read_bytes()!=data:output.write_bytes(data) if not output.exists() or output.read_bytes()!=data:output.write_bytes(data)
association_shards={};completion_shards={}
for prefix,items in associations(entries,frequencies).items():association_shards.setdefault('a'+predictive_bucket(prefix),{})[prefix]=items
for prefix,items in completions(entries,frequencies).items():completion_shards.setdefault('c'+predictive_bucket(prefix),{})[prefix]=items
for name,content in {**association_shards,**completion_shards}.items():
(target/(name+'.json')).write_text(json.dumps(content,ensure_ascii=False,separators=(',',':')),encoding='utf-8',newline='\n')
# Every original candidate remains accessible, in original per-key order. # Every original candidate remains accessible, in original per-key order.
restored = {};restored14={} restored = {};restored14={}
for name in shards: for name in shards:
@@ -69,6 +108,8 @@ def convert(source, target):
t9_shards=sum(not name.startswith('d')for name in shards), t9_shards=sum(not name.startswith('d')for name in shards),
key14_shards=sum(name.startswith('d')for name in shards),key14_groups=GROUPS14,key14_prefix_length=3, key14_shards=sum(name.startswith('d')for name in shards),key14_groups=GROUPS14,key14_prefix_length=3,
largest_shard_bytes=max((target/(name+'.json')).stat().st_size for name in shards), largest_shard_bytes=max((target/(name+'.json')).stat().st_size for name in shards),
association_shards=sorted(association_shards),completion_shards=sorted(completion_shards),
predictive_largest_shard_bytes=max((target/(name+'.json')).stat().st_size for name in [*association_shards,*completion_shards]),
source_files={name:hashlib.sha256(data).hexdigest() for name,data in sorted(files.items())}) source_files={name:hashlib.sha256(data).hexdigest() for name,data in sorted(files.items())})
if frequency_path.exists():report['t9_frequency_source']={'project':'jieba 0.42.1','license':'MIT','sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()} if frequency_path.exists():report['t9_frequency_source']={'project':'jieba 0.42.1','license':'MIT','sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()}
output=target/'meta.json';data=(json.dumps(report, ensure_ascii=False, indent=2)+'\n').encode('utf-8') output=target/'meta.json';data=(json.dumps(report, ensure_ascii=False, indent=2)+'\n').encode('utf-8')
@@ -0,0 +1,32 @@
{
"你好": ["世界", "呀", "啊"],
"谢谢": ["你", "大家", "支持"],
"不客": ["气"],
"早上": ["好"],
"晚上": ["好"],
"晚安": ["啦"],
"再": ["见"],
"生日": ["快乐"],
"新年": ["快乐"],
"今天": ["天气", "下午", "晚上"],
"明天": ["见", "早上", "下午"],
"天气": ["很好", "不错"],
"我": ["想", "要", "是", "们"],
"我们": ["一起", "可以", "去"],
"你": ["好", "的", "们"],
"请": ["问", "稍等", "帮忙"],
"没": ["关系", "问题"],
"没有": ["问题", "关系"],
"可以": ["的", "吗"],
"知道": ["了"],
"收到": ["了"],
"我爱": ["你"],
"人工": ["智能"],
"中国": ["人民", "文化"],
"北京": ["大学", "天气"],
"正在": ["输入", "加载"],
"输入": ["法", "文字"],
"手表": ["应用", "设置"],
"周末": ["快乐", "愉快"],
"工作": ["顺利"]
}
@@ -26,6 +26,8 @@ export function digitsOf(text) {
return result return result
} }
export function chars(text){return Array.from(String(text||''))} export function chars(text){return Array.from(String(text||''))}
export function contextTail(text){const match=String(text||'').match(/[\u3400-\u9fff]{1,4}$/);return match?match[0]:''}
function predictiveBucket(value){let result=0;for(let i=0;i<value.length;i++)result=(result*31+value.charCodeAt(i))&65535;return (result%256+256).toString(16).slice(1)}
// Four columns and four row units. Phrases take more width/height, never ellipsis. // Four columns and four row units. Phrases take more width/height, never ellipsis.
export function candidatePages(candidates) { export function candidatePages(candidates) {
const pages=[];let rows=[],usedRows=0,row=null const pages=[];let rows=[],usedRows=0,row=null
@@ -93,15 +95,28 @@ export function createDictionary(read) {
})() })()
try{return await pending[name]}finally{delete pending[name]} try{return await pending[name]}finally{delete pending[name]}
} }
async function metadata(){if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta}return manifest}
async function predict(context){
const tail=contextTail(context);if(!tail)return []
const meta=await metadata(),result=[],seen={}
for(let length=tail.length;length>0&&result.length<8;length--){
const prefix=tail.slice(-length),name='a'+predictiveBucket(prefix)
if(!meta.association_shards||meta.association_shards.indexOf(name)<0)continue
const rows=(await load(name))[prefix]||[]
for(const row of rows)if(!seen[row[0]]){seen[row[0]]=true;result.push({word:row[0],pinyin:'',consume:0,frequency:row[1]});if(result.length===8)break}
}
return result
}
return { return {
async lookup(raw,t9,preferences) { predict,
async lookup(raw,t9,preferences,context) {
const input=String(raw||'').toLowerCase() const input=String(raw||'').toLowerCase()
const dual=t9==='14',ambiguous=!!t9 const dual=t9==='14',ambiguous=!!t9
if(!(dual?/^[0-9a-d]{1,32}$/:t9?/^[2-9]{1,32}$/:/^[a-z]{1,32}$/).test(input))return [] if(!(dual?/^[0-9a-d]{1,32}$/:t9?/^[2-9]{1,32}$/:/^[a-z]{1,32}$/).test(input))return []
const digits=ambiguous?input:digitsOf(input) const digits=ambiguous?input:digitsOf(input)
const merged={},frequencies={} const merged={},frequencies={}
function merge(data){Object.assign(merged,data.entries);Object.assign(frequencies,data.frequencies)} function merge(data){Object.assign(merged,data.entries);Object.assign(frequencies,data.frequencies)}
if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta} await metadata()
const names=[] const names=[]
const prefixLength=dual?3:4 const prefixLength=dual?3:4
for(let length=1;length<=Math.min(prefixLength-1,digits.length);length++)names.push((dual?'ds':'s')+digits.slice(0,length)) for(let length=1;length<=Math.min(prefixLength-1,digits.length);length++)names.push((dual?'ds':'s')+digits.slice(0,length))
@@ -112,13 +127,32 @@ export function createDictionary(read) {
const result=[],seen={} const result=[],seen={}
for(const key of matching)for(const word of merged[key]){ for(const key of matching)for(const word of merged[key]){
const identity=word+'@'+key.length const identity=word+'@'+key.length
if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length})} if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length,frequency:frequencies[word]||0})}
}
if(input.length>=2){
const mode=dual?'14':t9?'t9':'py',prefix=mode+':'+input,extras=[]
if(input.length<prefixLength){
const name='c'+predictiveBucket(prefix)
if(manifest.completion_shards&&manifest.completion_shards.indexOf(name)>=0){
for(const row of (await load(name))[prefix]||[])extras.push({word:row[0],pinyin:row[1],frequency:row[2],missing:row[1].length-input.length,consume:input.length,completion:true})
}
}else for(const key of Object.keys(merged)){
const code=dual?code14Of(key):t9?digitsOf(key):key
if(code.length<=input.length||code.length>input.length+4||code.indexOf(input)!==0)continue
for(const word of merged[key])extras.push({word,pinyin:key,frequency:frequencies[word]||0,missing:key.length-input.length,consume:input.length,completion:true})
}
extras.sort((a,b)=>b.frequency-a.frequency||a.missing-b.missing)
let added=0
for(const item of extras){const identity=item.word+'@'+item.consume;if(!seen[identity]){seen[identity]=true;result.push(item);if(++added===12)break}}
} }
const preferenceKey=dual?'k14:'+input:input const preferenceKey=dual?'k14:'+input:input
const prefs=preferences&&Array.isArray(preferences[preferenceKey])?preferences[preferenceKey]:[] const prefs=preferences&&Array.isArray(preferences[preferenceKey])?preferences[preferenceKey]:[]
const rank=item=>{const i=prefs.findIndex(x=>x.word===item.word&&x.consume===item.consume);return i<0?100:i} const rank=item=>{const i=prefs.findIndex(x=>x.word===item.word&&x.consume===item.consume);return i<0?100:i}
// Rank only real dictionary results: a damaged preference cannot inject text. const predictions=await predict(context),contextRank=item=>{const i=predictions.findIndex(x=>x.word===item.word);return i<0?0:8-i}
return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||(ambiguous?(b.item.consume-a.item.consume||(frequencies[b.item.word]||0)-(frequencies[a.item.word]||0)):0)||a.index-b.index).map(x=>x.item) const category=item=>item.completion?1:item.consume===input.length?0:2
// Preserve every exact candidate; complete unfinished syllables before
// shorter fallbacks, and use word frequency plus the editor's local context.
return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||category(a.item)-category(b.item)||b.item.consume-a.item.consume||contextRank(b.item)-contextRank(a.item)||(b.item.frequency||0)-(a.item.frequency||0)||a.index-b.index).map(x=>x.item)
}, },
clear(){cache.length=0;manifest=null}, clear(){cache.length=0;manifest=null},
get cacheSize(){return cache.length} get cacheSize(){return cache.length}
@@ -3,16 +3,17 @@ export function normalizeTheme(value) { return value === 'blackgreen' ? 'blackgr
export function themeStyles(value) { export function themeStyles(value) {
const dark = normalizeTheme(value) === 'blackgreen' const dark = normalizeTheme(value) === 'blackgreen'
const c = dark const c = dark
? {bg:'#000000',card:'#1C1C1E',raised:'#2C2C2E',text:'#F2F7FA',muted:'#B3B3B8',dim:'#A0A0A7',accent:'#00C777',user:'#0E5C44',line:'#3C3C40',danger:'#FF6978'} ? {bg:'#000000',card:'#1C1C1E',raised:'#2C2C2E',text:'#F2F7FA',muted:'#B3B3B8',dim:'#707075',accent:'#00C777',user:'#0E5C44',line:'#3C3C40',danger:'#FF6978'}
: {bg:'#ECEEF2',card:'#FFFFFF',raised:'#F8F9FB',text:'#20232B',muted:'#737984',dim:'#737984',accent:'#2867D9',user:'#2867D9',line:'#D9DDE4',danger:'#B5424C'} : {bg:'#ECEEF2',card:'#FFFFFF',raised:'#F8F9FB',text:'#20232B',muted:'#737984',dim:'#737984',accent:'#2867D9',user:'#2867D9',line:'#D9DDE4',danger:'#B5424C'}
const ink='color:'+c.text+';',muted='color:'+c.muted+';' const ink='color:'+c.text+';',muted='color:'+c.muted+';'
return { return {
surface:'background-color:'+c.bg+';'+ink, surface:'background-color:'+c.bg+';'+ink,
card:'background-color:'+c.card+';'+ink, card:'background-color:'+c.card+';'+ink,
raised:'background-color:'+c.raised+';'+ink, raised:'background-color:'+c.raised+';'+ink,
cyan:'color:'+(dark?'#4DB5FF':'#2867D9')+';',
ink,muted,dim:'color:'+c.dim+';',accent:'color:'+c.accent+';', ink,muted,dim:'color:'+c.dim+';',accent:'color:'+c.accent+';',
primary:'background-color:'+c.accent+';color:'+(dark?'#000000':'#FFFFFF')+';', primary:'background-color:'+c.accent+';color:'+(dark?'#000000':'#FFFFFF')+';',
secondary:'background-color:'+c.card+';color:'+c.accent+';', secondary:'background-color:'+c.raised+';'+ink,
user:'background-color:'+c.user+';color:#FFFFFF;',white:'color:#FFFFFF;', user:'background-color:'+c.user+';color:#FFFFFF;',white:'color:#FFFFFF;',
input:'background-color:'+c.card+';'+ink+'border-bottom:2px solid '+c.line+';', input:'background-color:'+c.card+';'+ink+'border-bottom:2px solid '+c.line+';',
plain:'background-color:transparent;'+ink, plain:'background-color:transparent;'+ink,
@@ -56,14 +56,14 @@
import storage from '@system.storage' import storage from '@system.storage'
import router from '@system.router' import router from '@system.router'
import {normalizeTheme, themeStyles as stylesForTheme} from '../common/ime-theme.js' import {normalizeTheme, themeStyles as stylesForTheme} from '../common/ime-theme.js'
import {createDictionary,chars,candidatePages,edit,erase,preview,learn,sanitizePreferences,KEY14_GROUPS,display14} from '../common/ime-engine.js' import {createDictionary,chars,contextTail,candidatePages,edit,erase,preview,learn,sanitizePreferences,KEY14_GROUPS,display14} from '../common/ime-engine.js'
const LETTERS=['qwertyuiop','asdfghjkl','zxcvbnm'] const LETTERS=['qwertyuiop','asdfghjkl','zxcvbnm']
const SYMBOLS=['!"#$%&',"'()*+,",'-./:;<','=>?@[\\',']^_`{|','}~,。?!'] const SYMBOLS=['!"#$%&',"'()*+,",'-./:;<','=>?@[\\',']^_`{|','}~,。?!']
function keys(rows){return rows.map(row=>Array.from(row).map(value=>({label:value,value:value,sub:''})))} function keys(rows){return rows.map(row=>Array.from(row).map(value=>({label:value,value:value,sub:''})))}
export default { export default {
data:{titleLabel:'蟹蟹输入',inputTheme:'bluewhite',themeStyles:stylesForTheme('bluewhite'),initial:'',title:'蟹蟹输入',inputType:'text',maxLength:2048,allowNewline:true,secret:false,text:'',cursor:0,composition:'',compositionDisplay:'',display:'│',count:0,limit:2048,mode:'t9',modeLabel:'九宫拼音',upper:false,symbolPage:0,panel:'keys',keyRows:[],layoutRows:[],keyStyle:'',labelStyle:'',bar:[],visibleCandidates:[],candidateRows:[],total:0,candidatePage:0,pageCount:1,busy:false,hint:'离线全量词库',learning:false,clearArmed:false}, data:{titleLabel:'蟹蟹输入',inputTheme:'bluewhite',themeStyles:stylesForTheme('bluewhite'),initial:'',title:'蟹蟹输入',inputType:'text',maxLength:2048,allowNewline:true,secret:false,revealSecret:false,text:'',cursor:0,composition:'',compositionDisplay:'',display:'│',count:0,limit:2048,mode:'t9',modeLabel:'九宫拼音',upper:false,symbolPage:0,panel:'keys',keyRows:[],layoutRows:[],keyStyle:'',labelStyle:'',bar:[],visibleCandidates:[],candidateRows:[],total:0,candidatePage:0,pageCount:1,busy:false,hint:'离线全量词库',learning:false,clearArmed:false},
dictionary:null,preferences:{},allCandidates:[],pages:[],active:true,life:null,requestId:0,lookupTimer:null,repeatTimer:null,repeatDelay:null,result:null,original:'', dictionary:null,preferences:{},allCandidates:[],pages:[],prediction:false,active:true,life:null,requestId:0,lookupTimer:null,repeatTimer:null,repeatDelay:null,result:null,original:'',
onInit(){ onInit(){
this.titleLabel=chars(this.title).slice(0,4).join('')+(chars(this.title).length>4?'…':'');this.inputTheme=normalizeTheme(this.inputTheme);this.themeStyles=stylesForTheme(this.inputTheme) this.titleLabel=chars(this.title).slice(0,4).join('')+(chars(this.title).length>4?'…':'');this.inputTheme=normalizeTheme(this.inputTheme);this.themeStyles=stylesForTheme(this.inputTheme)
this.life={active:true,id:0};this.secret=!!this.secret||this.inputType==='password' this.life={active:true,id:0};this.secret=!!this.secret||this.inputType==='password'
@@ -74,12 +74,14 @@
this.mode=this.inputType==='number'||this.inputType==='pin'?'num':this.inputType==='url'||this.inputType==='password'?'en':'t9' this.mode=this.inputType==='number'||this.inputType==='pin'?'num':this.inputType==='url'||this.inputType==='password'?'en':'t9'
this.renderKeys();this.updateDisplay() this.renderKeys();this.updateDisplay()
}, },
onShow(){this.active=true;this.life.active=true;if(this.composition)this.schedule()}, onShow(){this.active=true;this.life.active=true;this.schedule()},
onHide(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.secret)this.clearPrivate()}, onHide(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.secret)this.clearPrivate()},
clearPrivate(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();this.text='';this.initial='';this.original='';this.composition='';this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.updateDisplay()}, clearPrivate(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();this.text='';this.initial='';this.original='';this.composition='';this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.updateDisplay()},
onDestroy(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.dictionary)this.dictionary.clear();this.allCandidates=[];this.preferences={};this.text='';this.initial='';this.original='';router.close(this.$page,this.result||{accepted:false});this.result=null}, onDestroy(){this.active=false;this.life.active=false;this.invalidate();this.stopRepeat();if(this.dictionary)this.dictionary.clear();this.allCandidates=[];this.preferences={};this.text='';this.initial='';this.original='';router.close(this.$page,this.result||{accepted:false});this.result=null},
invalidate(){this.requestId++;this.life.id=this.requestId;if(this.lookupTimer!==null){clearTimeout(this.lookupTimer);this.lookupTimer=null}this.busy=false}, invalidate(){this.requestId++;this.life.id=this.requestId;if(this.lookupTimer!==null){clearTimeout(this.lookupTimer);this.lookupTimer=null}this.busy=false},
updateDisplay(){this.count=chars(this.text).length;this.display=preview(this.text,this.cursor,this.secret);this.compositionDisplay=this.mode==='14'?display14(this.composition):this.composition}, updateDisplay(){const count=chars(this.text).length,display=preview(this.text,this.cursor,this.secret&&!this.revealSecret),composition=this.mode==='14'?display14(this.composition):this.composition;if(this.count!==count)this.count=count;if(this.display!==display)this.display=display;if(this.compositionDisplay!==composition)this.compositionDisplay=composition},
inputContext(){return this.secret?'':contextTail(chars(this.text).slice(0,this.cursor).join(''))},
clearCandidates(){this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.pageCount=1},
renderKeys(){ renderKeys(){
if(this.mode==='t9')this.keyRows=[[{label:'标点',value:'symbols',sub:'1'},{label:'ABC',value:'2',sub:'2'},{label:'DEF',value:'3',sub:'3'}],[{label:'GHI',value:'4',sub:'4'},{label:'JKL',value:'5',sub:'5'},{label:'MNO',value:'6',sub:'6'}],[{label:'PQRS',value:'7',sub:'7'},{label:'TUV',value:'8',sub:'8'},{label:'WXYZ',value:'9',sub:'9'}]] if(this.mode==='t9')this.keyRows=[[{label:'标点',value:'symbols',sub:'1'},{label:'ABC',value:'2',sub:'2'},{label:'DEF',value:'3',sub:'3'}],[{label:'GHI',value:'4',sub:'4'},{label:'JKL',value:'5',sub:'5'},{label:'MNO',value:'6',sub:'6'}],[{label:'PQRS',value:'7',sub:'7'},{label:'TUV',value:'8',sub:'8'},{label:'WXYZ',value:'9',sub:'9'}]]
else if(this.mode==='num')this.keyRows=keys(['123','456','789','.0-']) else if(this.mode==='num')this.keyRows=keys(['123','456','789','.0-'])
@@ -107,37 +109,40 @@
}, },
schedule(){ schedule(){
this.compositionDisplay=this.mode==='14'?display14(this.composition):this.composition this.compositionDisplay=this.mode==='14'?display14(this.composition):this.composition
this.invalidate();this.allCandidates=[];this.bar=[];this.visibleCandidates=[];this.candidateRows=[];this.pages=[];this.total=0;this.pageCount=1;this.candidatePage=0 this.invalidate();this.candidatePage=0
if(!this.composition||!this.active)return const chinese=this.mode==='t9'||this.mode==='py'||this.mode==='14',prediction=!this.composition
const id=this.requestId;this.busy=true;this.hint='正在查词…' if(!this.active||!chinese||(!this.composition&&!this.inputContext())){this.clearCandidates();this.prediction=false;return}
if(prediction!==this.prediction)this.clearCandidates()
this.prediction=prediction
const id=this.requestId;this.busy=true;if(!this.bar.length)this.hint=prediction?'联想…':'正在查词…'
this.lookupTimer=setTimeout(()=>{this.lookupTimer=null;this.query(id)},45) this.lookupTimer=setTimeout(()=>{this.lookupTimer=null;this.query(id)},45)
}, },
async query(id){ async query(id){
const raw=this.composition,life=this.life const raw=this.composition,life=this.life,context=this.inputContext()
try{const result=await this.dictionary.lookup(raw,this.mode==='14'?'14':this.mode==='t9',this.learning?this.preferences:{}) try{const result=raw?await this.dictionary.lookup(raw,this.mode==='14'?'14':this.mode==='t9',this.learning?this.preferences:{},context):await this.dictionary.predict(context)
if(!life.active||id!==life.id)return if(!life.active||id!==life.id)return
this.allCandidates=result;this.total=result.length;this.bar=result.slice(0,3);this.pages=candidatePages(result);this.pageCount=Math.max(1,this.pages.length);this.pageCandidates();this.hint=result.length?result.length+' 个候选':'暂无匹配 · 可退格修改' this.prediction=!raw;this.allCandidates=result;this.total=result.length;this.bar=result.slice(0,3);this.pages=candidatePages(result);this.pageCount=Math.max(1,this.pages.length);this.candidatePage=0;this.pageCandidates();this.hint=result.length?(!raw?'联想 · ':'')+result.length+' 个候选':raw?'暂无匹配 · 可退格修改':'离线输入'
}catch(error){if(life.active&&id===life.id)this.hint='词库暂不可用,可切换英文输入'} }catch(error){if(life.active&&id===life.id){this.clearCandidates();this.hint='词库暂不可用,可切换英文输入'}}
finally{if(life.active&&id===life.id)this.busy=false} finally{if(life.active&&id===life.id)this.busy=false}
}, },
pageCandidates(){const rows=this.pages[this.candidatePage]||[];this.visibleCandidates=[];this.candidateRows=rows.map(row=>({style:'height:'+(row.units*43)+'px;width:'+(row.slots*25)+'%',items:row.items.map(entry=>{this.visibleCandidates.push(entry.item);return {item:entry.item,style:'flex:'+entry.span+';height:'+(row.units*43-4)+'px;'+this.themeStyles.card,labelStyle:'font-size:'+(entry.span===1?25:entry.span===2?23:20)+'px;'+this.themeStyles.ink}})}))}, pageCandidates(){const rows=this.pages[this.candidatePage]||[];this.visibleCandidates=[];this.candidateRows=rows.map(row=>({style:'height:'+(row.units*43)+'px;width:'+(row.slots*25)+'%',items:row.items.map(entry=>{this.visibleCandidates.push(entry.item);return {item:entry.item,style:'flex:'+entry.span+';height:'+(row.units*43-4)+'px;'+this.themeStyles.card,labelStyle:'font-size:'+(entry.span===1?25:entry.span===2?23:20)+'px;'+this.themeStyles.ink}})}))},
choose(item){ choose(item){
if(this.busy||!this.composition||!this.allCandidates.some(x=>x.word===item.word&&x.pinyin===item.pinyin&&x.consume===item.consume))return if(this.busy||(!this.composition&&(!this.prediction||item.consume!==0))||!this.allCandidates.some(x=>x.word===item.word&&x.pinyin===item.pinyin&&x.consume===item.consume))return
if(chars(this.text).length+chars(item.word).length>this.limit){this.hint='剩余空间不足,未截断候选词';return} if(chars(this.text).length+chars(item.word).length>this.limit){this.hint='剩余空间不足,未截断候选词';return}
const raw=this.composition;this.insert(item.word) const raw=this.composition;this.insert(item.word)
if(this.learning&&!this.secret){try{this.preferences=learn(this.preferences,this.mode==='14'?'k14:'+raw:raw,item);storage.set('crab.ime.preferences',this.preferences)}catch(error){this.learning=false}} if(raw&&this.learning&&!this.secret){try{this.preferences=learn(this.preferences,this.mode==='14'?'k14:'+raw:raw,item);storage.set('crab.ime.preferences',this.preferences)}catch(error){this.learning=false}}
this.composition=raw.slice(item.consume);this.panel='keys';this.schedule() this.composition=raw.slice(item.consume);this.panel='keys';this.schedule()
}, },
expand(){this.panel='candidates';this.pageCandidates()},previousPage(){this.candidatePage=Math.max(0,this.candidatePage-1);this.pageCandidates()},nextPage(){this.candidatePage=Math.min(this.pageCount-1,this.candidatePage+1);this.pageCandidates()}, expand(){this.panel='candidates';this.pageCandidates()},previousPage(){this.candidatePage=Math.max(0,this.candidatePage-1);this.pageCandidates()},nextPage(){this.candidatePage=Math.min(this.pageCount-1,this.candidatePage+1);this.pageCandidates()},
backspace(){this.clearArmed=false;if(this.composition){this.composition=this.composition.slice(0,-1);this.schedule()}else{const next=erase(this.text,this.cursor);this.text=next.text;this.cursor=next.cursor;this.updateDisplay()}}, backspace(){this.clearArmed=false;if(this.composition){this.composition=this.composition.slice(0,-1);this.schedule()}else{const next=erase(this.text,this.cursor);this.text=next.text;this.cursor=next.cursor;this.updateDisplay();this.schedule()}},
startDelete(){this.stopRepeat();this.backspace();this.repeatDelay=setTimeout(()=>{this.repeatDelay=null;this.repeatTimer=setInterval(()=>this.backspace(),120)},450)}, startDelete(){this.stopRepeat();this.backspace();this.repeatDelay=setTimeout(()=>{this.repeatDelay=null;this.repeatTimer=setInterval(()=>this.backspace(),120)},450)},
endDelete(){this.stopRepeat()}, endDelete(){this.stopRepeat()},
stopRepeat(){if(this.repeatTimer!==null){clearInterval(this.repeatTimer);this.repeatTimer=null}if(this.repeatDelay!==null){clearTimeout(this.repeatDelay);this.repeatDelay=null}}, stopRepeat(){if(this.repeatTimer!==null){clearInterval(this.repeatTimer);this.repeatTimer=null}if(this.repeatDelay!==null){clearTimeout(this.repeatDelay);this.repeatDelay=null}},
space(){if(this.composition){if(!this.busy&&this.allCandidates.length)this.choose(this.allCandidates[0]);else this.hint='请等待候选或修改拼音'}else this.insert(' ')}, space(){if(this.composition){if(!this.busy&&this.allCandidates.length)this.choose(this.allCandidates[0]);else this.hint='请等待候选或修改拼音'}else{this.insert(' ');this.schedule()}},
rawInput(){if((this.mode==='t9'||this.mode==='14')&&this.composition){this.hint='编码请先选择候选';return false}if(this.composition){if(!this.insert(this.composition))return false;this.composition='';this.schedule()}return true}, rawInput(){if((this.mode==='t9'||this.mode==='14')&&this.composition){this.hint='编码请先选择候选';return false}if(this.composition){if(!this.insert(this.composition))return false;this.composition='';this.schedule()}return true},
showTools(){this.stopRepeat();this.panel='tools';this.clearArmed=false},closePanel(){this.panel='keys';this.clearArmed=false}, showTools(){this.stopRepeat();this.panel='tools';this.clearArmed=false},closePanel(){this.panel='keys';this.clearArmed=false},
cursorLeft(){this.cursor=Math.max(0,this.cursor-1);this.updateDisplay()},cursorRight(){this.cursor=Math.min(chars(this.text).length,this.cursor+1);this.updateDisplay()},cursorStart(){this.cursor=0;this.updateDisplay()},cursorEnd(){this.cursor=chars(this.text).length;this.updateDisplay()}, cursorLeft(){this.cursor=Math.max(0,this.cursor-1);this.updateDisplay();this.schedule()},cursorRight(){this.cursor=Math.min(chars(this.text).length,this.cursor+1);this.updateDisplay();this.schedule()},cursorStart(){this.cursor=0;this.updateDisplay();this.schedule()},cursorEnd(){this.cursor=chars(this.text).length;this.updateDisplay();this.schedule()},
newline(){if(this.allowNewline&&this.inputType!=='url'&&this.inputType!=='number'&&!this.secret)this.insert('\n');else this.hint='此输入项不支持换行'}, newline(){if(this.allowNewline&&this.inputType!=='url'&&this.inputType!=='number'&&!this.secret){this.insert('\n');this.schedule()}else this.hint='此输入项不支持换行'},
clearText(){if(!this.clearArmed){this.clearArmed=true;this.hint='再次点击确认清空';return}this.text='';this.cursor=0;this.composition='';this.clearArmed=false;this.schedule();this.updateDisplay()}, clearText(){if(!this.clearArmed){this.clearArmed=true;this.hint='再次点击确认清空';return}this.text='';this.cursor=0;this.composition='';this.clearArmed=false;this.schedule();this.updateDisplay()},
toggleLearning(){if(this.secret){this.hint='私密输入不记录学习';return}try{storage.set('crab.ime.learning',!this.learning);this.learning=!this.learning;this.schedule()}catch(error){this.learning=false;this.hint='本机存储不可用,学习已关闭'}}, toggleLearning(){if(this.secret){this.hint='私密输入不记录学习';return}try{storage.set('crab.ime.learning',!this.learning);this.learning=!this.learning;this.schedule()}catch(error){this.learning=false;this.hint='本机存储不可用,学习已关闭'}},
clearPreferences(){try{storage.delete('crab.ime.preferences');this.preferences={};this.schedule();this.hint='本应用学习记录已清除'}catch(error){this.hint='学习清除失败,请稍后重试'}}, clearPreferences(){try{storage.delete('crab.ime.preferences');this.preferences={};this.schedule();this.hint='本应用学习记录已清除'}catch(error){this.hint='学习清除失败,请稍后重试'}},
@@ -2,8 +2,8 @@
"package": "com.sydxky.rtos.inputmethod", "package": "com.sydxky.rtos.inputmethod",
"name": "蟹蟹输入", "name": "蟹蟹输入",
"icon": "/assets/icon.png", "icon": "/assets/icon.png",
"versionName": "1.2.2", "versionName": "1.2.3",
"versionCode": 10202, "versionCode": 10203,
"features": [], "features": [],
"router": { "router": {
"entry": "main", "entry": "main",
@@ -30,6 +30,7 @@ p.choose({...hello});check(p.text==='你好'&&!p.composition,'candidate event pa
p.finish();check(closed.at(-1).accepted&&closed.at(-1).text==='你好','finish returns text') p.finish();check(closed.at(-1).accepted&&closed.at(-1).text==='你好','finish returns text')
p=page({initial:'unchanged'});p.insert('!');p.cancel();check(closed.at(-1).accepted===false,'cancel does not return edited text') p=page({initial:'unchanged'});p.insert('!');p.cancel();check(closed.at(-1).accepted===false,'cancel does not return edited text')
p=page({inputType:'password',initial:'😀secret'});p.toggleLearning();check(!p.learning&&!p.display.includes('secret'),'password masked and cannot learn');p.onHide();check(!p.text&&!p.initial&&!p.original,'password clears on hide') p=page({inputType:'password',initial:'😀secret'});p.toggleLearning();check(!p.learning&&!p.display.includes('secret'),'password masked and cannot learn');p.onHide();check(!p.text&&!p.initial&&!p.original,'password clears on hide')
p=page({inputType:'password',revealSecret:true,initial:'Visible123!'});check(p.display.includes('Visible123!')&&p.secret&&!p.learning,'explicit password reveal preserves private input');p.toggleLearning();check(!p.learning,'revealed password cannot enable learning');p.onHide();check(!p.text&&!p.initial&&!p.original,'revealed password clears on hide')
p=page({initial:'你😀好'});p.cursor=2;p.backspace();check(p.text==='你好','UI Unicode deletion') p=page({initial:'你😀好'});p.cursor=2;p.backspace();check(p.text==='你好','UI Unicode deletion')
p.startDelete();check(p.repeatDelay!==null,'delete hold delay starts');p.onHide();check(p.repeatDelay===null&&p.repeatTimer===null,'hide stops repeat timers') p.startDelete();check(p.repeatDelay!==null,'delete hold delay starts');p.onHide();check(p.repeatDelay===null&&p.repeatTimer===null,'hide stops repeat timers')
p=page();let resolve;p.dictionary={lookup:()=>new Promise(yes=>resolve=yes)};p.composition='64';const pending=p.query(p.requestId);p.onHide();resolve([{word:'你',consume:2,pinyin:'ni'}]);await pending;check(p.total===0,'late dictionary response cannot update hidden page') p=page();let resolve;p.dictionary={lookup:()=>new Promise(yes=>resolve=yes)};p.composition='64';const pending=p.query(p.requestId);p.onHide();resolve([{word:'你',consume:2,pinyin:'ni'}]);await pending;check(p.total===0,'late dictionary response cannot update hidden page')
@@ -43,4 +44,15 @@ p.english();p.english();check(p.keyRows.flat().some(x=>x.value==='Z'),'uppercase
p=page({initial:'test'});p.clearText();check(p.text==='test'&&p.clearArmed,'destructive clear needs second tap');p.clearText();check(p.text==='','confirmed clear') p=page({initial:'test'});p.clearText();check(p.text==='test'&&p.clearArmed,'destructive clear needs second tap');p.clearText();check(p.text==='','confirmed clear')
p.composition='shi';p.schedule();const owned=p.lookupTimer;p.onDestroy();check(!timers.has(owned)&&p.lookupTimer===null,'destroy releases owned lookup timer') p.composition='shi';p.schedule();const owned=p.lookupTimer;p.onDestroy();check(!timers.has(owned)&&p.lookupTimer===null,'destroy releases owned lookup timer')
p=page();p.learning=true;p.composition='64';p.allCandidates=[{word:'你',pinyin:'ni',consume:2}];storageFails=true;p.choose(p.allCandidates[0]);check(p.text==='你'&&!p.composition&&!p.learning,'storage failure cannot duplicate committed candidate');p.toggleLearning();check(!p.learning,'storage failure leaves learning off');storageFails=false p=page();p.learning=true;p.composition='64';p.allCandidates=[{word:'你',pinyin:'ni',consume:2}];storageFails=true;p.choose(p.allCandidates[0]);check(p.text==='你'&&!p.composition&&!p.learning,'storage failure cannot duplicate committed candidate');p.toggleLearning();check(!p.learning,'storage failure leaves learning off');storageFails=false
p=page();p.mode='py';p.composition='niha';await p.query(p.requestId)
p.choose(p.bar[0]);p.invalidate();await p.query(p.requestId)
check(p.text==='你好'&&p.prediction&&p.bar[0].word==='世界','partial spelling commits once and offers context continuation')
p.choose(p.bar[0]);check(p.text==='你好世界'&&!p.composition,'continuation appends only the next word')
p=page();p.mode='py';p.composition='shi';await p.query(p.requestId)
const steadyBar=p.bar,steadyText=p.display;p.composition='shij';p.schedule()
check(p.busy&&p.bar===steadyBar&&p.display===steadyText,'pending lookup retains candidate and preview nodes instead of clearing visible text')
const beforeStale=p.text;p.choose(steadyBar[0]);check(p.text===beforeStale,'retained candidates cannot commit while their replacement is pending')
p=page({initial:'你好'});p.composition='';await p.query(p.requestId);p.space();check(p.text==='你好 '&&!p.text.includes('世界')&&!p.prediction&&!p.bar.length,'space ends the prediction context without auto-committing or leaving stale suggestions')
p=page({initial:'你好'});p.composition='';await p.query(p.requestId);p.newline();check(p.text==='你好\n'&&!p.prediction&&!p.bar.length,'newline ends the prediction context and clears stale suggestions')
p=page({inputType:'password',initial:'你好'});check(p.inputContext()==='','password content is never used as predictive context')
console.log(JSON.stringify({suite:'ime-client',assertions:count,status:'passed'})) console.log(JSON.stringify({suite:'ime-client',assertions:count,status:'passed'}))
@@ -56,4 +56,14 @@ check(edit('你好',2,'世界',3).limited,'maximum length enforced')
check(!preview('my-secret',5,true).includes('secret'),'secret preview masked') check(!preview('my-secret',5,true).includes('secret'),'secret preview masked')
check(chars('😀').length===1,'Unicode length counts codepoints') check(chars('😀').length===1,'Unicode length counts codepoints')
dictionary.clear();check(dictionary.cacheSize===0,'cache release') dictionary.clear();check(dictionary.cacheSize===0,'cache release')
check((await dictionary.lookup('niha',false,{}))[0].word==='你好','unfinished pinyin completes a real word before shorter fallback')
check((await dictionary.lookup(digitsOf('niha'),true,{}))[0].word==='你好','unfinished T9 shares the completion policy')
check((await dictionary.lookup(code14Of('niha'),'14',{})).some(x=>x.word==='你好'&&x.consume===4&&x.completion),'14-key completion consumes only the actual input tokens')
check((await dictionary.lookup('beij',false,{}))[0].word==='北京','word frequency places common completion before rare homophones')
const continuation=await dictionary.predict('你好')
check(continuation[0].word==='世界'&&continuation.every(x=>x.consume===0)&&continuation.length<=8,'post-commit continuations use the longest context with bounded results')
check((await dictionary.predict('你好。')).length===0&&(await dictionary.predict('secret123')).length===0,'punctuation and non-Chinese contexts end prediction')
check((await dictionary.predict('前文你好'))[0].word==='世界','prediction uses only the bounded suffix before the cursor')
check((await dictionary.lookup('shijie',false,{}))[0].word==='世界','common full-pinyin homophone is ranked by licensed word frequency')
check(dictionary.cacheSize<=4,'prediction and completion retain the bounded cache')
console.log(JSON.stringify({suite:'ime-engine',assertions:count,keys:meta.keys,candidates:meta.candidates,status:'passed'})) console.log(JSON.stringify({suite:'ime-engine',assertions:count,keys:meta.keys,candidates:meta.candidates,status:'passed'}))
@@ -50,6 +50,8 @@ def main():
run('auto-open',"await tc.getByClass('document').click();await tc.wait(.4);p=router.getPage(1);check(router.getLength()===2&&p.mode==='t9','field opens full screen T9');root.editText();check(router.getLength()===2,'duplicate opener guarded');") run('auto-open',"await tc.getByClass('document').click();await tc.wait(.4);p=router.getPage(1);check(router.getLength()===2&&p.mode==='t9','field opens full screen T9');root.editText();check(router.getLength()===2,'duplicate opener guarded');")
run('t9-phrase',"for(var label of ['MNO','GHI','GHI','ABC','MNO'])await click(label);await ready();check(p.bar[0].word==='你好','frequency ranks common phrase first');await click('你好');check(p.text==='你好'&&!p.composition,'native candidate commits phrase');") run('t9-phrase',"for(var label of ['MNO','GHI','GHI','ABC','MNO'])await click(label);await ready();check(p.bar[0].word==='你好','frequency ranks common phrase first');await click('你好');check(p.text==='你好'&&!p.composition,'native candidate commits phrase');")
run('continuous',"reset();p.mode='py';p.renderKeys();p.composition='nihaoshijie';p.schedule();await ready();check(p.allCandidates.some(x=>x.word==='你好'&&x.consume===5),'prefix phrase retained');await click('你好');await ready();check(p.composition==='shijie','only matched pinyin consumed');await click('世界');check(p.text==='你好世界'&&!p.composition,'remaining syllables compose');") run('continuous',"reset();p.mode='py';p.renderKeys();p.composition='nihaoshijie';p.schedule();await ready();check(p.allCandidates.some(x=>x.word==='你好'&&x.consume===5),'prefix phrase retained');await click('你好');await ready();check(p.composition==='shijie','only matched pinyin consumed');await click('世界');check(p.text==='你好世界'&&!p.composition,'remaining syllables compose');")
run('completion-prediction',"reset();p.mode='py';p.renderKeys();for(var letter of ['n','i','h','a'])await click(letter);await ready();check(p.bar[0].word==='你好'&&p.bar[0].consume===4,'unfinished spelling completes a real phrase');await click('你好');await ready();check(p.prediction&&p.bar[0].word==='世界','committed word offers bounded context continuation');await click('世界');check(p.text==='你好世界'&&!p.composition,'prediction appends once without repeating the prefix');")
run('steady-candidates',"reset();p.mode='py';p.renderKeys();p.composition='shi';p.schedule();await ready();var previous=p.bar[0].word,previewBefore=p.display;p.press('j');check(p.busy&&p.bar[0].word===previous&&p.display===previewBefore,'pending result retains preview and candidates');p.choose(p.bar[0]);check(!p.text,'stale retained candidate cannot commit');await ready();")
run('complete-candidates',"reset();p.mode='py';p.renderKeys();p.composition='shi';p.schedule();await ready();var total=p.total;await click('更多');check(p.visibleCandidates.length===16&&p.candidateRows.length===4&&p.pageCount>1,'full list paginated');for(var i=1;i<p.pageCount;i++)await click('下一页');check(p.candidatePage===p.pageCount-1,'last candidate page accessible');") run('complete-candidates',"reset();p.mode='py';p.renderKeys();p.composition='shi';p.schedule();await ready();var total=p.total;await click('更多');check(p.visibleCandidates.length===16&&p.candidateRows.length===4&&p.pageCount>1,'full list paginated');for(var i=1;i<p.pageCount;i++)await click('下一页');check(p.candidatePage===p.pageCount-1,'last candidate page accessible');")
run('candidate-select',"var last=p.visibleCandidates[p.visibleCandidates.length-1];await click(last.word);check(p.text===last.word,'last original candidate can commit');") run('candidate-select',"var last=p.visibleCandidates[p.visibleCandidates.length-1];await click(last.word);check(p.text===last.word,'last original candidate can commit');")
run('full-pinyin',"reset();await click('26键');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','standard 10-9-7 QWERTY order');for(var letter of ['s','h','u','r','u','f','a'])await click(letter);await ready();check(p.bar.some(x=>x.word==='输入法'),'long phrase in full dictionary');await click('输入法');check(p.text==='输入法','native full pinyin commit');") run('full-pinyin',"reset();await click('26键');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','standard 10-9-7 QWERTY order');for(var letter of ['s','h','u','r','u','f','a'])await click(letter);await ready();check(p.bar.some(x=>x.word==='输入法'),'long phrase in full dictionary');await click('输入法');check(p.text==='输入法','native full pinyin commit');")
+4
查看文件
@@ -1,3 +1,7 @@
# 截图版本说明
当前1.2.3在OWW261上运行后台离屏原生组件测试,没有采集本版前台截图。UI_PREVIEW.png及下列图片分别来自历史1.2.2和1.2.0,不作为1.2.3前台视觉验收。
# 1.2.2 实际界面 # 1.2.2 实际界面
Android来自OWW261真机;RTOS来自Glyphix原生模拟器。拼图只裁掉圆屏模拟器外壳,以下保留原始帧。 Android来自OWW261真机;RTOS来自Glyphix原生模拟器。拼图只裁掉圆屏模拟器外壳,以下保留原始帧。