cj-tauri 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/host_arkweb.cj
CHANGED
|
@@ -1,778 +1,924 @@
|
|
|
1
|
-
package cjTauri
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 鸿蒙 ArkWeb 宿主(`@When[env == "ohos"]` 门禁)
|
|
5
|
-
*
|
|
6
|
-
* ## 为什么判据是 `env == "ohos"`
|
|
7
|
-
*
|
|
8
|
-
* 鸿蒙目标下 `os` 仍报 `Linux`(探针实测),与桌面 Linux 撞车;`arch` 只能把 x86_64 桌面
|
|
9
|
-
* 与 aarch64 分开,可桌面 aarch64 Linux 一样是 `aarch64`——用它当判据会把「跑在 GTK 上」
|
|
10
|
-
* 误判成「跑在 ArkWeb 上」。官方条件编译映射表给的是 `env`:`aarch64-linux-ohos` → `env == "ohos"`,
|
|
11
|
-
* 桌面 Linux 是 `gnu`,两者才真正互斥(`docs/` 里的条件编译文档「目标平台和条件编译映射表」)。
|
|
12
|
-
*
|
|
13
|
-
* ## 为什么 ArkUI 组件不在这里
|
|
14
|
-
*
|
|
15
|
-
* 框架核心包一旦 import `kit.ArkUI` / `ohos.arkui.*`,**所有**消费方(含桌面构建)都要跟着
|
|
16
|
-
* 拉 ArkUI 依赖。所以本文件只做「与 UI 无关的那半边」:页面来源定稿、脚本队列、IPC 接线、
|
|
17
|
-
* 生命周期标志;建 `Web` 组件的那半边在应用侧(`ArkWebView` 样板),经 [ArkWebRuntime] 取用。
|
|
18
|
-
*
|
|
19
|
-
* ## 线程模型(与桌面两平台的根本差异)
|
|
20
|
-
*
|
|
21
|
-
* 桌面:`start()` 里阻塞着跑宿主事件循环,`waitForExit()` 等它退出。
|
|
22
|
-
* 鸿蒙:ArkUI 的事件循环归系统所有(UI 线程),`start()` **不能**阻塞——它只登记页面来源,
|
|
23
|
-
* 由 UI 线程上的 `Web` 组件在 build 时取走。于是 `TauriApp.run()` 必须跑在非 UI 线程
|
|
24
|
-
* (应用在 Ability 里 `spawn`),UI 线程负责建树。
|
|
25
|
-
*
|
|
26
|
-
* 跨线程只走两条路:① 本文件的队列(`Mutex` 保护)② 页面消息经 `javaScriptProxy`
|
|
27
|
-
* 同步进来。**不要在应用线程直接调 ArkWeb/ArkUI**——与桌面 Linux 侧
|
|
28
|
-
* 「GTK/WebKit 调用必须在原生 pthread 内」同源的约束。
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
@When[env == "ohos"]
|
|
32
|
-
import std.collection.*
|
|
33
|
-
|
|
34
|
-
@When[env == "ohos"]
|
|
35
|
-
import std.fs.File
|
|
36
|
-
|
|
37
|
-
@When[env == "ohos"]
|
|
38
|
-
import std.sync.*
|
|
39
|
-
|
|
40
|
-
@When[env == "ohos"]
|
|
41
|
-
import stdx.encoding.json.JsonArray
|
|
42
|
-
|
|
43
|
-
@When[env == "ohos"]
|
|
44
|
-
import stdx.encoding.json.JsonString
|
|
45
|
-
|
|
46
|
-
// 页面来源契约(load(source) 收敛,RFC §4.3):Asset 由胶水注入的 rawfile 解析器材化
|
|
47
|
-
@When[env == "ohos"]
|
|
48
|
-
import cjTauri.platform.*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/*
|
|
68
|
-
static var
|
|
69
|
-
|
|
70
|
-
/*
|
|
71
|
-
static let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
synchronized(ArkWebRuntime.queueLock) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
public
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
private var
|
|
211
|
-
private
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
private var
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
private var
|
|
218
|
-
|
|
219
|
-
private var
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
public func
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
public func
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
eprintln("[cj-tauri] ArkWeb:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
var
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
eprintln("[cj-tauri] ArkWeb:
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
*
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this.jsInCount
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
1
|
+
package cjTauri
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 鸿蒙 ArkWeb 宿主(`@When[env == "ohos"]` 门禁)
|
|
5
|
+
*
|
|
6
|
+
* ## 为什么判据是 `env == "ohos"`
|
|
7
|
+
*
|
|
8
|
+
* 鸿蒙目标下 `os` 仍报 `Linux`(探针实测),与桌面 Linux 撞车;`arch` 只能把 x86_64 桌面
|
|
9
|
+
* 与 aarch64 分开,可桌面 aarch64 Linux 一样是 `aarch64`——用它当判据会把「跑在 GTK 上」
|
|
10
|
+
* 误判成「跑在 ArkWeb 上」。官方条件编译映射表给的是 `env`:`aarch64-linux-ohos` → `env == "ohos"`,
|
|
11
|
+
* 桌面 Linux 是 `gnu`,两者才真正互斥(`docs/` 里的条件编译文档「目标平台和条件编译映射表」)。
|
|
12
|
+
*
|
|
13
|
+
* ## 为什么 ArkUI 组件不在这里
|
|
14
|
+
*
|
|
15
|
+
* 框架核心包一旦 import `kit.ArkUI` / `ohos.arkui.*`,**所有**消费方(含桌面构建)都要跟着
|
|
16
|
+
* 拉 ArkUI 依赖。所以本文件只做「与 UI 无关的那半边」:页面来源定稿、脚本队列、IPC 接线、
|
|
17
|
+
* 生命周期标志;建 `Web` 组件的那半边在应用侧(`ArkWebView` 样板),经 [ArkWebRuntime] 取用。
|
|
18
|
+
*
|
|
19
|
+
* ## 线程模型(与桌面两平台的根本差异)
|
|
20
|
+
*
|
|
21
|
+
* 桌面:`start()` 里阻塞着跑宿主事件循环,`waitForExit()` 等它退出。
|
|
22
|
+
* 鸿蒙:ArkUI 的事件循环归系统所有(UI 线程),`start()` **不能**阻塞——它只登记页面来源,
|
|
23
|
+
* 由 UI 线程上的 `Web` 组件在 build 时取走。于是 `TauriApp.run()` 必须跑在非 UI 线程
|
|
24
|
+
* (应用在 Ability 里 `spawn`),UI 线程负责建树。
|
|
25
|
+
*
|
|
26
|
+
* 跨线程只走两条路:① 本文件的队列(`Mutex` 保护)② 页面消息经 `javaScriptProxy`
|
|
27
|
+
* 同步进来。**不要在应用线程直接调 ArkWeb/ArkUI**——与桌面 Linux 侧
|
|
28
|
+
* 「GTK/WebKit 调用必须在原生 pthread 内」同源的约束。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
@When[env == "ohos"]
|
|
32
|
+
import std.collection.*
|
|
33
|
+
|
|
34
|
+
@When[env == "ohos"]
|
|
35
|
+
import std.fs.File
|
|
36
|
+
|
|
37
|
+
@When[env == "ohos"]
|
|
38
|
+
import std.sync.*
|
|
39
|
+
|
|
40
|
+
@When[env == "ohos"]
|
|
41
|
+
import stdx.encoding.json.JsonArray
|
|
42
|
+
|
|
43
|
+
@When[env == "ohos"]
|
|
44
|
+
import stdx.encoding.json.JsonString
|
|
45
|
+
|
|
46
|
+
// 页面来源契约(load(source) 收敛,RFC §4.3):Asset 由胶水注入的 rawfile 解析器材化
|
|
47
|
+
@When[env == "ohos"]
|
|
48
|
+
import cjTauri.platform.*
|
|
49
|
+
|
|
50
|
+
// chdir:musl libc 的 C 函数(与 serial_ohos.cj 的 FFI 同模式;@When 门禁保桌面零影响)。
|
|
51
|
+
// foreign func 是包级符号——必须放文件顶层,嵌进类体会报「member function can not be foreign」。
|
|
52
|
+
@When[env == "ohos"]
|
|
53
|
+
@C
|
|
54
|
+
foreign func chdir(path: CString): Int32
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 鸿蒙宿主的进程级运行时:沙箱目录、各窗口宿主、跨线程队列开关
|
|
58
|
+
*
|
|
59
|
+
* 应用要做两件事(都是「框架拿不到、只有 Ability 有」的东西):
|
|
60
|
+
* 1. 在 `onCreate` 里 `ArkWebRuntime.configure(context.filesDir)`——`run(html)` 没有
|
|
61
|
+
* `loadData` 可用(ArkWeb 的仓颉绑定没有这个 API),内联 HTML 只能写进沙箱再以 `file://` 加载;
|
|
62
|
+
* 2. 在 `onWindowStageCreate` 里 `windowStage.loadContent("EntryView")`,并在**非 UI 线程**
|
|
63
|
+
* `spawn { app.run(html) }`——UI 线程要让给 ArkUI 建树。
|
|
64
|
+
*/
|
|
65
|
+
@When[env == "ohos"]
|
|
66
|
+
public class ArkWebRuntime {
|
|
67
|
+
/* 应用沙箱目录(filesDir 下的子目录由本类自己创建)。空串 = 尚未 configure。 */
|
|
68
|
+
static var sandboxDir: String = ""
|
|
69
|
+
|
|
70
|
+
/* label → 宿主。UI 组件按 label 取自己的那一份;多窗口下不许共用一个实例。 */
|
|
71
|
+
static let hosts: HashMap<String, ArkWebHost> = HashMap<String, ArkWebHost>()
|
|
72
|
+
|
|
73
|
+
/* 应用级退出标志:鸿蒙的「关窗」等于「退出 Ability」,只能由应用侧执行 terminateSelf。 */
|
|
74
|
+
static var quitRequested: Bool = false
|
|
75
|
+
|
|
76
|
+
/* 队列锁:worker 线程写、UI 线程读(见文件头的线程模型)。 */
|
|
77
|
+
static let queueLock: Mutex = Mutex()
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 配置沙箱目录(必须在 `TauriApp.run()` 之前调用,且只调一次)
|
|
81
|
+
*
|
|
82
|
+
* 用 `context.filesDir`:鸿蒙上它是应用私有可写目录,进程重启后内容仍在,
|
|
83
|
+
* 于是页面文件可以在**每次启动**重建(内容由应用决定,不做持久化依赖)。
|
|
84
|
+
*
|
|
85
|
+
* **cwd 同时切进沙箱**:鸿蒙上进程 cwd 指向不可写位置(实测 errno 13),而应用唯一
|
|
86
|
+
* 免权限可写的就是沙箱目录。相对路径(业务的工作区、store 插件的落盘、scope 的
|
|
87
|
+
* `$CWD` 变量)全部以 cwd 为基准——不切的话,所有按相对路径落盘的插件在设备上
|
|
88
|
+
* 全部不可用。这等价于 Tauri 移动端的做法:路径基准必须是 appDataDir,不是进程 cwd。
|
|
89
|
+
*/
|
|
90
|
+
public static func configure(filesDir: String): Unit {
|
|
91
|
+
if (filesDir.size == 0) {
|
|
92
|
+
eprintln("[cj-tauri] ArkWebRuntime.configure: filesDir is empty, run(html) will be rejected")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
ArkWebRuntime.sandboxDir = filesDir
|
|
96
|
+
unsafe {
|
|
97
|
+
let cpath = LibC.mallocCString(filesDir)
|
|
98
|
+
let r = chdir(cpath)
|
|
99
|
+
LibC.free(cpath)
|
|
100
|
+
if (r != 0) {
|
|
101
|
+
eprintln("[cj-tauri] chdir(${filesDir}) failed: errno=${r}(相对路径落盘仍以原 cwd 为基准)")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
eprintln("[cj-tauri] ArkWeb sandbox: ${filesDir}")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 登记一个宿主(构造时调;同一 label 重复登记=替换,与进程级唯一窗口的语义一致) */
|
|
108
|
+
static func register(host: ArkWebHost): Unit {
|
|
109
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
110
|
+
ArkWebRuntime.hosts[host.label()] = host
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 注销(dispose 时调;对应的 UI 组件随后会被系统回收) */
|
|
115
|
+
static func unregister(label: String): Unit {
|
|
116
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
117
|
+
ArkWebRuntime.hosts.remove(label)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 按 label 取宿主:**UI 侧(`ArkWebView` 组件)唯一的取用入口**
|
|
123
|
+
*
|
|
124
|
+
* 返回 None 是正常状态(应用先建树、`app.run()` 稍后才登记宿主),组件据此显示空白,
|
|
125
|
+
* 下一轮轮询再取——不要在这里报错。
|
|
126
|
+
*/
|
|
127
|
+
public static func hostFor(label: String): Option<ArkWebHost> {
|
|
128
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
129
|
+
match (ArkWebRuntime.hosts.get(label)) {
|
|
130
|
+
case Some(h) => return Some(h)
|
|
131
|
+
case None => return None
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 应用级退出请求(`quit()` / 窗口销毁都置它;Ability 侧轮询后自己 `terminateSelf()`) */
|
|
137
|
+
public static func requestQuit(): Unit {
|
|
138
|
+
ArkWebRuntime.quitRequested = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 是否已请求退出(应用在 Ability 里轮询它来决定要不要 terminateSelf) */
|
|
142
|
+
public static func shouldQuit(): Bool {
|
|
143
|
+
return ArkWebRuntime.quitRequested
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 一条待应用侧呈现的原生消息框(`dialog:message` 命令的载荷)
|
|
149
|
+
*
|
|
150
|
+
* 为什么要过一道队列:ArkUI 的弹窗 `getUIContext().showAlertDialog(...)` 只能在**应用侧组件**
|
|
151
|
+
* 里、且在 UI 线程上调(框架不碰 ArkUI,见文件头),而命令 handler 跑在 worker 线程上。
|
|
152
|
+
* 于是框架只描述「弹什么」,呈现与点击结果交给 UI 侧回填。
|
|
153
|
+
*
|
|
154
|
+
* `kind` 原样透传(`dialog` 插件的语义:`info` / `warning` / `error` 只有一个「确定」,
|
|
155
|
+
* `confirm` 才有取消)。按钮文案与布局属于呈现,由应用侧决定,框架不猜。
|
|
156
|
+
*/
|
|
157
|
+
@When[env == "ohos"]
|
|
158
|
+
public class ArkWebDialogRequest {
|
|
159
|
+
public let id: Int64
|
|
160
|
+
public let kind: String
|
|
161
|
+
public let title: String
|
|
162
|
+
public let message: String
|
|
163
|
+
|
|
164
|
+
public init(id: Int64, kind: String, title: String, message: String) {
|
|
165
|
+
this.id = id
|
|
166
|
+
this.kind = kind
|
|
167
|
+
this.title = title
|
|
168
|
+
this.message = message
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 是否「确定 / 取消」两键(`info` / `warning` / `error` 只有一个确定键,弹出即成立)
|
|
173
|
+
*
|
|
174
|
+
* 判定本身在 `src/dialog_spec.cj`(三平台共用一份):这里原先写死 `kind == "confirm"`,
|
|
175
|
+
* 而这个文件带平台门禁、桌面编不到,同一个 kind 在两个平台各判一次就迟早判岔。
|
|
176
|
+
*/
|
|
177
|
+
public func isConfirm(): Bool {
|
|
178
|
+
return dialogShapeIsTwoKey(dialogShapeOf(this.kind))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `showMessageDialog` 等 UI 侧回填的上限(毫秒)
|
|
184
|
+
*
|
|
185
|
+
* 桌面两平台在这里真的阻塞到用户点按钮,鸿蒙上只能等 UI 线程来取。窗口没建、页面没跑
|
|
186
|
+
* 的情况下无限期钉住一个 worker 线程比超时更糟,所以给一个宽上限:超时就如实返回 false
|
|
187
|
+
* 并在 stderr 里说清是「没弹出来」而不是「用户取消」——两者对调用方都是 false,但日志分得清。
|
|
188
|
+
*/
|
|
189
|
+
@When[env == "ohos"]
|
|
190
|
+
let CJ_ARKWEB_DIALOG_WAIT_MS: Int64 = 120000
|
|
191
|
+
|
|
192
|
+
/** 等回填的轮询间隔(毫秒):只影响「点了按钮 → 命令返回」的延迟,与页面心跳无关 */
|
|
193
|
+
@When[env == "ohos"]
|
|
194
|
+
let CJ_ARKWEB_DIALOG_POLL_MS: Int64 = 20
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 一个鸿蒙窗口的宿主
|
|
198
|
+
*
|
|
199
|
+
* 与桌面宿主的分工差异:这里**所有**图形相关动作都只是「入队 / 记状态」,真正执行发生在
|
|
200
|
+
* UI 线程(`ArkWebView` 组件的 build 与定时器里)。这样框架侧不需要任何 ArkUI 符号,
|
|
201
|
+
* 也不会在 worker 线程上碰 ArkUI(那会 abort,见 AGENTS §4 的栈边界坑)。
|
|
202
|
+
*/
|
|
203
|
+
@When[env == "ohos"]
|
|
204
|
+
public class ArkWebHost <: WebViewHost {
|
|
205
|
+
private let windowLabel: String
|
|
206
|
+
// 资产解析器:胶水在 load 前注入(rawfile 材化);鸿蒙没有 cwd 读盘兜底
|
|
207
|
+
private var assetResolver: Option<AssetResolver> = None
|
|
208
|
+
|
|
209
|
+
/* 页面来源:start(html) 先存 html、落盘后 pageUrl 才成立;startUrl 直接定 pageUrl。 */
|
|
210
|
+
private var pageHtml: String = ""
|
|
211
|
+
private var pageUrl: String = ""
|
|
212
|
+
|
|
213
|
+
/* document-start 预执行脚本(插件 shim 与 label 注入走这条通道,顺序敏感)。 */
|
|
214
|
+
private var initScripts: ArrayList<String> = ArrayList<String>()
|
|
215
|
+
|
|
216
|
+
/* 待 UI 线程执行的脚本(runJs / 注入 / 回投都进这里,见 takePendingScripts)。 */
|
|
217
|
+
private var pendingScripts: ArrayList<String> = ArrayList<String>()
|
|
218
|
+
|
|
219
|
+
private var devToolsEnabled: Bool = false
|
|
220
|
+
private var visible: Bool = true
|
|
221
|
+
private var disposed: Bool = false
|
|
222
|
+
private var quitFlag: Bool = false
|
|
223
|
+
|
|
224
|
+
/* 取证计数:页面开始 / 结束加载各几次,桥日志按它对账(AGENTS §1 门禁 2)。 */
|
|
225
|
+
private var pageBeginCount: Int64 = 0
|
|
226
|
+
private var pageEndCount: Int64 = 0
|
|
227
|
+
private var jsInCount: Int64 = 0
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* 原生对话框(showMessageDialog)的三个槽,全部受 ArkWebRuntime.queueLock 保护:
|
|
231
|
+
* worker 线程入队并等待、UI 线程取走并回填,跨线程只经这两个动作交接。
|
|
232
|
+
* id 单调自增(每窗一份),所以「晚到的答案」撞不上新请求。
|
|
233
|
+
*/
|
|
234
|
+
private var dialogSeq: Int64 = 0
|
|
235
|
+
private let pendingDialogs: ArrayList<ArkWebDialogRequest> = ArrayList<ArkWebDialogRequest>()
|
|
236
|
+
private let awaitingDialogs: HashSet<Int64> = HashSet<Int64>()
|
|
237
|
+
private let dialogAnswers: HashMap<Int64, Bool> = HashMap<Int64, Bool>()
|
|
238
|
+
private var dialogRequestCount: Int64 = 0
|
|
239
|
+
private var dialogAnswerCount: Int64 = 0
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
* 菜单模型快照:worker 线程写(setMenu / setMenuItemState)、UI 线程读(menuModel),
|
|
243
|
+
* 与上面三个对话框槽同受 queueLock 保护。ArkUI 那半边(bindMenu)在应用侧,
|
|
244
|
+
* 框架这边只存模型——理由见 arkweb_menu.cj 的类注释。
|
|
245
|
+
*/
|
|
246
|
+
private var menuRoot: ArrayList<ArkWebMenuItem> = ArrayList<ArkWebMenuItem>()
|
|
247
|
+
private var menuItemCount: Int64 = 0
|
|
248
|
+
private var menuClickCount: Int64 = 0
|
|
249
|
+
|
|
250
|
+
private var messageHandler: (String) -> Unit = { _ => }
|
|
251
|
+
private var shellHandler: (String) -> Unit = { _ => }
|
|
252
|
+
private var destroyHandler: () -> Unit = { => }
|
|
253
|
+
|
|
254
|
+
public init(label: String) {
|
|
255
|
+
this.windowLabel = label
|
|
256
|
+
ArkWebRuntime.register(this)
|
|
257
|
+
eprintln("[cj-tauri] ArkWeb host created: label=\"${label}\"")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public func label(): String {
|
|
261
|
+
return this.windowLabel
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 登记内联 HTML 页面(不阻塞:真正的组件由 UI 线程建)
|
|
266
|
+
*
|
|
267
|
+
* 与桌面不同,这里**没有** `loadData` 可调(ArkWeb 的仓颉绑定只给了 `loadUrl`),
|
|
268
|
+
* 所以 HTML 先落盘到沙箱、再以 `file://` 交给 `Web` 组件。副作用要如实说:页面有了
|
|
269
|
+
* 真实来源(`location.protocol == "file:"`),`fetch` / XHR 会被同源策略挡住——
|
|
270
|
+
* 与 Tauri 2 在 `run(html)` 下的表现一致,不是本实现独有的限制。
|
|
271
|
+
*/
|
|
272
|
+
public func start(html: String): Unit {
|
|
273
|
+
this.pageHtml = html
|
|
274
|
+
this.pageUrl = this.writeHtmlToSandbox(html)
|
|
275
|
+
eprintln("[cj-tauri] ArkWeb page: label=\"${this.windowLabel}\" source=inline url=\"${this.pageUrl}\"")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 登记 URL 页面(http(s):// 或 file://)
|
|
280
|
+
*
|
|
281
|
+
* 这条路径有真实来源,相对资源 / 动态 import / 前端路由都成立;`CJ_TAURI_DEV_URL`
|
|
282
|
+
* 的 dev server 就靠它。注入时机见 `injectInitScriptsOnBegin` 的说明。
|
|
283
|
+
*/
|
|
284
|
+
public func startUrl(url: String): Unit {
|
|
285
|
+
this.pageHtml = ""
|
|
286
|
+
this.pageUrl = url
|
|
287
|
+
eprintln("[cj-tauri] ArkWeb page: label=\"${this.windowLabel}\" source=url url=\"${url}\"")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 加载页面(唯一入口,RFC §4.3)
|
|
292
|
+
*
|
|
293
|
+
* 与桌面的差异:Asset **没有 cwd 读盘兜底**——hap 里没有工程目录,资产只能来自
|
|
294
|
+
* 胶水注入的 resolver(rawfile 材化)。缺注入或资产缺失时如实报错,不静默白屏。
|
|
295
|
+
*/
|
|
296
|
+
public func load(source: PageSource): Unit {
|
|
297
|
+
match (source) {
|
|
298
|
+
case Inline(html) => this.start(html)
|
|
299
|
+
case Url(url) => this.startUrl(url)
|
|
300
|
+
case DevServer(url) => this.startUrl(url)
|
|
301
|
+
case Asset(name) =>
|
|
302
|
+
match (this.assetResolver) {
|
|
303
|
+
case Some(r) =>
|
|
304
|
+
match (r.readText(name)) {
|
|
305
|
+
case Some(html) => this.start(html)
|
|
306
|
+
case None =>
|
|
307
|
+
throw AssetResolutionException("资产未找到:${name}(胶水注入的 rawfile 里没有这个资产,检查清单 [assets] 与骨架 rawfile 拷贝)")
|
|
308
|
+
}
|
|
309
|
+
case None =>
|
|
310
|
+
throw AssetResolutionException("资产 ${name} 未注入 AssetResolver(鸿蒙侧胶水须先把 rawfile 材化并注入解析器,再 load)")
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 注入资产解析器(胶水在 load 前调用,rawfile 材化器) */
|
|
316
|
+
public func setAssetResolver(resolver: Option<AssetResolver>): Unit {
|
|
317
|
+
this.assetResolver = resolver
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 注册预执行脚本(必须早于 load,与桌面同一契约)
|
|
322
|
+
*
|
|
323
|
+
* 内联 HTML 走「拼进 head 最前」这条通道(真正早于页面脚本);URL 页面只能等
|
|
324
|
+
* `onPageBegin` 再 runJavaScript——那时页面顶层脚本可能已经跑过,**这是本平台的已知限制**,
|
|
325
|
+
* 不是静默降级:首次注入会在 stderr 说明一次(见 `noteLateInjection`)。
|
|
326
|
+
*/
|
|
327
|
+
public func addInitScript(js: String): Unit {
|
|
328
|
+
if (js.size == 0) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
this.initScripts.add(js)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public func reload(): Unit {
|
|
335
|
+
this.enqueueScript("__cjTauriReloadRequest()", true)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public func setWindowConfig(config: WindowConfig): Unit {
|
|
339
|
+
// 鸿蒙应用窗口由 Ability 的 WindowStage 决定,逻辑尺寸概念与桌面不同:
|
|
340
|
+
// 这里如实说明不生效,而不是假装设成功(能力位也不含任何窗口控制位)。
|
|
341
|
+
let _ = config
|
|
342
|
+
eprintln("[cj-tauri] ArkWeb: window config is managed by WindowStage on this platform (ignored)")
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
public func setDevTools(enabled: Bool): Unit {
|
|
346
|
+
this.devToolsEnabled = enabled
|
|
347
|
+
// 真正调 setWebDebuggingAccess 的时机在 UI 侧(controller 就绪之后)。
|
|
348
|
+
eprintln("[cj-tauri] ArkWeb: web debugging requested: ${enabled}")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public func openDevTools(): Unit {
|
|
352
|
+
// ArkWeb 的调试走「设备侧 Web 调试」通道,没有桌面那种独立 DevTools 窗口。
|
|
353
|
+
eprintln("[cj-tauri] ArkWeb: openDevTools is not available on this platform (use web debugging)")
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ---- 菜单:框架存模型,应用侧用 bindMenu 画(见 arkweb_menu.cj 的类注释) ---- */
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 收下应用给的菜单线路格式(`MenuModel.toWire()`),解析成快照存起来。
|
|
360
|
+
*
|
|
361
|
+
* **不阻塞、也不画界面**:真正的菜单由应用侧的 `ArkWebView` 在 `bindMenu` 里画,
|
|
362
|
+
* UI 线程经 [menuModel] 取快照。这样框架包不引用 ArkUI(AGENTS §4 的 SIGSEGV 坑)。
|
|
363
|
+
*/
|
|
364
|
+
public func setMenu(wire: String): Unit {
|
|
365
|
+
let parsed = parseArkWebMenuWire(wire)
|
|
366
|
+
var rows: Int64 = 0
|
|
367
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
368
|
+
this.menuRoot = parsed
|
|
369
|
+
rows = countMenuItems(parsed)
|
|
370
|
+
this.menuItemCount = rows
|
|
371
|
+
}
|
|
372
|
+
eprintln("[cj-tauri] ArkWeb: menu applied (${wire.size} bytes, ${rows} item(s), app-side bindMenu)")
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 运行期改单项状态(按 id 找,找不到静默忽略——与桌面两平台同口径)。
|
|
377
|
+
*
|
|
378
|
+
* 这里**不再抛异常**:菜单模型住在本进程,改动是纯内存动作;UI 下一轮 [menuModel]
|
|
379
|
+
* 拿到的就是新状态(`bindMenu` 每次 build 重新生成,不需要额外通知)。
|
|
380
|
+
*/
|
|
381
|
+
public func setMenuItemState(id: String, enabled: Bool, checked: Bool): Unit {
|
|
382
|
+
var hit = false
|
|
383
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
384
|
+
for (item in this.menuRoot) {
|
|
385
|
+
match (item.find(id)) {
|
|
386
|
+
case Some(node) =>
|
|
387
|
+
node.enabled = enabled
|
|
388
|
+
node.checked = checked
|
|
389
|
+
hit = true
|
|
390
|
+
case None => ()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
eprintln("[cj-tauri] ArkWeb: menu item state id=\"${id}\" enabled=${enabled} checked=${checked} hit=${hit}")
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* UI 线程取菜单快照(每次一份深拷贝,UI 改它不会动到宿主里的模型)。
|
|
399
|
+
*
|
|
400
|
+
* 应用侧的 `ArkWebView` 在 `build()` 里按它生成 `bindMenu` 的 `MenuElement` 数组;
|
|
401
|
+
* 没有菜单时回空表(应用照常建树,别等菜单才画 Web)。
|
|
402
|
+
*/
|
|
403
|
+
public func menuModel(): ArrayList<ArkWebMenuItem> {
|
|
404
|
+
let out = ArrayList<ArkWebMenuItem>()
|
|
405
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
406
|
+
for (item in this.menuRoot) {
|
|
407
|
+
out.add(item.snapshot())
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 应用侧 UI 里「这一项被点了」→ 回投 shell 事件。
|
|
415
|
+
*
|
|
416
|
+
* 报文与桌面 C 桥的 `cj_core_menu_clicked` **逐字同形**
|
|
417
|
+
* (`{"kind":"menu","id":…,"enabled":…,"checked":…}`),于是应用层的 `onShellEvent`、
|
|
418
|
+
* 前端 `menu:click`、以及应用按事件同步自己模型的代码都不用分平台。
|
|
419
|
+
*
|
|
420
|
+
* 勾选项的翻转由本函数做(与 Windows 侧「Win32 不替我们翻,自己翻」同口径):
|
|
421
|
+
* 报出去的是**翻转后**的状态,页面照它更新即可。
|
|
422
|
+
*/
|
|
423
|
+
public func noteMenuClick(id: String): Unit {
|
|
424
|
+
var enabled = true
|
|
425
|
+
var checked = false
|
|
426
|
+
var found = false
|
|
427
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
428
|
+
for (item in this.menuRoot) {
|
|
429
|
+
match (item.find(id)) {
|
|
430
|
+
case Some(node) =>
|
|
431
|
+
if (node.isCheckbox()) {
|
|
432
|
+
node.checked = !node.checked
|
|
433
|
+
}
|
|
434
|
+
enabled = node.enabled
|
|
435
|
+
checked = node.checked
|
|
436
|
+
found = true
|
|
437
|
+
case None => ()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!found) {
|
|
442
|
+
eprintln("[cj-tauri] ArkWeb: menu click ignored (id=\"${id}\" is not in the current menu)")
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
this.menuClickCount++
|
|
446
|
+
eprintln("[cj-tauri] ArkWeb: menu clicked id=\"${id}\" enabled=${enabled} checked=${checked}")
|
|
447
|
+
this.shellHandler("{\"kind\":\"menu\",\"id\":\"${arkWebJsonEscape(id)}\",\"enabled\":${enabled}," +
|
|
448
|
+
"\"checked\":${checked}}")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
public func setTitlebarSubtitle(text: String): Unit {
|
|
452
|
+
let _ = text
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
public func setDragDropPolicy(enabled: Bool, files: Bool): Unit {
|
|
456
|
+
let _ = enabled
|
|
457
|
+
let _ = files
|
|
458
|
+
eprintln("[cj-tauri] ArkWeb: drag/drop policy unsupported (host capability bit is 0)")
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
public func setGlobalHotkeys(wire: String): Unit {
|
|
462
|
+
let _ = wire
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
public func setTray(wire: String): Unit {
|
|
466
|
+
let _ = wire
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public func setWindowVisible(visible: Bool): Unit {
|
|
470
|
+
this.visible = visible
|
|
471
|
+
// 可见性由 ArkUI 的组件树决定(`visibility` 属性),宿主只记状态供 UI 侧读取。
|
|
472
|
+
eprintln("[cj-tauri] ArkWeb: window visible -> ${visible}")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
public func setMinimizeToTray(enabled: Bool): Unit {
|
|
476
|
+
let _ = enabled
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** 能力位:菜单已置 1(2026-10-24 真机取证),其余仍为 0(AGENTS §1 门禁 4) */
|
|
480
|
+
public func hostCapabilities(): Int32 {
|
|
481
|
+
/*
|
|
482
|
+
* 菜单是鸿蒙侧第一项真落地的宿主能力:框架存模型(arkweb_menu.cj)、应用侧 bindMenu 画、
|
|
483
|
+
* 点击经 noteMenuClick 回投(报文与桌面同形)。其余位仍为 0——判据是「仓颉侧有没有 API」:
|
|
484
|
+
* 托盘(鸿蒙无系统托盘)、全局快捷键(只有组件级 keyboardshortcut)、拖入文件(SDK 只有
|
|
485
|
+
* 拖出侧的 drag / dragPreview,没有 drop 回调)、通知(SDK 无 notification_manager 模块)、
|
|
486
|
+
* 标题栏副标题(无 setWindowTitle)。AGENTS §1 门禁 4:没有实机证据的位置 1。
|
|
487
|
+
*/
|
|
488
|
+
return CJ_CAP_MENU
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
public func setShellHandler(handler: (String) -> Unit): Unit {
|
|
492
|
+
this.shellHandler = handler
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
public func setDestroyHandler(handler: () -> Unit): Unit {
|
|
496
|
+
this.destroyHandler = handler
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
public func showFileDialog(save: Bool, title: String, filter: String): String {
|
|
500
|
+
let _ = save
|
|
501
|
+
let _ = title
|
|
502
|
+
let _ = filter
|
|
503
|
+
eprintln("[cj-tauri] ArkWeb: native file dialog unsupported (needs a picker module)")
|
|
504
|
+
return ""
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
public func showFileDialogPreset(save: Bool, title: String, filter: String, defaultName: String): String {
|
|
508
|
+
let _ = defaultName
|
|
509
|
+
return this.showFileDialog(save, title, filter)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public func showFolderDialog(title: String): String {
|
|
513
|
+
let _ = title
|
|
514
|
+
eprintln("[cj-tauri] ArkWeb: native folder dialog unsupported (needs a picker module)")
|
|
515
|
+
return ""
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 原生消息框(`dialog:message` 命令的落点)
|
|
520
|
+
*
|
|
521
|
+
* 与桌面的差异:桌面在 C 桥里**同步阻塞**到用户点按钮;鸿蒙的弹窗只有 ArkUI 侧能做,
|
|
522
|
+
* 所以这里退成「入队 → 有界轮询等 UI 侧回填」。等待发生在命令 worker 线程上
|
|
523
|
+
* (命令分发本来就是异步的,见 AGENTS §2),不占 UI 线程,窗口照常响应。
|
|
524
|
+
*
|
|
525
|
+
* 返回 false 的三种情况要在日志里分得清:宿主已销毁(拒绝)、UI 侧没来取(超时)、
|
|
526
|
+
* 用户点了取消(answered + confirmed=false)。
|
|
527
|
+
*/
|
|
528
|
+
public func showMessageDialog(kind: String, title: String, message: String): Bool {
|
|
529
|
+
if (this.disposed) {
|
|
530
|
+
eprintln("[cj-tauri] ArkWeb dialog rejected: label=\"${this.windowLabel}\" (host disposed)")
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
var id: Int64 = 0
|
|
534
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
535
|
+
this.dialogSeq += 1
|
|
536
|
+
id = this.dialogSeq
|
|
537
|
+
this.dialogRequestCount += 1
|
|
538
|
+
this.pendingDialogs.add(ArkWebDialogRequest(id, kind, title, message))
|
|
539
|
+
this.awaitingDialogs.add(id)
|
|
540
|
+
}
|
|
541
|
+
eprintln(
|
|
542
|
+
"[cj-tauri] ArkWeb dialog queued: label=\"${this.windowLabel}\" id=${id} kind=\"${kind}\" title=\"${title}\""
|
|
543
|
+
)
|
|
544
|
+
var waited: Int64 = 0
|
|
545
|
+
while (waited < CJ_ARKWEB_DIALOG_WAIT_MS) {
|
|
546
|
+
var answered = false
|
|
547
|
+
var confirmed = false
|
|
548
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
549
|
+
if (this.dialogAnswers.contains(id)) {
|
|
550
|
+
confirmed = this.dialogAnswers[id]
|
|
551
|
+
this.dialogAnswers.remove(id)
|
|
552
|
+
answered = true
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (answered) {
|
|
556
|
+
eprintln(
|
|
557
|
+
"[cj-tauri] ArkWeb dialog answered: label=\"${this.windowLabel}\" id=${id} confirmed=${confirmed}"
|
|
558
|
+
)
|
|
559
|
+
return confirmed
|
|
560
|
+
}
|
|
561
|
+
sleep(Duration.millisecond * CJ_ARKWEB_DIALOG_POLL_MS)
|
|
562
|
+
waited += CJ_ARKWEB_DIALOG_POLL_MS
|
|
563
|
+
}
|
|
564
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
565
|
+
this.awaitingDialogs.remove(id)
|
|
566
|
+
this.dropPendingDialog(id)
|
|
567
|
+
this.dialogAnswers.remove(id)
|
|
568
|
+
}
|
|
569
|
+
eprintln(
|
|
570
|
+
"[cj-tauri] ArkWeb dialog timed out: label=\"${this.windowLabel}\" id=${id} after ${CJ_ARKWEB_DIALOG_WAIT_MS}ms (UI side never answered; this is NOT a user cancel)"
|
|
571
|
+
)
|
|
572
|
+
return false
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 取一条待呈现的对话框(**只在 UI 线程调**:`ArkWebView` 组件的页面心跳回调里)
|
|
577
|
+
*
|
|
578
|
+
* 取走即从队列移除,所以同一个请求不会弹两次;返回 None 是绝大多数心跳的正常结果。
|
|
579
|
+
*/
|
|
580
|
+
public func takePendingDialog(): Option<ArkWebDialogRequest> {
|
|
581
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
582
|
+
if (this.pendingDialogs.isEmpty()) {
|
|
583
|
+
return None
|
|
584
|
+
}
|
|
585
|
+
let request = this.pendingDialogs[0]
|
|
586
|
+
this.pendingDialogs.remove(at: 0)
|
|
587
|
+
return Some(request)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 回填用户的选择(**只在 UI 线程调**:弹窗按钮的回调里)
|
|
593
|
+
*
|
|
594
|
+
* 只接受仍在等待的 id:超时之后等待方已经放弃,这时再记一个答案会把它一直挂在这个槽里
|
|
595
|
+
* (没人取走),所以按 `awaitingDialogs` 校验一次,并在日志里说明被丢弃的那次点击。
|
|
596
|
+
*/
|
|
597
|
+
public func answerDialog(id: Int64, confirmed: Bool): Unit {
|
|
598
|
+
var accepted = false
|
|
599
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
600
|
+
if (this.awaitingDialogs.contains(id)) {
|
|
601
|
+
this.awaitingDialogs.remove(id)
|
|
602
|
+
this.dialogAnswers[id] = confirmed
|
|
603
|
+
this.dialogAnswerCount += 1
|
|
604
|
+
accepted = true
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (accepted) {
|
|
608
|
+
eprintln(
|
|
609
|
+
"[cj-tauri] ArkWeb dialog answer: label=\"${this.windowLabel}\" id=${id} confirmed=${confirmed}"
|
|
610
|
+
)
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
eprintln(
|
|
614
|
+
"[cj-tauri] ArkWeb dialog answer dropped: label=\"${this.windowLabel}\" id=${id} confirmed=${confirmed} (no longer awaiting)"
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** 从待呈现队列里按 id 移除(超时清场用;调用方必须已持有 `queueLock`) */
|
|
619
|
+
private func dropPendingDialog(id: Int64): Unit {
|
|
620
|
+
var index: Int64 = 0
|
|
621
|
+
while (index < this.pendingDialogs.size) {
|
|
622
|
+
if (this.pendingDialogs[index].id == id) {
|
|
623
|
+
this.pendingDialogs.remove(at: index)
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
index += 1
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 写文本进系统剪贴板(鸿蒙 = 原生 pasteboard,见 `clipboard_bridge.cj` 的鸿蒙段)
|
|
632
|
+
*
|
|
633
|
+
* 返回 false 只有三种可能:句柄拿不到、UDMF 对象建不出来、平台拒绝这次写入。
|
|
634
|
+
* **不要把 false 读成「暂时没接」**——这条能力在鸿蒙上已经接了,false 就是真失败。
|
|
635
|
+
*/
|
|
636
|
+
public func clipboardWriteText(text: String): Bool {
|
|
637
|
+
return arkwebClipboardWriteText(text)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* 读系统剪贴板文本:None = 空 / 非文本 / 平台拒绝(含缺 `READ_PASTEBOARD` 权限)
|
|
642
|
+
*
|
|
643
|
+
* 三种 None 对调用方是同一个「没拿到」——接口契约就是 Option,不在这儿编造区分。
|
|
644
|
+
*/
|
|
645
|
+
public func clipboardReadText(): Option<String> {
|
|
646
|
+
return arkwebClipboardReadText()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
public func notify(title: String, body: String): Bool {
|
|
650
|
+
let _ = title
|
|
651
|
+
let _ = body
|
|
652
|
+
eprintln("[cj-tauri] ArkWeb: notification unsupported (host capability bit is 0)")
|
|
653
|
+
return false
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 在页面里执行一段脚本(框架的 `resolve` / `event` 回投就走这条)
|
|
658
|
+
*
|
|
659
|
+
* 不直接调 `controller.runJavaScript`:那个 API 属于 UI 线程,而本方法几乎总是从
|
|
660
|
+
* IPC worker 线程进来(命令异步分发,见 AGENTS §2)。所以只入队,由 UI 侧的定时器取走。
|
|
661
|
+
* 这样「回投顺序」也自然跟着队列走。
|
|
662
|
+
*/
|
|
663
|
+
public func runJs(script: String): Unit {
|
|
664
|
+
if (script.size == 0 || this.disposed) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
this.enqueueScript(script, true)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* 请求退出
|
|
672
|
+
*
|
|
673
|
+
* 鸿蒙没有「关掉这个窗口」这种宿主动作:窗口归 Ability,退出得由应用 `terminateSelf()`。
|
|
674
|
+
* 所以这里只置标志(`shouldQuit()` 与 `ArkWebRuntime.shouldQuit()` 都能读到),
|
|
675
|
+
* 应用据此结束自己的生命周期;框架不假装自己关掉了窗口。
|
|
676
|
+
*/
|
|
677
|
+
public func quit(): Unit {
|
|
678
|
+
if (this.quitFlag) {
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
this.quitFlag = true
|
|
682
|
+
ArkWebRuntime.requestQuit()
|
|
683
|
+
eprintln("[cj-tauri] ArkWeb: quit requested: label=\"${this.windowLabel}\" (app should terminateSelf)")
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
public func setMessageHandler(handler: (String) -> Unit): Unit {
|
|
687
|
+
this.messageHandler = handler
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
public func shouldQuit(): Bool {
|
|
691
|
+
return this.quitFlag || ArkWebRuntime.shouldQuit()
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public func dispose(): Unit {
|
|
695
|
+
if (this.disposed) {
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
this.disposed = true
|
|
699
|
+
ArkWebRuntime.unregister(this.windowLabel)
|
|
700
|
+
eprintln(
|
|
701
|
+
"[cj-tauri] ArkWeb host disposed: label=\"${this.windowLabel}\" pageBegin=${this.pageBeginCount} pageEnd=${this.pageEndCount} jsIn=${this.jsInCount} dialogRequested=${this.dialogRequestCount} dialogAnswered=${this.dialogAnswerCount}"
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* ------------------------- UI 侧(ArkWebView 组件)取用面 ------------------------- */
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 组件要加载的地址(空串 = 还没登记,组件应保持空白并下一轮再问)
|
|
709
|
+
*
|
|
710
|
+
* 为什么可能为空:应用先建 UI 树、再 `spawn { app.run(html) }`,两者先后不定。
|
|
711
|
+
* 组件拿到空串不是错误,是正常的启动竞态,别在这里打日志刷屏。
|
|
712
|
+
*/
|
|
713
|
+
public func currentUrl(): String {
|
|
714
|
+
return this.pageUrl
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** 是否要求可见(组件据此设 `visibility`);隐藏态页面照常跑,还原入口由应用给 */
|
|
718
|
+
public func isVisible(): Bool {
|
|
719
|
+
return this.visible
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** 是否要求开启 Web 调试(组件在建 controller 后调 `setWebDebuggingAccess`) */
|
|
723
|
+
public func devToolsWanted(): Bool {
|
|
724
|
+
return this.devToolsEnabled
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* 取走全部待执行脚本(**必须在 UI 线程调用**)
|
|
729
|
+
*
|
|
730
|
+
* 取走即清空:这些脚本的语义是「尽快执行一次」,重复执行会把 resolve 投两次
|
|
731
|
+
* (页面侧 pending 表按 id 匹配,重复投递会让第二次变空操作,但语义仍应保持一次)。
|
|
732
|
+
*/
|
|
733
|
+
public func takePendingScripts(): ArrayList<String> {
|
|
734
|
+
let out = ArrayList<String>()
|
|
735
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
736
|
+
for (s in this.pendingScripts) {
|
|
737
|
+
out.add(s)
|
|
738
|
+
}
|
|
739
|
+
this.pendingScripts.clear()
|
|
740
|
+
}
|
|
741
|
+
return out
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* 取走 init 脚本(页面开始加载时调一次:内联 HTML 已拼进 head,这里的只用于 URL 页面)
|
|
746
|
+
*
|
|
747
|
+
* 不做「取走即清空」:URL 页面的 reload 后仍需要同一批 shim,清空会让第二次加载丢掉插件。
|
|
748
|
+
*/
|
|
749
|
+
public func initScriptsSnapshot(): ArrayList<String> {
|
|
750
|
+
let out = ArrayList<String>()
|
|
751
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
752
|
+
for (s in this.initScripts) {
|
|
753
|
+
out.add(s)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return out
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 页面轮询泵的原生端点(`javaScriptProxy` 的 `poll` 方法,UI 线程同步调用)
|
|
761
|
+
*
|
|
762
|
+
* 为什么是「页面拉」而不是「原生推」:ArkUI 的仓颉绑定没有定时器、也没有 UIContext,
|
|
763
|
+
* 框架线程既定不了时也不能安全碰 ArkUI(AGENTS §4 的栈边界坑);而页面里 `setInterval`
|
|
764
|
+
* 是最普通的东西。于是把「什么时候执行待发脚本」交给页面,原生只做同步的取队列动作。
|
|
765
|
+
*
|
|
766
|
+
* 返回值是**脚本字符串的 JSON 数组**(不是 JSON 报文):页面用间接 eval 逐条执行,
|
|
767
|
+
* 这样 `_dispatch({...})` 这类语句能直接跑在全局作用域,而不是变成字符串。
|
|
768
|
+
* 空队列返回 `[]`——页面侧据此连 JSON.parse 都不做。
|
|
769
|
+
*/
|
|
770
|
+
public func pollScriptsJson(): String {
|
|
771
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
772
|
+
if (this.pendingScripts.isEmpty()) {
|
|
773
|
+
return "[]"
|
|
774
|
+
}
|
|
775
|
+
let arr = JsonArray()
|
|
776
|
+
for (s in this.pendingScripts) {
|
|
777
|
+
arr.add(JsonString(s))
|
|
778
|
+
}
|
|
779
|
+
this.pendingScripts.clear()
|
|
780
|
+
return arr.toString()
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** 页面开始加载(UI 侧回调;用于注入与取证计数) */
|
|
785
|
+
public func notePageBegin(): Unit {
|
|
786
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
787
|
+
this.pageBeginCount += 1
|
|
788
|
+
}
|
|
789
|
+
eprintln("[cj-tauri] ArkWeb page begin: label=\"${this.windowLabel}\" count=${this.pageBeginCount}")
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** 页面加载结束(UI 侧回调);`system:host` 的取证就看这两个计数是否对得上 */
|
|
793
|
+
public func notePageEnd(): Unit {
|
|
794
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
795
|
+
this.pageEndCount += 1
|
|
796
|
+
}
|
|
797
|
+
eprintln("[cj-tauri] ArkWeb page end: label=\"${this.windowLabel}\" count=${this.pageEndCount}")
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* 页面 → 原生:`javaScriptProxy` 的同步回调(UI 线程)
|
|
802
|
+
*
|
|
803
|
+
* 报文格式与桌面两平台**同一份**(`{type:"invoke"|"emit", id, cmd, event, window, ...}`),
|
|
804
|
+
* 所以 IPC 层不需要为鸿蒙分叉。这里只做计数与转发,不做校验——校验在 `IpcHub`。
|
|
805
|
+
*/
|
|
806
|
+
public func acceptPageMessage(raw: String): Unit {
|
|
807
|
+
if (raw.size == 0 || this.disposed) {
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
// reload 是**控制消息、不进 IPC hub**:桌面两平台由 C 桥在投递前截掉(CJT_RELOAD_MSG),
|
|
811
|
+
// 鸿蒙没有 C 桥这一层,拦截点就在这里。用子串判断而不是解析 JSON:报文由本框架自己的
|
|
812
|
+
// 桥脚本产生,形状固定;解析失败会把 reload 静默丢掉,反而更难查。
|
|
813
|
+
match (raw.indexOf("\"type\":\"reload\"")) {
|
|
814
|
+
case Some(_) =>
|
|
815
|
+
eprintln("[cj-tauri] ArkWeb: reload requested by page: label=\"${this.windowLabel}\"")
|
|
816
|
+
this.reload()
|
|
817
|
+
return
|
|
818
|
+
case None => ()
|
|
819
|
+
}
|
|
820
|
+
// 拉取心跳同理:页面只是「叫一声」,真正的推送由 UI 侧(组件的 post 回调)用
|
|
821
|
+
// runJavaScript 完成——`javaScriptProxy` 的返回值传不回页面(踩坑文档 8.1.5.1),
|
|
822
|
+
// 所以这条报文不进 IPC hub,这里直接返回。
|
|
823
|
+
match (raw.indexOf("\"type\":\"pull\"")) {
|
|
824
|
+
case Some(_) => return
|
|
825
|
+
case None => ()
|
|
826
|
+
}
|
|
827
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
828
|
+
this.jsInCount += 1
|
|
829
|
+
}
|
|
830
|
+
this.messageHandler(raw)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** 页面消息计数(取证:`js -> native` 次数对账,AGENTS §1 门禁 2) */
|
|
834
|
+
public func messageCount(): Int64 {
|
|
835
|
+
return this.jsInCount
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/* ------------------------------ 内部辅助 ------------------------------ */
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 入队一段脚本
|
|
842
|
+
*
|
|
843
|
+
* `direct` 只是留给调用点自说明用的(两处语义相同,暂不分支);保留它是因为将来
|
|
844
|
+
* 「document-start 注入」与「普通 runJs」可能需要不同的排队策略(前者要在 onPageBegin 之前)。
|
|
845
|
+
*/
|
|
846
|
+
private func enqueueScript(script: String, direct: Bool): Unit {
|
|
847
|
+
let _ = direct
|
|
848
|
+
synchronized(ArkWebRuntime.queueLock) {
|
|
849
|
+
this.pendingScripts.add(script)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* 把内联 HTML 落盘并返回 `file://` 地址
|
|
855
|
+
*
|
|
856
|
+
* 为什么必须落盘:ArkWeb 的仓颉绑定没有 `loadData`(只有 `loadUrl`),内联字符串
|
|
857
|
+
* 无法直接喂给组件。文件名按 label 分,多窗口互不覆盖。
|
|
858
|
+
* 失败(未 configure、磁盘不可写)时返回空串——组件据此保持空白,stderr 留凭证,
|
|
859
|
+
* **不**抛异常把 `app.run()` 带走(`run()` 抛异常的表现是应用秒退、hilog 一行都没有)。
|
|
860
|
+
*/
|
|
861
|
+
private func writeHtmlToSandbox(html: String): String {
|
|
862
|
+
if (ArkWebRuntime.sandboxDir.size == 0) {
|
|
863
|
+
eprintln("[cj-tauri] ArkWeb: run(html) rejected: ArkWebRuntime.configure(filesDir) was not called")
|
|
864
|
+
return ""
|
|
865
|
+
}
|
|
866
|
+
let path = "${ArkWebRuntime.sandboxDir}/cj-tauri-${this.windowLabel}.html"
|
|
867
|
+
try {
|
|
868
|
+
File.writeTo(path, this.injectBootstrap(html).toArray())
|
|
869
|
+
} catch (e: Exception) {
|
|
870
|
+
eprintln("[cj-tauri] ArkWeb: failed to write page to sandbox: ${path}: ${e.message}")
|
|
871
|
+
return ""
|
|
872
|
+
}
|
|
873
|
+
return "file://${path}"
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* 把 label、桥脚本与 init 脚本拼进 HTML 的 head 最前
|
|
878
|
+
*
|
|
879
|
+
* 为什么走这条而不是 `onPageBegin` 里 runJavaScript:ArkWeb 的仓颉绑定没有
|
|
880
|
+
* `javaScriptOnDocumentStart`(桌面两平台都有对应 API),`onPageBegin` 是能拿到的最早时机,
|
|
881
|
+
* 那时页面顶层脚本**可能已经跑过**——内联页面因此改走「直接拼进 HTML」,语义才与桌面一致
|
|
882
|
+
* (document-start,早于页面脚本)。
|
|
883
|
+
*
|
|
884
|
+
* 拼装顺序:label → 桥 → init 脚本。init 脚本(插件 shim)可能读 `__CJ_TAURI_LABEL__`,
|
|
885
|
+
* 所以 label 必须最先;桥要早于 shim,因为 shim 会调用 `invoke`。
|
|
886
|
+
*/
|
|
887
|
+
private func injectBootstrap(html: String): String {
|
|
888
|
+
let scripts = StringBuilder("<script>${arkWebLabelScript(this.windowLabel)}</script>")
|
|
889
|
+
scripts.append("<script>${ARKWEB_BRIDGE_JS}</script>")
|
|
890
|
+
for (js in this.initScriptsSnapshot()) {
|
|
891
|
+
scripts.append("<script>${js}</script>")
|
|
892
|
+
}
|
|
893
|
+
let block = scripts.toString()
|
|
894
|
+
// 插在 head 开标签之后(最早的可执行位置);没有 head 就找 body,都没有就整段前置。
|
|
895
|
+
match (html.indexOf("<head>")) {
|
|
896
|
+
case Some(at) => return "${html[0..at + 6]}${block}${html[at + 6..html.size]}"
|
|
897
|
+
case None => ()
|
|
898
|
+
}
|
|
899
|
+
match (html.indexOf("<body")) {
|
|
900
|
+
case Some(at) => return "${html[0..at]}${block}${html[at..html.size]}"
|
|
901
|
+
case None => ()
|
|
902
|
+
}
|
|
903
|
+
return "${block}${html}"
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* 菜单项总数(含各级子孙):只用于日志对账,不参与渲染 */
|
|
908
|
+
@When[env == "ohos"]
|
|
909
|
+
private func countMenuItems(items: ArrayList<ArkWebMenuItem>): Int64 {
|
|
910
|
+
var total: Int64 = 0
|
|
911
|
+
for (item in items) {
|
|
912
|
+
total++
|
|
913
|
+
total += countMenuItems(item.children)
|
|
914
|
+
}
|
|
915
|
+
return total
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* shell 报文里 id 的最小转义:只处理 `\` 与 `"`(与 C 侧 cj_json_escape_into 同口径) */
|
|
919
|
+
@When[env == "ohos"]
|
|
920
|
+
private func arkWebJsonEscape(text: String): String {
|
|
921
|
+
// 迭代 String 得到的是 UInt8 字节(AGENTS §4),`String(byte)` 并不接受 UInt8——
|
|
922
|
+
// 这里只需要处理两个 ASCII 元字符,直接走 replace 更稳,也不碰字节/码点的口径。
|
|
923
|
+
return text.replace("\\", "\\\\").replace("\"", "\\\"")
|
|
924
|
+
}
|