diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 828071c..6c5095d 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "project": "蟹蟹输入 RTOS / Android手表适配改版", - "version": "1.2.3", + "version": "1.2.4", "license": "MIT", "android": true, "independent_history": true, @@ -9,12 +9,12 @@ ".gitattributes": "8e689518e5810501eb34583fa369475902805d8dd6c3ea78699469de4fa22932", ".gitignore": "cf35fe4d5c1c2ca6ae6bd343642fee3d04cd8d3799510a02ca94df73a2d4daad", "android/.gitignore": "9907f78ab6ad68ac162f15ed5554c0ea9bafecf6eb25f2388ba0c11d40fab35d", - "android/app/build.gradle": "57f1fbc4c9b4bf2b6e85db2977e81b79544a18d8c8c28c26ef9f2aaab25d641a", + "android/app/build.gradle": "9d0b476141e72df003d3232561fe30890d93c114197e03f614866c45c3d169b5", "android/app/proguard-rules.pro": "41e72ccc70b31f2da13fc597d5196590ec25e0422c2766e1555f3348d64ac6ed", "android/app/proguard-test.pro": "dc5f87f1ec90eb24823174f80d19e93d3e69919af70891529e4c54a744988aa4", "android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java": "d5a7284b6693ee116a9e5450b4c0e38df2a71812c79664ee04311d8088db33ac", "android/app/src/main/AndroidManifest.xml": "cd096a1f1bd560114c865260bbdc50f43971089598968b065581cf07f1dbee69", - "android/app/src/main/java/com/sydxky/crabinput/MainActivity.java": "ee17c3d9f20509f382a9d2d9475d4dbddd7b57cea690bd6577a396b174400b7f", + "android/app/src/main/java/com/sydxky/crabinput/MainActivity.java": "a777366f52e5974f55e1298f2d8b6cd24dbf94fb8d5a57c25d9ac4b491fe6e36", "android/app/src/main/res/drawable/ic_launcher.xml": "9f22878890b584bb6151223cc280e542f82f354cc69e1d47f2dc6093a606c071", "android/app/src/main/res/values/styles.xml": "37c2bd1bbf1017e84448c0177f8999fd44f5cbca8c0afde5478594dc7be55279", "android/build.gradle": "79f56a485c696a3b800776255c37729515464340f568bf5163e5f81b60086198", @@ -24,12 +24,12 @@ "android/gradle.properties": "1a62f4c0a2a08044c706c378451342a11b307842b6b2ad434a86d6f970acc201", "android/gradlew": "80a4c62723bb67cbabe92c37606d380089c647184afa1bf2b8c136f0e15d2cd6", "android/gradlew.bat": "0e82fa239f8f4dca4efc83863c16c3b143cb6faab6f50cbefab4ee0bd03e5b5f", - "android/host/build.gradle": "8ddfb0f5f4b25e64c8c7d2b4da057445c47939b3ed4b56fceacf7f0ae966c722", + "android/host/build.gradle": "9bede276d4c1b8f7229307d8ba9ae569472f7470a3e0dc743d9c9f0ca4fb3615", "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": "91619d8681376727fd8459157db5dc1e84d261134fc3c8c9354ec01f52fafe7d", "android/keyboard/build.gradle": "512edae398ac76078488f9d2439e23032029b44b4d1bbbbe266d9e2d11effedc", "android/keyboard/consumer-rules.pro": "74ec127457bb863b30cc41be93b5f6b1eb003783addb2cda36b3fa1cf7e80432", - "android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java": "73343a906f5ab36f465d587d8d47f3391e4513435ce59e4a4ffc1208445868c3", + "android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java": "d92ab17b0872ec6bbd6eecee897c1376d42721c86dd380e2b0334b95055370ce", "android/keyboard/src/main/AndroidManifest.xml": "343d5be1ac17cdd45d1a2f2ddc616c509290e655869125a9422a96d88aa66066", "android/keyboard/src/main/assets/dictionary-manifest.json": "aab6bf005ec08396e877163c4edb835299946b99362df97ab32ded80eb3e78a4", "android/keyboard/src/main/assets/licenses/CrabInput-LICENSE": "027e84ad11fd2992040da0aad2ece7a74fdf61540dc2d74cc8a281fd76eb942c", @@ -43,20 +43,21 @@ "android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java": "1f03cda3833ba282d12eaf66ddcbb5f4354bb8b99a1d2c9de1b4f430c9effcac", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "8427a7d3dbee68031ee5964ad7f22cf64d94cf70049b7e05ae2ec2aca63fade8", "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java": "e5253607c117d18084ca1167868b3ef7ef0f28529700b70507c70af531d17291", - "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "e9c8b43608a634c35cd942672d6da7f96e710ee35e4d3050ad110eff7a7d0c69", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "1ac71672ca55856a5dbbcbde4af243274b0c59d71cb74fd677ad6ea63815790e", "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/CandidateRankingTest.java": "3e163c8137829de2f26fa737b4a983f0bb4544c0598f2b635e3ddc55631bcb74", "android/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "android/NOTICE.md": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", - "android/README.md": "b20dba67d7e314a4db1d9124a7dd0f230c3dd05f6a904fceb83cc4220df980ec", + "android/README.md": "490bf2b1cd03a2aa11cc806edccd057876c84b3778dd7ddfe3d31831081a9442", "android/scripts/build-dictionary.py": "4643f5422f2383a56d8c0653e47e3fc6b02481f065efd924958569fb48435969", "android/scripts/capture-device.py": "34023ff4fb0ff80a8811b1222aad3b58c70b628333edb56efffa3e6adaf05ace", - "android/scripts/validate-device.py": "c520bbeed36106dd1cc3c237d1f94b4adb933884ea47582bb4278128650f3d5f", + "android/scripts/validate-device.py": "149f73cc3b6174f0487d95b2bd03e6792f4897036331bb8e0c1012b07dca6644", "android/settings.gradle": "73a5f1e8c25c44b0c5f983ed822c4256dea33b14b8d53115ed3be118aeaaee1d", "android/VALIDATION_1.2.0.md": "7ce04837f631f9e491ecfb96e8e056c4b01c4aa2f24a061d540c5280201848e6", "android/VALIDATION_1.2.2.md": "6d3ebe3f107099516d2dbcae025e41d516ba8d4de4a40fc9af3342ba9ac9d345", "android/VALIDATION_1.2.3.md": "603ed3903589f4718892ee4211d59c563c638ea3547c28df27f5eef04651bd33", + "android/VALIDATION_1.2.4.md": "6ec3e7e2ea9d19d7e5393a8af17eb5d3e76a2db382b70f788fddac259482cb98", "build-rtos-game.ps1": "a0cf9d6fa5ba1ecc22e768373dcca2f87e1e58fc44f0a612b59c7215fb064df8", "com.sydxky.rtos.imefixture/build.ps1": "37cd2f3af83d9c1cb2f1d855d0d95cf2e822ac9e743c8370237c48c4a4d8c0a0", "com.sydxky.rtos.imefixture/glyphix.config.js": "d53b4b0e8597d9f7fb5fd3d53063d285b245669021527a1bfc3bc054c5122514", @@ -72,10 +73,10 @@ "com.sydxky.rtos.inputmethod/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "com.sydxky.rtos.inputmethod/NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", "com.sydxky.rtos.inputmethod/PRIVACY.md": "19049b0bb30add6fa5071ec16e9494f835e04ee1dad83108bfec34c71e16d679", - "com.sydxky.rtos.inputmethod/README.md": "837b66afd2b838a54c3940e1829300213eb6bbcbce7506fde90548deae5b2d8f", + "com.sydxky.rtos.inputmethod/README.md": "380ebdad19cc27d9358c802d1c3f5c899dad4e0f387ad8a1d5fcc496c29a51e9", "com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704", - "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "bf8110a215357fdf1020bf633cff9c7bb7ac155510b75e884b63e0698ecfef84", - "com.sydxky.rtos.inputmethod/scripts/install.mjs": "436a5560a54a56fbf923a370b7ec40b9d9670b1a8d3ef5d9b10e3603d71779ba", + "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "ad747c27931a683b7132e7273e4913dc2c35e110b2c6990083425a7e42817bd9", + "com.sydxky.rtos.inputmethod/scripts/install.mjs": "ab2a23bfd457e40f56f17bcf8868482b0ed2d626629e52f788f6adcc3b4ab71f", "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/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", @@ -90,17 +91,20 @@ "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/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-theme.js": "2e10201035adc278d32d4eb9cb42411e7e79e36631de9fe4cab0799ab790ac2e", - "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "d5a8e6ccf6622d77e94d4bb9b81e4faca403030648bb6e0d7957ce0593b5ceec", + "com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "7b3892b79241e2fa5bc8914bfc047bf6319194c8db1d8b33ff2a63449f859b1c", + "com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "60937c3a0d51ee3978f12302a31a550bcbe352dfcc2c62b45bcf3b4bab55e7ad", + "com.sydxky.rtos.inputmethod/src/common/ime-theme.js": "628a77ca3ef9cf239eab9883497ccfb29652f5f70ff0e584de6cf0c17c005999", + "com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux": "a445026ae4fa8dbb151d7d776ecd8269dc24f1775acf4b71cec30b5b8429e272", + "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "27a14680bff055c4625d7858879df7cde804b6aa399c81a07ce1079cdc60140d", "com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca", - "com.sydxky.rtos.inputmethod/src/manifest.json": "08dfbe892bcaad5e6d4cd176c5fea6178749557d2b45868991c3ea7a39e1696d", - "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "0a85252cd2548223f99e6582e3075dab1fe1a2ec88f3bb6d16593c2914878666", - "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "50f416bbbcaae1b867e3b88331ee2c27a95ad7315754d103e6684a223717a40a", + "com.sydxky.rtos.inputmethod/src/manifest.json": "c855f0db1d62b2da6e3f494c24134af27ef3665997dab35cc29494122b769a92", + "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "197034a327872629c1662fa3a437f41c9d31a23bf74656dad8d7d31ecfa43401", + "com.sydxky.rtos.inputmethod/tests/console-native.py": "9d708993e1f3071871420a3c02c10bf422e5025c3132e6a029ec8ef036e817a2", + "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "984a8cea899b8e0762773d48d56566729a9e720f402fa7b946645d0afa429e1a", "com.sydxky.rtos.inputmethod/tests/layout.py": "9b1fc238cae2f39e5385611cb3d5d636e40941cd4566c51d1407cfb72d6dbcff", - "com.sydxky.rtos.inputmethod/tests/native.py": "31eaf7a0b898d575866f0df7bd54a0ffe2c6f96ec915633eb63f52359441bfe5", - "com.sydxky.rtos.inputmethod/tests/visual.py": "25bb14687278b6961e1e4957dafdb28b95d1056fdbe58e6506bca853cd5f1fec", + "com.sydxky.rtos.inputmethod/tests/native.py": "07bb05ad97fe2ca06f7f4a55e57b914b025dd70c0a6ea0249c1195e05aed5fe5", + "com.sydxky.rtos.inputmethod/tests/sync.test.mjs": "89a65009591eb874f82aeafb425afca99c2d5336850277e1cfa2636e05345fee", + "com.sydxky.rtos.inputmethod/tests/visual.py": "cdd48d7cb4851fbcb6b96af5098405f4d02e93726cf02320faa461592b948477", "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/pinyinDictV5_20260416/key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8", @@ -171,6 +175,13 @@ "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.3.md": "2311037bd6901906318dd67882e5adc5055e45086a69408f66fdf8b050f39be7", + "com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md": "6c0f278ca0ab2625bf6875fd4106e0ffe45b4a15b58a36463f96ee63517a3af9", + "docs/1.2.4/rect-bluewhite-middle.png": "4789f2dc33a26869dc5ce0df74891dec67d08bdeb342f38b8265d73270e96569", + "docs/1.2.4/rect-bluewhite-py-lower.png": "b02bc442e2f830cbfb7c5d34f2eb55e72be5b993f61acf0b6c4bf9234211aa29", + "docs/1.2.4/round-blackgreen-en-lower.png": "08e3313cd949839105eccfaddbbd2fa6d136fd09808b33e792d6f87d43dc1e06", + "docs/1.2.4/round-bluewhite-en-lower.png": "e58737ca2f3cb8c11bf164a729c472344b881a6e96d778cb80601b9d10514df1", + "docs/1.2.4/round-bluewhite-en-upper.png": "36373e8d3b8dd95b03ae621bccb607db891bd438a1ee1009678efd7ce4264ebf", + "docs/1.2.4/round-bluewhite-middle.png": "79e7f93b52e06ec37621bd2c2ef1ff808e12c37a3c501d0f190b5b7dc1d89819", "docs/screenshots/android-candidates-16.png": "d244a3fb41300a7e8c478930307cfb3d4e781f016be53a9e93615bb79717ae73", "docs/screenshots/android-english.png": "adbc1ede8f2103a6435aa04efe604737895365ff3f53e75fcb9511b9a1f4f551", "docs/screenshots/android-fourteen.png": "88b06bdd1e6a6dc6f45722e01ce7930c94845062b8bf237aad5c5d086f37641c", @@ -189,11 +200,11 @@ "docs/screenshots/rtos-round-en.png": "b94f17f121d165c33f84b0eaa4c1ca48b2dbf9890bd1ab77147965fc138de6c2", "docs/screenshots/rtos-round-num.png": "d7fcf451229861fb564b45b6ada03501aed57cf9ca5c058833b9b65dbace06eb", "docs/screenshots/rtos-round-phrase-candidates.png": "507d09901681bbeae4da39fa0a700663d89b7149f7fab01dd603630c932b4dc4", - "docs/SCREENSHOTS.md": "e027e27a0838b83bda0369c1c3c986ec3635be2d6c8550ec11b8196424f0762d", - "docs/UI_PREVIEW.png": "a8fe477bfbc7efba2affdaa7f7314c773c123191a1fc0d04c2eae294b4aaa492", + "docs/SCREENSHOTS.md": "347e2eb2cb21aa332dbb20f8e2a8aaeb3561ffdcc7139766e92a78b0a24047a9", + "docs/UI_PREVIEW.png": "e58737ca2f3cb8c11bf164a729c472344b881a6e96d778cb80601b9d10514df1", "LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", "NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", - "README.md": "cc049c63848a649e5ba976045fb3a424c7839e3e22f7b11763d5e2c44356a9e1", + "README.md": "a36d44e8229f94915c590c88940bbc717e71aa8efb6f6e13fae7efd023c98924", "tests/emulator-validate.py": "2a549e3c7bc177b942826425e548f68db39a32d8a55ade080f0d8ab3e9c00060" } } diff --git a/README.md b/README.md index a3d51f9..994fcb4 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,20 @@ **MIT 开源,允许商用、修改、再分发和闭源集成。** 使用时请保留 [LICENSE](LICENSE) 和 [第三方许可](NOTICE.md)。厂商 SDK、运行时与设备固件不包含在本项目授权中。 -![历史界面,版本标记见截图说明](docs/UI_PREVIEW.png) +![1.2.4 RTOS原生测试工程界面,验证范围见截图说明](docs/UI_PREVIEW.png) -## 1.2.3 联想与稳定刷新 +## 1.2.4 光标贴合、26键扩大与大小写切换 -- 全拼、九宫、14键支持未完成拼音补全,按完整匹配、补全、较短前缀分层,结合词频及当前上下文排序;原始候选完整保留。 -- 输入词语后提供离线续写,例如“你好→世界”;使用光标前最多4个连续汉字,最多8个联想,不保存上下文,密码不参与。 -- Android复用文字与按键控件并合并同帧刷新;RTOS查询期间保留候选,旧候选在新结果到达前不能提交。 +- RTOS光标改为独立1像素细线,紧贴前后文字,保留真实空格、emoji与私密遮蔽;长文本仍保持光标可见。Android预览及示例编辑框去掉额外字间距。 +- 扩大26键,标准10/9/7三排;圆屏逐排按安全宽度收放,方屏充分利用宽度。 +- 点“abc”进入英文,再点“大写”切到大写,点“小写”恢复小写;按钮显示下一步动作,键面与实际输入同步。 +- 保留1.2.3未完成拼音补全、上下文续词及稳定刷新:全拼/九宫/14键同步支持;完全离线,密码不参与上下文。 -RTOS 53项引擎、42项UX及四配置构建通过。Android 7项Java测试及OWW261后台原生组件3项通过,包括24帧非空绘制、控件复用、补全/续写和密码隔离。本版未重跑前台交互;RTOS原生Inspector被其他任务占用,未接管。截图为明确标记的历史1.2.2界面,不冒充本版视觉验收。 +RTOS **59项引擎、49项UX及四配置生产构建通过**。独立原生测试工程中,生产键盘组件源码逐字节相同,圆/方普通及QuickApp四配置共96个状态全部通过,光标额外布局间距0px、26个按键边界合格;各配置均实际点击输入`qQq`。96张原生截图随验收ZIP提供。 -[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.3.md) · [本版Android验收](android/VALIDATION_1.2.3.md) +Android **7项Java测试及OWW261后台原生组件4项通过**,包含24帧非空绘制/控件复用、补全/续词、密码隔离、实际大小写切换和圆屏边界。Release APK/AAR、生产签名及API27元数据通过。上述是组件回归,未代替最终发布APK的前台端到端验收;本版未重跑原前台交互。RTOS截图来自明确标记的原生测试工程,Android截图为历史资料。 + +[本版RTOS验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md) · [本版Android验收](android/VALIDATION_1.2.4.md) ## 1.2.0 已有功能 @@ -29,8 +32,8 @@ RTOS 53项引擎、42项UX及四配置构建通过。Android 7项Java测试及OW | 平台 | 接入与产物 | 已完成的验证 | | --- | --- | --- | -| Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp四配置构建;53项引擎和42项UX脚本断言;原生视觉未重跑 | -| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261后台原生组件3项通过;24帧非空绘制及控件复用;前台交互未重跑 | +| Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp四配置构建;59项引擎和49项UX;独立原生组件四配置96状态/截图通过 | +| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261后台原生组件4项通过;24帧非空绘制及控件复用;前台交互未重跑 | Android 版本作为**应用内公开组件**,由每个应用自行接入;组件不注册系统输入法。RTOS 页面也需宿主接入,系统级注册依赖厂商支持。 @@ -38,7 +41,7 @@ Android 版本作为**应用内公开组件**,由每个应用自行接入; ## 下载、源码与反馈 -- [v1.2.3 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.3):AAR、演示 APK、两屏 RTOS QuickApp、验收截图及 SHA-256。 +- [v1.2.4 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.4):AAR、演示 APK、两屏 RTOS QuickApp、原生组件验收记录/截图及 SHA-256。 - [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) - [已有问题](https://git2.sydxky.cn/sydxky/crab-input-rtos/issues) · [免注册提交问题与建议](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos) @@ -79,7 +82,7 @@ CrabKeyboard.Binding keyboard = CrabKeyboard.attach(this, editText); & .\com.sydxky.rtos.inputmethod\build.ps1 ``` -脚本生成词库分片,执行 53 项引擎、42 项 UX 断言,并构建普通/QuickApp 四配置。每屏 QuickApp 都须保留 `bundle.pkg`、`icon.png`、`manifest.json` 三个文件。 +脚本生成词库分片,执行 59 项引擎、49 项 UX 断言,并构建普通/QuickApp 四配置。每屏 QuickApp 都须保留 `bundle.pkg`、`icon.png`、`manifest.json` 三个文件。 复现原生验收时先构建最小参考页并启动一次圆屏模拟器,获得厂商 launcher/global 资源后退出;不要同时占用 Inspector 14201: @@ -92,4 +95,10 @@ 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 ``` -视觉脚本依赖 Pillow。RTOS 运行时内存配置保持 8192 KiB。本版本已完成上述算法、组件回归与构建检查;未完成的交互、QuickApp原生视觉、RTOS真机、长期性能/续航和商店审核范围见本版验收记录。 +本版光标和26键回归可在独立原生模拟器中执行,不启用Inspector: + +```powershell +python .\com.sydxky.rtos.inputmethod\tests\console-native.py --work work/caret --output work/evidence/caret +``` + +视觉脚本依赖 Pillow。RTOS 运行时内存配置保持 8192 KiB。本版本已完成上述算法、组件回归与构建检查;未完成的整包前台交互、RTOS真机、长期性能/续航和商店审核范围见本版验收记录。 diff --git a/android/README.md b/android/README.md index bdda30a..9e5400b 100644 --- a/android/README.md +++ b/android/README.md @@ -1,21 +1,23 @@ # 蟹蟹输入 Android应用内键盘组件 -基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.3 / 10203。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 +基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.4 / 10204。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。 完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。 +1.2.4 去掉预览与示例编辑框的额外字间距,扩大26键;每排继续按实际屏幕弦宽计算安全边界。点“abc”进入英文,随后“大写/小写”按钮显示下一步动作,字母标签与实际输入保持一致。见[本版验收](./VALIDATION_1.2.4.md)。 + 1.2.3 增加未完成拼音补全、上下文续写及词频排序;复用预览、按键及候选控件,按帧合并刷新。联想仅在内存中使用当前输入框光标前的中文,不持久化全文,密码内容不参与。见[本版验收](./VALIDATION_1.2.3.md)。 1.2.2 去掉英文、数字、符号模式未使用的候选栏空位,文字与键盘紧密相接。本版真机间距检查通过;完整测试的限制见[验收记录](./VALIDATION_1.2.2.md)。 ## 一分钟接入 -将发行版`crab-keyboard-1.2.3.aar`放入宿主`app/libs/`: +将发行版`crab-keyboard-1.2.4.aar`放入宿主`app/libs/`: ```groovy -dependencies { implementation files('libs/crab-keyboard-1.2.3.aar') } +dependencies { implementation files('libs/crab-keyboard-1.2.4.aar') } ``` ```java diff --git a/android/VALIDATION_1.2.4.md b/android/VALIDATION_1.2.4.md new file mode 100644 index 0000000..fb68e30 --- /dev/null +++ b/android/VALIDATION_1.2.4.md @@ -0,0 +1,9 @@ +# Android 1.2.4 光标、26键及大小写验收 + +2026-11-14,版本1.2.4/10204,最低API27。预览及演示/独立宿主编辑框显式关闭额外字间距,原生EditText负责文本与光标定位。26键扩大高度,各排仍按实际圆屏弦宽分配安全区域。英文按钮显示下一步“大写/小写”,键面与实际输入同步。保留1.2.3离线补全、续词、控件复用和合并同帧刷新,密码不提供联想上下文。 + +7项Java测试通过。Release APK/AAR、宿主及组件测试APK构建通过;SQLite全量回读104,837组索引、163,238候选和完整性检查通过。lint 0错误:演示应用1项备份声明警告,组件2项测试依赖版本提示。最终APK为1.2.4/10204、最低API27、zipalign通过,原生产证书1名签名者,APK v2签名有效。证书SHA-256为b4632fff23289fdfa2b4a644b4e9d8fc18f7b0a3ed49fae1353ea5690cc3961b;最终APK SHA-256为37a0a80fd61ff7079a5d21ffaf016f7776de5a2df8933f1221ad8beae2ef4f6f。 + +OWW261 Android11/API30上4项后台原生组件回归通过:24帧measure/layout/draw持续绘出文字并复用预览和按键;niha补全你好、接续世界且仅提交一次;密码上下文隔离;实际点击大写/小写按钮及字母键输入qQq,并检查屏幕内的所有控件与圆屏按钮四角边界。 + +测试在离屏原生View执行,未打开Activity、切换前台或修改默认系统输入法。使用debug组件,15个运行时class成员与发布AAR逐字节相同。这是组件回归,不是发布APK前台端到端视觉验收;最终APK没有安装到共享手表,原9项前台交互未重跑。本版没有Android前台截图,历史1.2.2/1.2.0截图仅作历史资料。另一款方表、长期性能/功耗与商店审核未验收。 diff --git a/android/app/build.gradle b/android/app/build.gradle index 938d617..2dc3066 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId 'com.sydxky.crabinput' minSdk 27 targetSdk 35 - versionCode 10203 - versionName '1.2.3' + versionCode 10204 + versionName '1.2.4' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } signingConfigs { diff --git a/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java b/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java index abfac17..b24e148 100644 --- a/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java +++ b/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java @@ -46,7 +46,7 @@ public final class MainActivity extends Activity { // Vendor EditText backgrounds add large vertical insets on watches. // Explicit padding keeps both empty hints and entered text fully visible. editor.setPadding(px(12),0,px(12),0);editor.setGravity(Gravity.CENTER_VERTICAL); - editor.setIncludeFontPadding(false);editor.setSingleLine(true); + editor.setIncludeFontPadding(false);editor.setLetterSpacing(0f);editor.setSingleLine(true); android.graphics.drawable.GradientDrawable background=new android.graphics.drawable.GradientDrawable();background.setColor(Color.WHITE);background.setCornerRadius(px(12));editor.setBackground(background); LinearLayout.LayoutParams layout=new LinearLayout.LayoutParams(-1,px(47));layout.topMargin=px(7);body.addView(editor,layout);bindings.add(CrabKeyboard.attach(this,editor)); } diff --git a/android/host/build.gradle b/android/host/build.gradle index 9900d4a..50ce907 100644 --- a/android/host/build.gradle +++ b/android/host/build.gradle @@ -2,7 +2,7 @@ plugins { id 'com.android.application' } android { namespace 'com.sydxky.crabinput.host' compileSdk 37 - defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10203; versionName '1.2.3' } + defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10204; versionName '1.2.4' } compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation project(':keyboard') } diff --git a/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java b/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java index 700f478..fc28692 100644 --- a/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java +++ b/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java @@ -13,7 +13,7 @@ public final class HostActivity extends Activity { @Override public void onCreate(Bundle saved){ super.onCreate(saved);LinearLayout layout=new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);layout.setGravity(Gravity.CENTER);layout.setPadding(55,45,55,45); TextView title=new TextView(this);title.setText("独立应用接入示例");title.setTextSize(13);layout.addView(title); - EditText editor=new EditText(this);editor.setContentDescription("fixture_editor");editor.setHint("点击输入");layout.addView(editor,new LinearLayout.LayoutParams(-1,-2)); + EditText editor=new EditText(this);editor.setContentDescription("fixture_editor");editor.setHint("点击输入");editor.setLetterSpacing(0f);layout.addView(editor,new LinearLayout.LayoutParams(-1,-2)); layout.setFocusableInTouchMode(true);layout.requestFocus();setContentView(layout);binding=CrabKeyboard.attach(this,editor); } @Override protected void onDestroy(){if(binding!=null)binding.close();super.onDestroy();} diff --git a/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java b/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java index 8625c49..f56ff44 100644 --- a/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java +++ b/android/keyboard/src/androidTest/java/com/sydxky/crabinput/SurfaceRegressionTest.java @@ -77,4 +77,25 @@ public class SurfaceRegressionTest { finally{controller.close();} }); } + @Test public void caseSwitchTypesBothCasesInsideWatchBounds(){ + ui(()->{ + EditText editor=new EditText(context());KeyboardController controller=session(editor,InputType.TYPE_CLASS_TEXT); + try{ + controller.action("mode:en");KeyboardSurface surface=controller.surface; + int[] widths={466,467,466};String[] keys={"q","Q","q"};String[] switches={"大写","小写"}; + for(int step=0;step<3;step++){ + int width=widths[step];surface.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(466,View.MeasureSpec.EXACTLY));surface.layout(0,0,width,466); + View key=find(surface,keys[step]);assertNotNull(key);assertTrue(key.performClick()); + for(int i=0;i=0&&child.getTop()>=0&&child.getRight()<=width&&child.getBottom()<=466); + if(context().getResources().getConfiguration().isScreenRound()&&child instanceof android.widget.Button){ + for(int x:new int[]{child.getLeft(),child.getRight()})for(int y:new int[]{child.getTop(),child.getBottom()})assertTrue("Button corners stay inside the round screen",Math.hypot(x-width/2.0,y-233)<=width/2.0-1); + } + } + if(step<2){View toggle=find(surface,switches[step]);assertNotNull(toggle);assertTrue(toggle.performClick());} + } + assertEquals("qQq",editor.getText().toString());assertFalse(controller.state.upper); + }finally{controller.close();} + }); + } } diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java index 3433a8d..288c95e 100644 --- a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java @@ -107,6 +107,7 @@ final class KeyboardSurface extends ViewGroup { float previewY=top+29; String preview=state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview; TextView previewView=label("preview",preview,21,INK);previewView.setMaxLines(1);previewView.setEllipsize(android.text.TextUtils.TruncateAt.START); + previewView.setLetterSpacing(0f); previewView.setOnClickListener(v->actions.action("tools"));previewView.setContentDescription("编辑光标"); float pi=inset(previewY,30);place(previewView,pi,previewY,410-2*pi,30); if(state.expanded) { buildCandidates(previewY+38);return; } @@ -137,10 +138,11 @@ final class KeyboardSurface extends ViewGroup { } float keyHeight=captions.length==4?36:49; // Hide an unused suggestion strip in direct entry modes to enlarge the keys. - if(!isChinese()){keyHeight=captions.length==4?43:58;} + if(!isChinese()){keyHeight=captions.length==4?43:66;} + else if(state.mode.equals("py")){keyHeight=55;} for(int i=0;i=7?21:24); 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.mode.equals("en")?(state.upper?"小写":"大写"):"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13); bottom(modeY+34,false); }finally{ java.util.Iterator> iterator=children.entrySet().iterator(); diff --git a/android/scripts/validate-device.py b/android/scripts/validate-device.py index f9ffaae..695a2ca 100644 --- a/android/scripts/validate-device.py +++ b/android/scripts/validate-device.py @@ -1,5 +1,5 @@ """Install the release and run embedded-component instrumentation on one explicit device.""" -import argparse,hashlib,json,pathlib,subprocess,time +import argparse,hashlib,json,pathlib,re,subprocess,time APP=pathlib.Path(__file__).resolve().parents[1] def main(): @@ -31,9 +31,10 @@ def main(): 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) output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True) - expected=3 if args.background_regression else 9 + expected=4 if args.background_regression else 9 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} + version=re.search(r"versionName\s+'([^']+)'",(APP/'app/build.gradle').read_text(encoding='utf-8')).group(1) + report={'model':model,'sdk':sdk,'version':version,'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') assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted' if not args.background_regression: diff --git a/com.sydxky.rtos.inputmethod/README.md b/com.sydxky.rtos.inputmethod/README.md index fc91a8c..ac73878 100644 --- a/com.sydxky.rtos.inputmethod/README.md +++ b/com.sydxky.rtos.inputmethod/README.md @@ -2,7 +2,9 @@ **蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。 -独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.3 / 10203`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 +独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.4 / 10204`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 + +1.2.4 将字体字符光标换成独立1像素细线,光标紧贴前后文字,保留真实空格与Unicode边界;扩大26键,逐排适配圆屏安全区域。英文模式按钮明确显示下一步“大写/小写”,切换后实际输入大小写一致。验证范围见[本版验收](./VALIDATION_1.2.4.md)。 1.2.3 新增未完成拼音补全、离线上下文续写和词频排序;查询期间保留候选,避免先清空再重画。验证范围见[本版验收](./VALIDATION_1.2.3.md)。 @@ -21,7 +23,7 @@ - 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。 - 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。 - 第一行显示三个候选。点击右侧下箭头按4×4自适应分页查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。完整拼音、九宫和14键先匹配完整编码,再提供未完成拼音补全和较短前缀;通过本地 jieba 0.42.1 词频及当前光标前的中文上下文排序,学习偏好可优先排序。 -- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。 +- 点“abc”进入英文;再点“大写”切换大写,点“小写”恢复小写,字母键标签与输入字符同步。支持数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。 - 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。 - 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。 @@ -71,3 +73,9 @@ python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证 初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。 MIT 许可副本也随运行包保留。jieba 词频只用于歧义候选排序,不联网、不分词上传。官方公开 API 不足以实现全局输入法安装;本轮仍按用户确认的模拟器与提交包范围交付,真机安装与平台审核单独处理。 + +## 2026-11-14 内部组件接入与持续同步 + +在 1.2.3 最新引擎和上下文联想基础上,内部源抽出 `src/components/CrabInput/index.ux`,`keyboard/index.ux` 变为兼容封装。组件采用 config 属性与 complete/cancel 事件;宿主不再维护第二套键盘。腕上星语 1.4.1 直接挂载组件,其他宿主仍可继续调用旧路由。私密输入默认遮盖;宿主显式 `revealSecret:true` 时明文显示,但不学习、不持久化。 + +安装脚本支持 `node scripts/install.mjs <宿主> --watch` 持续同步及 `--check` 只读校验;涵盖组件、封装、引擎、主题、词库、图标和第三方许可,生成来源 SHA-256 清单。不变文件不重写,旧的自有分片会清理,其他宿主文件保留。原始词库或转换器更新会自动重新生成,生成中禁止通过校验。`tests/sync.test.mjs` 实际验证监听后的源码/词库更新、删除及哈希拒绝。完整约定见 [腕上星语接入文档](../com.sydxky.rtos.aichat/INPUT_COMPONENT.md)。这次没有发布新的 Git2 输入法版本。 diff --git a/com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md new file mode 100644 index 0000000..8f22eb6 --- /dev/null +++ b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.4.md @@ -0,0 +1,11 @@ +# RTOS 1.2.4 光标、26键及大小写验收 + +2026-11-14,版本1.2.4/10204。光标改为独立1像素细线,前后文字使用连续布局,保留真实空格、Unicode码点及私密遮蔽;长文本按预览宽度预算截取,使光标留在预览内。26键使用明确的10/9/7三排,扩大英文按键并按圆屏高度收放各排宽度。英文切换按钮显示下一步“大写/小写”,按当前状态规范提交字符,避免旧回调大小写错配。 + +59项引擎和49项UX脚本断言通过。厂商gx v0.10.1/3ed38835、8192KiB内存,普通/QuickApp圆方四配置生产构建通过。104,837组索引、163,238候选完整保留,沿用1.2.3离线补全、上下文续词、候选稳定刷新及密码隔离。 + +新增tests/console-native.py,使用自行启动、不启用Inspector的独立原生模拟器。将生产components/CrabInput/index.ux逐字节复制到自动运行的测试工程,仅增加测试入口并改变测试工程路由;报告记录生产源码SHA-256及各配置测试包摘要。没有连接或接管其他任务的Inspector14201。 + +圆屏466×466、方屏390×450的普通/QuickApp四配置各24个状态,共96个原生状态通过。覆盖白蓝/黑绿两套主题,中文、词组、英文、混合emoji、开头/中间/结尾、长文及私密遮蔽光标;实测前文字—光标—后文字额外布局间距均为0px,光标宽1px。每配置检查拼音/英文与大小写的26个按键边界,圆屏逐个检查按键四角不超出半径231px安全区。四配置均通过@system.test真实点击切换并输入qQq,不以直接调用press替代点击。 + +96张截图为上述原生测试工程的真实画面,未绘制替代UI;它们证明生产键盘组件在测试入口中的布局,不代表完整发布包的全套端到端验收。结果、原始日志、截图与源码摘要随验收ZIP提供。本版没有重跑原84项前后台交互;RTOS真机、固件接入、长期性能/功耗和商店审核未验收。 diff --git a/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py b/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py index 7ba43d3..69aa672 100644 --- a/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py +++ b/com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py @@ -49,6 +49,16 @@ def from94(value): return number def convert(source, target): + target.parent.mkdir(parents=True, exist_ok=True) + assert target.resolve().is_relative_to((APP/'src/assets').resolve()) + marker=target.parent/('.'+target.name+'.generating') + with marker.open('x',encoding='ascii') as handle:handle.write('generating') + try: + _convert(source,target) + finally: + marker.unlink() + +def _convert(source, target): files = {p.name: p.read_bytes() for p in source.glob('*.dat')} frequency_path=APP/'third_party/jieba/dict.txt' frequencies={} @@ -82,8 +92,8 @@ def convert(source, target): # Only replace files listed in the previous generated manifest. if (target/'meta.json').exists(): previous=json.loads((target/'meta.json').read_text(encoding='utf-8')) - for name in previous['shards']: - assert re.fullmatch(r'(?:short|s?[0-9]+|ds?[0-9a-d]+)',name) + for name in [*previous['shards'],*previous.get('association_shards',[]),*previous.get('completion_shards',[])]: + assert re.fullmatch(r'(?:short|s?[0-9]+|ds?[0-9a-d]+|[ac][0-9a-f]{2})',name) old=target/(name+'.json') if name not in shards and old.exists():old.unlink() for name, content in shards.items(): @@ -101,7 +111,9 @@ def convert(source, target): for name in shards: (restored14 if name.startswith('d')else restored).update(json.loads((target/(name+'.json')).read_text(encoding='utf-8'))['entries']) assert restored == entries and restored14 == entries - report = dict(upstream='https://github.com/crabPangXie/crabKeyboard', + report = dict(association_seed_sha256=hashlib.sha256((APP/'src/common/association-seeds.json').read_bytes()).hexdigest(), + generator_sha256=hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(), + upstream='https://github.com/crabPangXie/crabKeyboard', commit='93d36b5002dee80a3d253b1ec7ec9e29800a104c', variant='pinyinDictV5_20260416', keys=len(entries), candidates=sum(map(len, entries.values())), shards=sorted(shards), diff --git a/com.sydxky.rtos.inputmethod/scripts/install.mjs b/com.sydxky.rtos.inputmethod/scripts/install.mjs index 9e2d2a0..19d1429 100644 --- a/com.sydxky.rtos.inputmethod/scripts/install.mjs +++ b/com.sydxky.rtos.inputmethod/scripts/install.mjs @@ -1,26 +1,139 @@ -// Copy the canonical keyboard into another Glyphix sandbox; no cross-app data. +// The canonical component is maintained here; hosts consume generated copies. import fs from 'node:fs' import path from 'node:path' +import crypto from 'node:crypto' +import {spawnSync} from 'node:child_process' import {fileURLToPath} from 'node:url' -const app=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..') -const target=path.resolve(process.argv[2]||'') -if(!process.argv[2]||!fs.existsSync(path.join(target,'src/manifest.json')))throw Error('Usage: node scripts/install.mjs ') -const oldMeta=path.join(target,'src/assets/ime-dict/meta.json') -if(fs.existsSync(oldMeta)){ - const current=JSON.parse(fs.readFileSync(path.join(app,'src/assets/ime-dict/meta.json'),'utf8')).shards - for(const name of JSON.parse(fs.readFileSync(oldMeta,'utf8')).shards){ - if(!/^(short|s?[0-9]+|ds?[0-9a-d]+)$/.test(name))throw Error('Invalid generated shard name') - const old=path.join(target,'src/assets/ime-dict',name+'.json') - if(!current.includes(name)&&fs.existsSync(old))fs.unlinkSync(old) +const APP=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..') +const RECEIPT='src/assets/crab-input-source.json' +const ROOTS=['keyboard','components/CrabInput','assets/ime-icons'] +const NOTICES={'crab-keyboard-license.txt':'third_party/crabKeyboard/LICENSE','jieba-license.txt':'third_party/jieba/LICENSE','crab-input-license.txt':'LICENSE','crab-input-notice.txt':'NOTICE.md','remix-icon-license.txt':'third_party/remix-icon/LICENSE'} +const digest=bytes=>crypto.createHash('sha256').update(bytes).digest('hex') +const readJson=file=>JSON.parse(fs.readFileSync(file,'utf8')) +function dictionaryReady(source){ + if(fs.existsSync(path.join(source,'src/assets/.ime-dict.generating')))throw Error('Dictionary generation is running; retry after it completes') + const converter=path.join(source,'scripts/convert-dictionary.py'),metaFile=path.join(source,'src/assets/ime-dict/meta.json') + if(!fs.existsSync(converter))return fs.existsSync(metaFile) + if(!fs.existsSync(metaFile))return false + const meta=readJson(metaFile) + const seeds=path.join(source,'src/common/association-seeds.json') + if(fs.existsSync(seeds)&&meta.association_seed_sha256!==digest(fs.readFileSync(seeds)))return false + if(meta.generator_sha256!==digest(fs.readFileSync(converter)))return false + const rawDirectory=path.join(source,'third_party/crabKeyboard/pinyinDictV5_20260416') + if(JSON.stringify(fs.readdirSync(rawDirectory).filter(name=>name.endsWith('.dat')).sort())!==JSON.stringify(Object.keys(meta.source_files||{}).sort()))return false + for(const [name,hash]of Object.entries(meta.source_files||{})){ + if(!/^key[0-9]+\.dat$|^value[0-9]+\.dat$/.test(name))throw Error('Invalid dictionary input filename') + const file=path.join(source,'third_party/crabKeyboard/pinyinDictV5_20260416',name) + if(!fs.existsSync(file)||digest(fs.readFileSync(file))!==hash)return false } + const frequency=path.join(source,'third_party/jieba/dict.txt') + return fs.existsSync(frequency)&&digest(fs.readFileSync(frequency))===meta.t9_frequency_source?.sha256 } -for(const relative of ['keyboard/index.ux','common/ime-engine.js','common/ime-lifecycle.js','common/ime-theme.js','assets/ime-dict','assets/ime-icons']){ - const destination=path.join(target,'src',relative);fs.mkdirSync(path.dirname(destination),{recursive:true});fs.cpSync(path.join(app,'src',relative),destination,{recursive:true}) +function prepareDictionary(source){ + if(dictionaryReady(source))return + const result=spawnSync('python',[path.join(source,'scripts/convert-dictionary.py')],{stdio:'inherit',windowsHide:true}) + if(result.status!==0||!dictionaryReady(source))throw Error('Generate the canonical dictionary before synchronizing') +} +const managed=name=>/^src\/(keyboard\/|components\/CrabInput\/|common\/ime-[a-z0-9-]+\.js$|assets\/ime-(dict|icons)\/|assets\/(crab-keyboard|jieba|crab-input|remix-icon)-(license|notice)\.txt$)/.test(name)&&!name.split('/').some(part=>part==='..'||part==='.')&&!name.includes('\\') +function walk(directory){return fs.readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name)).flatMap(entry=>{ + if(entry.isSymbolicLink())throw Error('Input source must not contain symbolic links') + return entry.isDirectory()?walk(path.join(directory,entry.name)).map(name=>entry.name+'/'+name):[entry.name] +})} +function includesRoute(target){ + const file=path.join(target,'input-component.json') + if(!fs.existsSync(file))return true + const config=readJson(file) + if(config.schema!==1||!['component','route'].includes(config.integration))throw Error('Invalid input-component.json') + return config.integration==='route' +} +export function snapshot(source=APP,includeRoute=true){ + if(!dictionaryReady(source))throw Error('Dictionary inputs changed; regenerate before checking') + const files={} + const add=(name,from)=>{const bytes=fs.readFileSync(from);files[name]={size:bytes.length,sha256:digest(bytes)}} + for(const root of ROOTS.filter(root=>includeRoute||root!=='keyboard'))for(const file of walk(path.join(source,'src',root)))add('src/'+root+'/'+file,path.join(source,'src',root,file)) + for(const file of fs.readdirSync(path.join(source,'src/common')).filter(name=>/^ime-[a-z0-9-]+\.js$/.test(name)))add('src/common/'+file,path.join(source,'src/common',file)) + const meta=readJson(path.join(source,'src/assets/ime-dict/meta.json')) + if(!Array.isArray(meta.shards)||!meta.shards.length||new Set(meta.shards).size!==meta.shards.length)throw Error('Dictionary is not generated or has invalid metadata') + for(const name of ['meta',...meta.shards,...(meta.association_shards||[]),...(meta.completion_shards||[])]){ + if(name!=='meta'&&!/^(short|s?[0-9]+|ds?[0-9a-d]+|[ac][0-9a-f]{2})$/.test(name))throw Error('Invalid generated shard name') + add('src/assets/ime-dict/'+name+'.json',path.join(source,'src/assets/ime-dict',name+'.json')) + } + for(const [name,file]of Object.entries(NOTICES))add('src/assets/'+name,path.join(source,file)) + const sorted=Object.fromEntries(Object.entries(files).sort(([a],[b])=>a.localeCompare(b))) + const body={schema:1,component:'CrabInput',apiVersion:1,integration:includeRoute?'route':'component',inputVersion:readJson(path.join(source,'src/manifest.json')).versionName,files:sorted} + if(!dictionaryReady(source))throw Error('Dictionary changed during snapshot') + return {...body,sourceHash:digest(JSON.stringify(body))} +} +function sourceFile(source,name){ + if(NOTICES[path.posix.basename(name)])return path.join(source,NOTICES[path.posix.basename(name)]) + return path.join(source,name) +} +function targetProject(target,source){ + if(path.resolve(target)===path.resolve(source)||!fs.existsSync(path.join(target,'src/manifest.json')))throw Error('Target must be a separate Glyphix host project') +} +export function checkInput(target,source=APP){ + targetProject(target,source) + const includeRoute=includesRoute(target),current=snapshot(source,includeRoute),receipt=path.join(target,RECEIPT) + if(!fs.existsSync(receipt)||JSON.stringify(readJson(receipt))!==JSON.stringify(current))throw Error('Input source changed; run install.mjs to synchronize before building') + for(const [name,info]of Object.entries(current.files)){ + const file=path.join(target,name) + if(!fs.existsSync(file)||digest(fs.readFileSync(file))!==info.sha256)throw Error('Input host copy differs: '+name) + } + if(snapshot(source,includeRoute).sourceHash!==current.sourceHash)throw Error('Input source changed during verification; retry after edits settle') + return current +} +export function syncInput(target,source=APP){ + targetProject(target,source) + prepareDictionary(source) + const includeRoute=includesRoute(target),current=snapshot(source,includeRoute),receipt=path.join(target,RECEIPT) + let previous=fs.existsSync(receipt)?readJson(receipt):null + const oldMeta=path.join(target,'src/assets/ime-dict/meta.json') + if(!previous&&fs.existsSync(oldMeta)){ + const oldDictionary=readJson(oldMeta),oldShards=[...oldDictionary.shards,...(oldDictionary.association_shards||[]),...(oldDictionary.completion_shards||[])] + if(!Array.isArray(oldShards)||oldShards.some(name=>!/^(short|s?[0-9]+|ds?[0-9a-d]+|[ac][0-9a-f]{2})$/.test(name)))throw Error('Invalid previous dictionary metadata') + previous={files:Object.fromEntries(oldShards.map(name=>['src/assets/ime-dict/'+name+'.json',{}]))} + } + let copied=0,removed=0 + // An interrupted copy must fail the build gate. + if(fs.existsSync(receipt))fs.unlinkSync(receipt) + for(const [name,info]of Object.entries(current.files)){ + const destination=path.join(target,name) + if(fs.existsSync(destination)&&digest(fs.readFileSync(destination))===info.sha256)continue + const bytes=fs.readFileSync(sourceFile(source,name)) + if(digest(bytes)!==info.sha256)throw Error('Input source changed during synchronization; retry') + fs.mkdirSync(path.dirname(destination),{recursive:true});fs.writeFileSync(destination,bytes);copied++ + } + // Only delete recorded, owned files; never touch host assets or business data. + for(const name of Object.keys(previous?.files||{}))if(!current.files[name]){ + if(!managed(name))throw Error('Invalid managed input path') + const destination=path.join(target,name);if(fs.existsSync(destination)){fs.unlinkSync(destination);removed++} + } + const manifestFile=path.join(target,'src/manifest.json'),manifest=readJson(manifestFile) + const before=JSON.stringify(manifest) + if(includeRoute)manifest.router.pages.keyboard={component:'index'} + else delete manifest.router.pages.keyboard + if(JSON.stringify(manifest)!==before)fs.writeFileSync(manifestFile,JSON.stringify(manifest,null,2)+'\n') + if(snapshot(source,includeRoute).sourceHash!==current.sourceHash)throw Error('Input source changed during synchronization; retry after edits settle') + fs.writeFileSync(receipt,JSON.stringify(current,null,2)+'\n') + return {sourceHash:current.sourceHash,files:Object.keys(current.files).length,copied,removed} +} +export function watchInput(target,source=APP,onSync=console.log){ + let timer=null,stopped=false,lastHash='' + const synchronize=()=>{ + timer=null;if(stopped)return + try{prepareDictionary(source);const hash=snapshot(source,includesRoute(target)).sourceHash;if(hash!==lastHash){const result=syncInput(target,source);lastHash=result.sourceHash;onSync({status:'synchronized',...result})}} + catch(error){lastHash='';onSync({status:'retrying',error:error.message})} + } + const schedule=()=>{if(timer)clearTimeout(timer);timer=setTimeout(synchronize,200)} + const watchers=['src','third_party','LICENSE','NOTICE.md'].map(root=>fs.watch(path.join(source,root),{recursive:true},schedule)) + const poll=setInterval(synchronize,2000);synchronize() + return ()=>{stopped=true;if(timer)clearTimeout(timer);clearInterval(poll);for(const watcher of watchers)watcher.close()} +} +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ + const [target,...flags]=process.argv.slice(2) + if(!target||flags.some(flag=>!['--check','--watch'].includes(flag))||flags.length>1)throw Error('Usage: node scripts/install.mjs [--check | --watch]') + if(flags.includes('--watch')){ + const stop=watchInput(path.resolve(target));process.once('SIGINT',()=>{stop();process.exit(0)});process.once('SIGTERM',()=>{stop();process.exit(0)}) + }else if(flags.includes('--check')){const result=checkInput(path.resolve(target));console.log('Input synchronized:',result.sourceHash)} + else console.log('Input synchronized:',syncInput(path.resolve(target))) } -const license=path.join(target,'src/assets/crab-keyboard-license.txt') -fs.copyFileSync(path.join(app,'third_party/crabKeyboard/LICENSE'),license) -fs.copyFileSync(path.join(app,'third_party/jieba/LICENSE'),path.join(target,'src/assets/jieba-license.txt')) -const manifestPath=path.join(target,'src/manifest.json'),manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8')) -manifest.router.pages.keyboard={component:'index'} -fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+'\n') -console.log('Installed canonical offline keyboard:',manifest.package) diff --git a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js index 8224ded..a0e1c98 100644 --- a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js +++ b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js @@ -55,11 +55,21 @@ export function erase(text,cursor) { if(at)value.splice(at-1,1) return {text:value.join(''),cursor:Math.max(0,at-1)} } -export function preview(text,cursor,secret) { +export function previewParts(text,cursor,secret) { const value=chars(text),at=Math.max(0,Math.min(value.length,cursor)) const left=value.slice(Math.max(0,at-15),at),right=value.slice(at,at+7) - return (at>15?'…':'')+(secret?'•'.repeat(left.length):left.join(''))+'│'+(secret?'•'.repeat(right.length):right.join(''))+(at+7list.reduce((sum,ch)=>sum+(secret?.6:/[MW@#%]/.test(ch)?.95:ch.codePointAt(0)>0x2e7f?1:.6),0) + // Keep the cursor in the narrowest round preview, including both ellipses. + // The actual cursor is a separate one-pixel element, never a font glyph. + const size=()=>width(left)+width(right)+(left.length7&&(left.length||right.length)){ + if(left.length&&(width(left)>4.5||!right.length))left.shift() + else if(right.length)right.pop() + else left.shift() + } + return {before:(left.length=0)inputs.splice(index,1)} +export function activeInputs(){return inputs.slice()} export function clearPrivateInputs(router) { + for(const input of activeInputs())input.onHide() for(const page of router.getPages()) { if(page.secret&&typeof page.clearPrivate==='function')page.clearPrivate() } diff --git a/com.sydxky.rtos.inputmethod/src/common/ime-theme.js b/com.sydxky.rtos.inputmethod/src/common/ime-theme.js index 5b4cc16..e8ba7e3 100644 --- a/com.sydxky.rtos.inputmethod/src/common/ime-theme.js +++ b/com.sydxky.rtos.inputmethod/src/common/ime-theme.js @@ -9,6 +9,7 @@ export function themeStyles(value) { return { surface:'background-color:'+c.bg+';'+ink, card:'background-color:'+c.card+';'+ink, + caret:'background-color:'+c.text+';', raised:'background-color:'+c.raised+';'+ink, cyan:'color:'+(dark?'#4DB5FF':'#2867D9')+';', ink,muted,dim:'color:'+c.dim+';',accent:'color:'+c.accent+';', diff --git a/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux b/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux new file mode 100644 index 0000000..53aee8d --- /dev/null +++ b/com.sydxky.rtos.inputmethod/src/components/CrabInput/index.ux @@ -0,0 +1,162 @@ + + + diff --git a/com.sydxky.rtos.inputmethod/src/keyboard/index.ux b/com.sydxky.rtos.inputmethod/src/keyboard/index.ux index 20c644a..6b094fa 100644 --- a/com.sydxky.rtos.inputmethod/src/keyboard/index.ux +++ b/com.sydxky.rtos.inputmethod/src/keyboard/index.ux @@ -1,152 +1,18 @@ - - + + + diff --git a/com.sydxky.rtos.inputmethod/src/manifest.json b/com.sydxky.rtos.inputmethod/src/manifest.json index 01e86c8..fb2f830 100644 --- a/com.sydxky.rtos.inputmethod/src/manifest.json +++ b/com.sydxky.rtos.inputmethod/src/manifest.json @@ -2,8 +2,8 @@ "package": "com.sydxky.rtos.inputmethod", "name": "蟹蟹输入", "icon": "/assets/icon.png", - "versionName": "1.2.3", - "versionCode": 10203, + "versionName": "1.2.4", + "versionCode": 10204, "features": [], "router": { "entry": "main", diff --git a/com.sydxky.rtos.inputmethod/tests/client.test.mjs b/com.sydxky.rtos.inputmethod/tests/client.test.mjs index a1a194b..08b1998 100644 --- a/com.sydxky.rtos.inputmethod/tests/client.test.mjs +++ b/com.sydxky.rtos.inputmethod/tests/client.test.mjs @@ -3,15 +3,15 @@ import vm from 'node:vm' import assert from 'node:assert/strict' import * as engine from '../src/common/ime-engine.js' import {normalizeTheme,themeStyles as stylesForTheme} from '../src/common/ime-theme.js' -const ux=fs.readFileSync(new URL('../src/keyboard/index.ux',import.meta.url),'utf8') +const ux=fs.readFileSync(new URL('../src/components/CrabInput/index.ux',import.meta.url),'utf8') const source=ux.match(/''' + +def main(): + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--work',type=pathlib.Path,required=True);parser.add_argument('--output',type=pathlib.Path,required=True);parser.add_argument('--profiles',nargs='+',default=emu.PROFILES) + args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True);fixture=args.work/'fixture';fixture.mkdir(parents=True,exist_ok=True) + shutil.copytree(APP/'src',fixture/'src',dirs_exist_ok=True);shutil.copy2(APP/'glyphix.config.js',fixture/'glyphix.config.js') + metadata=json.loads((fixture/'src/manifest.json').read_text(encoding='utf-8'));metadata['router']['entry']='native-probe';metadata['router']['pages']['native-probe']={'component':'index'} + (fixture/'src/manifest.json').write_text(json.dumps(metadata,ensure_ascii=False),encoding='utf-8') + (fixture/'src/native-probe').mkdir(exist_ok=True);(fixture/'src/native-probe/index.ux').write_text(PROBE,encoding='utf-8') + source_hash=hashlib.sha256((APP/'src/components/CrabInput/index.ux').read_bytes()).hexdigest() + assert (fixture/'src/components/CrabInput/index.ux').read_bytes()==(APP/'src/components/CrabInput/index.ux').read_bytes() + shell=shutil.which('pwsh')or'powershell';stable=pathlib.Path(r'C:\Users\herry\AppData\Local\Programs\PowerShell\7\pwsh.exe') + if stable.exists():shell=str(stable) + with (args.output/'fixture-build.log').open('w',encoding='utf-8')as log: + result=subprocess.run([shell,'-NoProfile','-File',str(APP.parent/'build-rtos-game.ps1'),'-ProjectRoot',str(fixture)],stdout=log,stderr=subprocess.STDOUT) + assert result.returncode==0,'See fixture-build.log' + report={'method':'owned native console; production keyboard copied byte-for-byte into an auto-running fixture','production_keyboard_sha256':source_hash,'production_keyboard_path':'src/components/CrabInput/index.ux','production_route_sha256':hashlib.sha256((APP/'src/keyboard/index.ux').read_bytes()).hexdigest(),'inspector_used':False,'cases':[],'profiles':[],'passed':False} + def rectangle(value): + if isinstance(value,dict)and'rect'in value:value=value['rect'] + return [value.get('left',value.get('x',0)),value.get('top',value.get('y',0)),value.get('width',value.get('w',0)),value.get('height',value.get('h',0))] + for profile in args.profiles: + image,artifacts=emu.prepare(profile,args.work) + device='generic-watch-'+('466x466'if profile.startswith('round')else'390x450');package=metadata['package'] + binary=fixture/'.glyphix-work'/('dist/'+device+'-quickapp/'+package+'/bundle.pkg'if profile.endswith('-quickapp')else'image/'+device+'/apps/'+package+'.pkg') + shutil.copy2(binary,image/'apps'/(package+'.pkg')) + command=[str(emu.ROOT/'temp/glyphix-v0.7.2-windows-x64/bin/glyphix-emu.exe'),'--config','pkgs.db','--target',package] + if profile.startswith('round'):command+=['--width','624','--height','624','--dpi','326','--font-size','32','--round','--surface','surface/surface-466x466.json'] + else:command+=['--width','390','--height','450','--dpi','321','--font-size','29'] + command+=['--memory-profile','8192','--font-map','font-faces','--font-family','sans-serif'] + log_path=args.output/(profile+'-console.log');seen=set();finished=False;captures=0 + with log_path.open('w',encoding='utf-8')as log: + process=subprocess.Popen(command,cwd=image,stdin=subprocess.PIPE,stdout=log,stderr=subprocess.STDOUT,creationflags=subprocess.CREATE_NO_WINDOW) + try: + deadline=time.monotonic()+100 + while time.monotonic()width or y+h>height:failures.append(key['key']) + if profile.startswith('round')and max(math.hypot(tx-233,ty-233)for tx in [x,x+w]for ty in [y,y+h])>231:failures.append(key['key']) + state.update(failures=failures,passed=len(state['keys'])==26 and not failures) + captures+=1;report['cases'].append(state);native.save(args.output/'caret-results.json',report);print(json.dumps(state,ensure_ascii=False),flush=True) + if 'PROBE_DONE'in line:finished=True + if finished:break + time.sleep(.05) + assert finished and captures==24,'Native fixture did not finish; see '+str(log_path) + assert 'terminated:'not in log_path.read_text(encoding='utf-8',errors='replace'),'Native assertion failed' + for state in report['cases']: + if state['profile']!=profile:continue + filename=profile+'-'+state['name']+'.png';snapshot=image/filename + assert snapshot.is_file() and snapshot.read_bytes().startswith(b'\x89PNG\r\n\x1a\n'),'Screenshot missing: '+filename + shutil.copy2(snapshot,args.output/filename);state['screenshot']=filename + report['profiles'].append({'profile':profile,'native_case_switch_passed':True,'captures':captures,'fixture_binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest()}) + finally: + if process.poll()is None:process.terminate() + process.wait(timeout=10);process.stdin.close() + report['passed']=all(row['passed']for row in report['cases']);native.save(args.output/'caret-results.json',report) + assert report['passed'],'See caret-results.json' + +if __name__=='__main__':main() diff --git a/com.sydxky.rtos.inputmethod/tests/engine.test.mjs b/com.sydxky.rtos.inputmethod/tests/engine.test.mjs index 2bba527..fa863a2 100644 --- a/com.sydxky.rtos.inputmethod/tests/engine.test.mjs +++ b/com.sydxky.rtos.inputmethod/tests/engine.test.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs' import path from 'node:path' import assert from 'node:assert/strict' import {fileURLToPath} from 'node:url' -import {createDictionary,digitsOf,code14Of,KEY14_GROUPS,display14,candidatePages,edit,erase,preview,learn,sanitizePreferences,chars} from '../src/common/ime-engine.js' +import {createDictionary,digitsOf,code14Of,KEY14_GROUPS,display14,candidatePages,edit,erase,preview,previewParts,learn,sanitizePreferences,chars} from '../src/common/ime-engine.js' const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../src') let count=0 function check(ok,label){assert.ok(ok,label);count++} @@ -55,6 +55,12 @@ check(erase('你😀好',2).text==='你好','Unicode backspace preserves surroga check(edit('你好',2,'世界',3).limited,'maximum length enforced') check(!preview('my-secret',5,true).includes('secret'),'secret preview masked') check(chars('😀').length===1,'Unicode length counts codepoints') +check(previewParts('不',1,false).before==='不'&&previewParts('不',1,false).after==='','end cursor has no inserted spacer or font-glyph advance') +check(previewParts('你😀好',2,false).before==='你😀'&&previewParts('你😀好',2,false).after==='好','middle cursor splits at a Unicode codepoint boundary') +check(previewParts('你好',0,false).before===''&&previewParts('你好',0,false).after==='你好','start cursor precedes text without padding') +check(previewParts('a b',2,false).before==='a '&&previewParts('a b',2,false).after==='b','intentional spaces are preserved') +check(!JSON.stringify(previewParts('my-secret',5,true)).includes('secret'),'split preview masks password on both sides') +check(chars(previewParts('长😀'.repeat(1000),1000,false).before).length<=8&&chars(previewParts('长😀'.repeat(1000),1000,false).after).length<=8,'long preview keeps the real cursor inside round-screen width') 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') diff --git a/com.sydxky.rtos.inputmethod/tests/native.py b/com.sydxky.rtos.inputmethod/tests/native.py index 6b23a58..a1f7874 100644 --- a/com.sydxky.rtos.inputmethod/tests/native.py +++ b/com.sydxky.rtos.inputmethod/tests/native.py @@ -45,29 +45,29 @@ def main(): if not entry['passed']:raise RuntimeError('Native failure '+case+': '+str(logs[-4:])) if not geometry['passed']:raise RuntimeError('Clipped native layout '+case+': '+str(geometry['failures'][:3])) try: - emu.evaluate("var router=require('@system.router').default,tc=require('@system.test').default,launch=require('@system.launch').default,root=router.getPage(0),p=null;function check(ok,m){if(!ok)throw new Error(m);console.log('IME_CHECK '+m)};async function click(t){var ids={'取消':'ime-cancel','空格':'ime-space','删除':'ime-delete','完成':'ime-finish','更多':'ime-expand','编辑':'ime-tools','上一页':'ime-page-prev','下一页':'ime-page-next','键盘':'ime-back','返回键盘':'ime-back'};if(ids[t]){await tc.getById(ids[t]).click();return}var pattern=t.split('').map(function(c){return String.fromCharCode(92)+'u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4)}).join('');await tc.getHasText(new RegExp('^'+pattern+'$')).click()}") - emu.evaluate("async function ready(){var end=Date.now()+5000;while(p.busy&&Date.now()input.$page===router.getPage(1))[0];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('你好');await ready();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('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;irow.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('14-key-phrase',"reset();await click('14键');check(p.mode==='14'&&p.keyRows.flat().length===14,'14-key mode has fourteen real buttons');for(var label of ['BN','UI','GH','AS','OP'])await click(label);await ready();check(p.bar.some(x=>x.word==='你好')&&p.allCandidates.some(x=>x.word==='不好'),'paired ambiguity retains both valid phrases');await click('完成');check(router.getLength()===2,'14-key uncommitted input blocks finish');await click('123');check(p.mode==='14'&&p.composition==='c3754','14-key mode switch preserves composition');await click('你好');check(p.text==='你好'&&!p.composition,'native 14-key phrase commit');") + run('14-key-phrase',"reset();await click('14键');check(p.mode==='14'&&p.keyRows.flat().length===14,'14-key mode has fourteen real buttons');for(var label of ['BN','UI','GH','AS','OP'])await click(label);await ready();check(p.bar.some(x=>x.word==='你好')&&p.allCandidates.some(x=>x.word==='不好'),'paired ambiguity retains both valid phrases');await click('完成');check(router.getLength()===2,'14-key uncommitted input blocks finish');await click('123');check(p.mode==='14'&&p.composition==='c3754','14-key mode switch preserves composition');await click('你好');await ready();check(p.text==='你好'&&!p.composition,'native 14-key phrase commit');") run('14-key-continuous',"reset();await click('14键');for(var label of ['BN','UI','GH','AS','OP','AS','GH','UI','JK','UI','ER'])await click(label);await ready();await click('你好');await ready();check(p.composition.length===6,'14-key consumes five matched keys only');await click('世界');check(p.text==='你好世界'&&!p.composition,'14-key continuous phrase commit');") run('14-key-candidates',"reset();await click('14键');for(var label of ['AS','GH','UI'])await click(label);await ready();check(p.total>20,'14-key full dictionary candidates');await click('更多');check(p.visibleCandidates.length<=16&&p.visibleCandidates.length>4,'14-key adaptive candidate grid');await click('下一页');check(p.candidatePage===1,'14-key native next candidate page');await click('上一页');check(p.candidatePage===0,'14-key native previous candidate page');var selected=p.visibleCandidates[1];await click(selected.word);check(p.text===selected.word,'14-key expanded candidate commit');") - run('english',"reset();await click('abc');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','English follows standard QWERTY');await click('abc');await click('Z');await click('ABC');await click('a');check(p.text==='Za','uppercase and lowercase native keys');") + run('english',"reset();await click('abc');check(p.keyRows.map(row=>row.map(key=>key.value).join('')).join('/')==='qwertyuiop/asdfghjkl/zxcvbnm','English follows standard QWERTY');await click('大写');await click('Z');await click('小写');await click('a');check(p.text==='Za','uppercase and lowercase native keys');") run('symbols',"reset();await click('符号');for(var symbol of ['@','%','&','?',',','。']){if(!p.keyRows.some(row=>row.some(key=>key.value===symbol)))await click('符号');await click(symbol)}check(p.text==='@%&?,。','ASCII and Chinese punctuation native keys');") run('cursor-unicode',"reset();p.text='你😀好';p.cursor=2;p.updateDisplay();await click('编辑');await click('删除一字');check(p.text==='你好','Unicode-safe deletion');await click('到开头');await click('返回键盘');await click('abc');await click('a');check(p.text==='a你好','insertion at moved cursor');") run('hold-delete',"reset();p.mode='en';p.renderKeys();p.text='abcdefghijklmnop';p.cursor=16;p.updateDisplay();await tc.wait(.25);await tc.getById('ime-delete').scroll(0,0,.95);await tc.wait(.1);check(p.text.length<15,'hold repeats delete');check(p.repeatTimer===null&&p.repeatDelay===null,'release stops repeats');") run('length-and-mode',"reset();p.limit=2;p.text='你';p.cursor=1;p.mode='py';p.renderKeys();p.composition='nihao';p.schedule();await ready();await click('你好');check(p.text==='你'&&p.composition==='nihao','overlong candidate not truncated');await click('完成');check(router.getLength()===2,'uncommitted composition not dropped');p.mode='t9';p.composition='64';p.renderKeys();p.english();check(p.mode==='t9'&&p.composition==='64','mode switch preserves uncommitted composition');") run('learning',"reset();await click('编辑');check(!p.learning,'learning defaults off');await click('学习:关');check(p.learning,'learning opt-in');await click('返回键盘');p.composition='64';p.schedule();await ready();await click(p.bar[1].word);check(Object.keys(p.preferences).length>0,'only chosen candidate learned');await click('编辑');await click('清除学习');check(Object.keys(p.preferences).length===0,'learning cleared');await click('学习:开');await click('返回键盘');") run('cancel',"p.text='discard';p.composition='';await click('取消');await tc.wait(.4);check(root.text===''&&router.getLength()===1,'cancel keeps original document');") - run('numeric',"await click('数字输入');await tc.wait(.3);p=router.getPage(1);check(p.mode==='num','numeric field auto keyboard');for(var digit of ['1','2','3','0'])await click(digit);await click('完成');await tc.wait(.3);check(root.status.indexOf('1230')>=0,'numeric result returned');") - run('password-privacy',"await click('私密输入');await tc.wait(.3);p=router.getPage(1);await click('s');await click('e');check(p.secret&&p.display.indexOf('s')<0&&!p.learning,'secret preview masked');await click('编辑');await click('学习:关');check(!p.learning,'secret cannot enable learning');await launch.launch('com.sydxky.rtos.imefixture');await tc.wait(.9);check(!p.active&&!p.text&&!p.initial,'secret erased on actual app switch');await tc.wait(2);await click('取消');") - run('long-text-clear',"await click('开始输入');await tc.wait(.3);p=router.getPage(1);p.text='长😀'.repeat(1000);p.cursor=2000;p.updateDisplay();check(p.count===2000&&p.display.length<60,'long text preview bounded');await click('编辑');await click('清空全文');check(p.count===2000,'clear needs confirmation');await click('确认清空');check(p.count===0,'confirmed clear');await click('返回键盘');") + run('numeric',"await click('数字输入');await tc.wait(.3);p=require('pkg://com.sydxky.rtos.inputmethod/common/ime-lifecycle.js').activeInputs().filter(input=>input.$page===router.getPage(1))[0];check(p.mode==='num','numeric field auto keyboard');for(var digit of ['1','2','3','0'])await click(digit);await click('完成');await tc.wait(.3);check(root.status.indexOf('1230')>=0,'numeric result returned');") + run('password-privacy',"await click('私密输入');await tc.wait(.3);p=require('pkg://com.sydxky.rtos.inputmethod/common/ime-lifecycle.js').activeInputs().filter(input=>input.$page===router.getPage(1))[0];await click('s');await click('e');check(p.secret&&p.display.indexOf('s')<0&&!p.learning,'secret preview masked');await click('编辑');await click('学习:关');check(!p.learning,'secret cannot enable learning');await launch.launch('com.sydxky.rtos.imefixture');await tc.wait(.9);check(!p.$valid||!p.active&&!p.text&&!p.initial,'secret erased on actual app switch');await tc.wait(2);await click('取消');") + run('long-text-clear',"await click('开始输入');await tc.wait(.3);p=require('pkg://com.sydxky.rtos.inputmethod/common/ime-lifecycle.js').activeInputs().filter(input=>input.$page===router.getPage(1))[0];p.text='长😀'.repeat(1000);p.cursor=2000;p.updateDisplay();check(p.count===2000&&p.display.length<60,'long text preview bounded');await click('编辑');await click('清空全文');check(p.count===2000,'clear needs confirmation');await click('确认清空');check(p.count===0,'confirmed clear');await click('返回键盘');") run('lookup-performance',"reset();var times=[];for(var word of ['nihao','zhongguo','shijie','shurufa','beijing','tianqi','shanghai','gongzuo']){var now=Date.now();var candidates=await p.dictionary.lookup(word,false,{});times.push(Date.now()-now);check(candidates.length>0,'lookup '+word)}check(p.dictionary.cacheSize<=4,'bounded decoded shard cache');check(Math.max.apply(null,times)<1000,'emulator lookup under one second');console.log('IME_TIMING '+JSON.stringify(times));") run('finish-save',"reset();p.insert('你好,RTOS!');await click('完成');await tc.wait(.4);check(root.text==='你好,RTOS!'&&router.getLength()===1,'finished text returned to opener');await click('保存草稿');check(root.saved,'explicit local draft save');") finally: diff --git a/com.sydxky.rtos.inputmethod/tests/sync.test.mjs b/com.sydxky.rtos.inputmethod/tests/sync.test.mjs new file mode 100644 index 0000000..9a879e8 --- /dev/null +++ b/com.sydxky.rtos.inputmethod/tests/sync.test.mjs @@ -0,0 +1,54 @@ +import fs from 'node:fs' +import path from 'node:path' +import {fileURLToPath} from 'node:url' +import assert from 'node:assert/strict' +import {syncInput,checkInput,watchInput} from '../scripts/install.mjs' +const parent=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../.glyphix-work') +fs.mkdirSync(parent,{recursive:true}) +const scratch=fs.mkdtempSync(path.join(parent,'sync-test-')),source=path.join(scratch,'source'),host=path.join(scratch,'host') +function write(root,name,body){const file=path.join(root,name);fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,body)} +function fixture(root){ + write(root,'src/manifest.json',JSON.stringify({versionName:'fixture',router:{pages:{}}})) + for(const name of ['keyboard/index.ux','components/CrabInput/index.ux','assets/ime-icons/icon.png',...['engine','theme','lifecycle'].map(n=>'common/ime-'+n+'.js')])write(root,'src/'+name,'first') + write(root,'src/assets/ime-dict/meta.json',JSON.stringify({shards:['2222']}));write(root,'src/assets/ime-dict/2222.json','{}') + for(const name of ['crabKeyboard','jieba','remix-icon'])write(root,'third_party/'+name+'/LICENSE','MIT') + write(root,'LICENSE','MIT');write(root,'NOTICE.md','third party notices') +} +let stop +try{ + fixture(source);write(host,'src/manifest.json',JSON.stringify({router:{pages:{}}}));write(host,'src/assets/host-data.txt','preserve') + write(host,'src/assets/ime-dict/meta.json',JSON.stringify({shards:['1111']}));write(host,'src/assets/ime-dict/1111.json','old format') + const first=syncInput(host,source);assert.equal(first.copied,13);assert.equal(first.removed,1);checkInput(host,source) + const timestamp=fs.statSync(path.join(host,'src/components/CrabInput/index.ux')).mtimeMs + assert.equal(syncInput(host,source).copied,0);assert.equal(fs.statSync(path.join(host,'src/components/CrabInput/index.ux')).mtimeMs,timestamp) + write(host,'src/common/ime-theme.js','stale');assert.throws(()=>checkInput(host,source),/differs/);assert.equal(syncInput(host,source).copied,1) + write(source,'src/common/ime-engine.js','second');assert.throws(()=>checkInput(host,source),/changed/) + write(host,'input-component.json',JSON.stringify({schema:1,integration:'component'}));syncInput(host,source) + assert.ok(!JSON.parse(fs.readFileSync(path.join(host,'src/manifest.json'),'utf8')).router.pages.keyboard) + assert.ok(!fs.existsSync(path.join(host,'src/keyboard/index.ux'))) + write(source,'src/keyboard/index.ux','unused compatibility route update');checkInput(host,source) + const events=[];stop=watchInput(host,source,event=>events.push(event)) + const initial=events.length + write(source,'src/components/CrabInput/index.ux','component update') + write(source,'src/common/ime-theme.js','theme update') + write(source,'src/assets/ime-icons/new-icon.png','new icon') + write(source,'src/common/ime-layout.js','new related module') + write(source,'src/assets/ime-dict/meta.json',JSON.stringify({shards:['3333'],association_shards:['a0f'],completion_shards:['c01']}));write(source,'src/assets/ime-dict/3333.json','{"changed":true}');write(source,'src/assets/ime-dict/a0f.json','{}');write(source,'src/assets/ime-dict/c01.json','{}') + const deadline=Date.now()+8000 + while(Date.now()setTimeout(resolve,50));try{checkInput(host,source);break}catch{}} + const current=checkInput(host,source) + assert.ok(fs.existsSync(path.join(host,'src/assets/ime-dict/a0f.json'))&&fs.existsSync(path.join(host,'src/assets/ime-dict/c01.json')),'prediction shards synchronize') + assert.notEqual(current.sourceHash,first.sourceHash);assert.ok(events.length>initial) + assert.equal(fs.readFileSync(path.join(host,'src/common/ime-layout.js'),'utf8'),'new related module') + assert.ok(!fs.existsSync(path.join(host,'src/assets/ime-dict/2222.json'))) + assert.equal(fs.readFileSync(path.join(host,'src/assets/host-data.txt'),'utf8'),'preserve') + const receipt=JSON.parse(fs.readFileSync(path.join(host,'src/assets/crab-input-source.json'),'utf8')) + receipt.files['../outside']={};write(host,'src/assets/crab-input-source.json',JSON.stringify(receipt)) + assert.throws(()=>syncInput(host,source),/Invalid managed/) + assert.ok(!fs.existsSync(path.join(host,'src/assets/crab-input-source.json')),'failed sync invalidates receipt') + console.log(JSON.stringify({suite:'input-sync',status:'passed',watchEvents:events.length,checks:['incremental','tamper rejection','source drift rejection','watch component engine theme icon dictionary update','obsolete shard removal','host isolation','invalid receipt rejection']})) +}finally{ + if(stop)stop() + if(!scratch.startsWith(parent+path.sep+'sync-test-'))throw Error('Unexpected test directory') + fs.rmSync(scratch,{recursive:true,force:true}) +} diff --git a/com.sydxky.rtos.inputmethod/tests/visual.py b/com.sydxky.rtos.inputmethod/tests/visual.py index 00b57c9..600b684 100644 --- a/com.sydxky.rtos.inputmethod/tests/visual.py +++ b/com.sydxky.rtos.inputmethod/tests/visual.py @@ -50,7 +50,7 @@ def main(): try: capture(profile,'home') emu.evaluate("var r=require('@system.router').default,t=require('@system.test').default;var root=r.getPage(0);t.getByClass('document').click()") - time.sleep(1);emu.evaluate("var p=r.getPage(1);p.composition='64426';p.schedule()") + time.sleep(1);emu.evaluate("var p=require('pkg://com.sydxky.rtos.inputmethod/common/ime-lifecycle.js').activeInputs().filter(input=>input.$page===r.getPage(1))[0];p.composition='64426';p.schedule()") time.sleep(.5);capture(profile,'t9') emu.evaluate("p.composition='shi';p.mode='py';p.renderKeys();p.schedule()") time.sleep(.5);capture(profile,'pinyin') diff --git a/docs/1.2.4/rect-bluewhite-middle.png b/docs/1.2.4/rect-bluewhite-middle.png new file mode 100644 index 0000000..e7fa1f5 Binary files /dev/null and b/docs/1.2.4/rect-bluewhite-middle.png differ diff --git a/docs/1.2.4/rect-bluewhite-py-lower.png b/docs/1.2.4/rect-bluewhite-py-lower.png new file mode 100644 index 0000000..2fd1fc4 Binary files /dev/null and b/docs/1.2.4/rect-bluewhite-py-lower.png differ diff --git a/docs/1.2.4/round-blackgreen-en-lower.png b/docs/1.2.4/round-blackgreen-en-lower.png new file mode 100644 index 0000000..fe59519 Binary files /dev/null and b/docs/1.2.4/round-blackgreen-en-lower.png differ diff --git a/docs/1.2.4/round-bluewhite-en-lower.png b/docs/1.2.4/round-bluewhite-en-lower.png new file mode 100644 index 0000000..121a493 Binary files /dev/null and b/docs/1.2.4/round-bluewhite-en-lower.png differ diff --git a/docs/1.2.4/round-bluewhite-en-upper.png b/docs/1.2.4/round-bluewhite-en-upper.png new file mode 100644 index 0000000..01ea357 Binary files /dev/null and b/docs/1.2.4/round-bluewhite-en-upper.png differ diff --git a/docs/1.2.4/round-bluewhite-middle.png b/docs/1.2.4/round-bluewhite-middle.png new file mode 100644 index 0000000..c0420ee Binary files /dev/null and b/docs/1.2.4/round-bluewhite-middle.png differ diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md index 66404bb..7f9d293 100644 --- a/docs/SCREENSHOTS.md +++ b/docs/SCREENSHOTS.md @@ -1,3 +1,20 @@ +# 1.2.4 RTOS原生组件截图 + +以下画面来自独立原生模拟器测试工程,生产keyboard/index.ux逐字节相同,仅增加自动测试入口。四配置96个状态与96张截图通过,未使用替代绘图。这不是最终发布包的全套端到端验收;完整96张图、原始日志和源码摘要见1.2.4验收ZIP。 + +UI_PREVIEW.png为本版圆屏英文小写画面。白蓝和黑绿主题、圆方屏26键及光标样例: + +![圆屏小写](1.2.4/round-bluewhite-en-lower.png) +![圆屏大写](1.2.4/round-bluewhite-en-upper.png) +![方屏全拼](1.2.4/rect-bluewhite-py-lower.png) +![黑绿英文](1.2.4/round-blackgreen-en-lower.png) +![圆屏文字中间光标](1.2.4/round-bluewhite-middle.png) +![方屏文字中间光标](1.2.4/rect-bluewhite-middle.png) + +Android本版只有后台原生组件回归,没有前台截图。以下旧图片及旧说明均保留为明确的历史资料。 + +## 历史截图记录 + # 截图版本说明 当前1.2.3在OWW261上运行后台离屏原生组件测试,没有采集本版前台截图。UI_PREVIEW.png及下列图片分别来自历史1.2.2和1.2.0,不作为1.2.3前台视觉验收。 diff --git a/docs/UI_PREVIEW.png b/docs/UI_PREVIEW.png index 0bac7cb..121a493 100644 Binary files a/docs/UI_PREVIEW.png and b/docs/UI_PREVIEW.png differ