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
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
package cjTauri.plugins.dialog
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import stdx.encoding.json.*
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 官方插件 dialog:原生对话框(对标 Tauri 的 dialog 插件)
|
|
10
|
-
*
|
|
11
|
-
* 命令:dialog:open / dialog:save / dialog:pickFolder / dialog:message
|
|
12
|
-
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
13
|
-
* "commands": ["dialog:open", "dialog:message"] // 明文逐条
|
|
14
|
-
* "permissions": ["dialog:default"] // 命名权限集(v1.1,一次放行一组)
|
|
15
|
-
* 前端:window.__CJ_TAURI__.dialog.open({ filter: "文本文件|*.txt|所有文件|*.*" })
|
|
16
|
-
* window.__CJ_TAURI__.dialog.pickFolder({ title: "选择目录" }) // 选目录(返回选中目录的绝对路径)
|
|
17
|
-
*
|
|
18
|
-
* 与 fs 插件的差别:这里要落到**系统对话框**,所以弹窗归宿主层(Windows 走 Win32 通用对话框、
|
|
19
|
-
* Linux 走 GTK),插件只做参数校验与命令分发——平台差异不进插件,与 AGENTS.md §2 的架构契约一致。
|
|
20
|
-
* 对话框需要父窗口,故必须等应用启动(start 之后)再调用;更早调用得到的是空结果(取消)。
|
|
21
|
-
*/
|
|
22
|
-
public class DialogPlugin <: Plugin {
|
|
23
|
-
// 四个命令共用一个处理器实例:setup() 里把宿主交给它(装配必须在 run()/runUrl() 之前完成)
|
|
24
|
-
private let handler: DialogCommands = DialogCommands()
|
|
25
|
-
|
|
26
|
-
public init() {}
|
|
27
|
-
|
|
28
|
-
public func name(): String {
|
|
29
|
-
return "dialog"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public func commands(): HashMap<String, CommandHandler> {
|
|
33
|
-
let m = HashMap<String, CommandHandler>()
|
|
34
|
-
m.add("open", this.handler)
|
|
35
|
-
m.add("save", this.handler)
|
|
36
|
-
m.add("pickFolder", this.handler)
|
|
37
|
-
m.add("message", this.handler)
|
|
38
|
-
return m
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 命名权限集(v1.1):files 放行三个「挑路径」命令(open / save / pickFolder),
|
|
43
|
-
* message(模态提示框)不在其中。只想让用户挑个文件或目录的应用引用 files 即可,
|
|
44
|
-
* 不必顺带拿到「能弹任意提示框」的能力。
|
|
45
|
-
*/
|
|
46
|
-
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
47
|
-
let m = HashMap<String, ArrayList<String>>()
|
|
48
|
-
let files = ArrayList<String>()
|
|
49
|
-
files.add("open")
|
|
50
|
-
files.add("save")
|
|
51
|
-
files.add("pickFolder")
|
|
52
|
-
m.add("files", files)
|
|
53
|
-
let all = ArrayList<String>()
|
|
54
|
-
all.add("open")
|
|
55
|
-
all.add("save")
|
|
56
|
-
all.add("pickFolder")
|
|
57
|
-
all.add("message")
|
|
58
|
-
m.add("default", all)
|
|
59
|
-
return m
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 前端 shim:往桥对象上挂一个 dialog 命名空间(与 FsPlugin 同款,不碰 invoke / listen / emit)
|
|
64
|
-
*/
|
|
65
|
-
public func jsShim(): String {
|
|
66
|
-
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
67
|
-
" t.dialog = {" +
|
|
68
|
-
" open: function (args) { return window.__CJ_TAURI__.invoke('dialog:open', args); }," +
|
|
69
|
-
" save: function (args) { return window.__CJ_TAURI__.invoke('dialog:save', args); }," +
|
|
70
|
-
" pickFolder: function (args) { return window.__CJ_TAURI__.invoke('dialog:pickFolder', args); }," +
|
|
71
|
-
" message: function (args) { return window.__CJ_TAURI__.invoke('dialog:message', args); }" +
|
|
72
|
-
" }; })();\n"
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 装配宿主:命令要靠它弹原生对话框。
|
|
77
|
-
* 这里只是把宿主对象记下来(宿主在构造 TauriApp 时就存在),真正弹窗要等 start 之后。
|
|
78
|
-
*
|
|
79
|
-
* **两个槽都装**:`app.webviewHost()` 只是默认窗那一个,多窗口下拿它等于
|
|
80
|
-
* 「谁先装配谁收所有弹窗」(第二扇窗的框排进默认窗队列,实测见 docs/鸿蒙ArkWeb接入踩坑.md §10.12);
|
|
81
|
-
* `app.hostOf` 才是按报文里的窗口取正确宿主的那条路,命令执行时用。
|
|
82
|
-
*/
|
|
83
|
-
public func setup(app: TauriApp): Unit {
|
|
84
|
-
this.handler.attachHost(app.webviewHost())
|
|
85
|
-
this.handler.attachHostResolver({ label => app.hostOf(label) })
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* dialog 插件的命令处理器(三个命令共用一份,按全名分派,与内置 SystemCommands 同形)
|
|
91
|
-
*/
|
|
92
|
-
// public 是给单测用的(跨包要能直接构造命令处理器,见 AGENTS §2「单测要直接构造的命令 handler 须 public」)
|
|
93
|
-
public class DialogCommands <: CommandHandler {
|
|
94
|
-
// 宿主层(弹原生对话框);未经 DialogPlugin 装配时为 None,命令给出可读的错误而不是崩
|
|
95
|
-
private var host: Option<WebViewHost> = None
|
|
96
|
-
|
|
97
|
-
/* 按 label 解析宿主(多窗口的正路):单槽 `host` 只记着**装配时那一个**宿主,
|
|
98
|
-
多窗口下用它就是「谁先装配谁收所有弹窗」——2026-10-20 鸿蒙真机实测:两扇窗同时弹框,
|
|
99
|
-
hilog 两次 `dialog take label=main`,第二次的 title 却是 second,且 main 的 @State
|
|
100
|
-
被覆盖成对面内容、第二窗自己的框从未显示(判据与过程见 docs/鸿蒙ArkWeb接入踩坑.md §10.12)。
|
|
101
|
-
单测也靠它注入桩宿主,不必构造真实 TauriApp。 */
|
|
102
|
-
private var hostByWindow: Option<(String) -> Option<WebViewHost>> = None
|
|
103
|
-
|
|
104
|
-
public init() {}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 装配**单个**宿主(单窗口 / 单测用)
|
|
108
|
-
*
|
|
109
|
-
* 多窗口一律走 `attachHostResolver`:这个槽不带窗口身份,第二扇窗的命令会落到这里那一个宿主上。
|
|
110
|
-
* 两者同时装时可以共存——解析器优先,`host` 退成兜底(解析器给出 None 时才用)。
|
|
111
|
-
*/
|
|
112
|
-
public func attachHost(h: WebViewHost): Unit {
|
|
113
|
-
this.host = Some(h)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** 装配「label → 宿主」解析器;由 DialogPlugin.setup() 传入 `app.hostOf` */
|
|
117
|
-
public func attachHostResolver(resolve: (String) -> Option<WebViewHost>): Unit {
|
|
118
|
-
this.hostByWindow = Some(resolve)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
122
|
-
match (cmd) {
|
|
123
|
-
case "dialog:open" => JsonString(this.pickFile(args, false, ipc))
|
|
124
|
-
case "dialog:save" => JsonString(this.pickFile(args, true, ipc))
|
|
125
|
-
case "dialog:pickFolder" => JsonString(this.pickFolder(args, ipc))
|
|
126
|
-
case "dialog:message" => JsonBool(this.message(args, ipc))
|
|
127
|
-
case _ => throw CommandException("unknown dialog command: ${cmd}")
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* 取**发起这条命令的窗口**的宿主
|
|
133
|
-
*
|
|
134
|
-
* 多窗口下必须按 `ipc.window` 解析:用装配时那一个宿主,第二扇窗的弹窗会排进默认窗的队列,
|
|
135
|
-
* 表象是「另一扇窗里冒出别人的框」外加「自己那个框永远不出现」。
|
|
136
|
-
* 解析器在场而窗口没登记时**如实报错**,不要悄悄回落到单槽——那正是串台的来源。
|
|
137
|
-
*/
|
|
138
|
-
private func requireHost(ipc: IpcContext): WebViewHost {
|
|
139
|
-
match (this.hostByWindow) {
|
|
140
|
-
case Some(resolve) => match (resolve(ipc.window)) {
|
|
141
|
-
case Some(h) => return h
|
|
142
|
-
// 解析器在场=多窗口正路:这里回落到单槽就是「弹到别人窗口里」,
|
|
143
|
-
// 所以宁可不弹,把窗口 label 原样报出去(少一个框好过串台)。
|
|
144
|
-
case None => throw CommandException(
|
|
145
|
-
"dialog: 窗口 \"${ipc.window}\" 没有登记宿主(addWindow(); 默认窗在 run() 之前也可用)")
|
|
146
|
-
}
|
|
147
|
-
case None => ()
|
|
148
|
-
}
|
|
149
|
-
match (this.host) {
|
|
150
|
-
case Some(h) => h
|
|
151
|
-
case None => throw CommandException(
|
|
152
|
-
"dialog: 宿主未装配(用 .plugin(DialogPlugin()) 装配插件后才可用)")
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 取必需的字符串参数;缺失或类型不对时抛业务错误,前端拿到的是可读原文 */
|
|
157
|
-
private func requireString(args: JsonObject, key: String): String {
|
|
158
|
-
match (args.get(key)) {
|
|
159
|
-
case Some(v) => match (v.kind()) {
|
|
160
|
-
case JsonKind.JsString => return v.asString().getValue()
|
|
161
|
-
case _ => throw CommandException("dialog: argument \"${key}\" must be a string")
|
|
162
|
-
}
|
|
163
|
-
case None => throw CommandException("dialog: missing argument \"${key}\"")
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 取可选字符串参数:缺失或类型不对都退回默认值(标题 / 过滤器都不是必需的) */
|
|
168
|
-
private func optionalString(args: JsonObject, key: String, fallback: String): String {
|
|
169
|
-
match (args.get(key)) {
|
|
170
|
-
case Some(v) => match (v.kind()) {
|
|
171
|
-
case JsonKind.JsString => return v.asString().getValue()
|
|
172
|
-
case _ => fallback
|
|
173
|
-
}
|
|
174
|
-
case None => fallback
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private func pickFile(args: JsonObject, save: Bool, ipc: IpcContext): String {
|
|
179
|
-
var title = this.optionalString(args, "title", "")
|
|
180
|
-
if (title.size == 0) {
|
|
181
|
-
if (save) {
|
|
182
|
-
title = "保存文件"
|
|
183
|
-
} else {
|
|
184
|
-
title = "打开文件"
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
let filter = this.optionalString(args, "filter", "")
|
|
188
|
-
return this.requireHost(ipc).showFileDialog(save, title, filter)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** 选目录(宿主侧 kind=6):只有 title 一个可选参数;取消回空串,与文件框同语义 */
|
|
192
|
-
private func pickFolder(args: JsonObject, ipc: IpcContext): String {
|
|
193
|
-
var title = this.optionalString(args, "title", "")
|
|
194
|
-
if (title.size == 0) {
|
|
195
|
-
title = "选择文件夹"
|
|
196
|
-
}
|
|
197
|
-
return this.requireHost(ipc).showFolderDialog(title)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private func message(args: JsonObject, ipc: IpcContext): Bool {
|
|
201
|
-
let text = this.requireString(args, "message")
|
|
202
|
-
let kind = this.optionalString(args, "kind", "info")
|
|
203
|
-
var title = this.optionalString(args, "title", "")
|
|
204
|
-
if (title.size == 0) {
|
|
205
|
-
title = "提示"
|
|
206
|
-
}
|
|
207
|
-
return this.requireHost(ipc).showMessageDialog(kind, title, text)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
1
|
+
package cjTauri.plugins.dialog
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 官方插件 dialog:原生对话框(对标 Tauri 的 dialog 插件)
|
|
10
|
+
*
|
|
11
|
+
* 命令:dialog:open / dialog:save / dialog:pickFolder / dialog:message
|
|
12
|
+
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
13
|
+
* "commands": ["dialog:open", "dialog:message"] // 明文逐条
|
|
14
|
+
* "permissions": ["dialog:default"] // 命名权限集(v1.1,一次放行一组)
|
|
15
|
+
* 前端:window.__CJ_TAURI__.dialog.open({ filter: "文本文件|*.txt|所有文件|*.*" })
|
|
16
|
+
* window.__CJ_TAURI__.dialog.pickFolder({ title: "选择目录" }) // 选目录(返回选中目录的绝对路径)
|
|
17
|
+
*
|
|
18
|
+
* 与 fs 插件的差别:这里要落到**系统对话框**,所以弹窗归宿主层(Windows 走 Win32 通用对话框、
|
|
19
|
+
* Linux 走 GTK),插件只做参数校验与命令分发——平台差异不进插件,与 AGENTS.md §2 的架构契约一致。
|
|
20
|
+
* 对话框需要父窗口,故必须等应用启动(start 之后)再调用;更早调用得到的是空结果(取消)。
|
|
21
|
+
*/
|
|
22
|
+
public class DialogPlugin <: Plugin {
|
|
23
|
+
// 四个命令共用一个处理器实例:setup() 里把宿主交给它(装配必须在 run()/runUrl() 之前完成)
|
|
24
|
+
private let handler: DialogCommands = DialogCommands()
|
|
25
|
+
|
|
26
|
+
public init() {}
|
|
27
|
+
|
|
28
|
+
public func name(): String {
|
|
29
|
+
return "dialog"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
33
|
+
let m = HashMap<String, CommandHandler>()
|
|
34
|
+
m.add("open", this.handler)
|
|
35
|
+
m.add("save", this.handler)
|
|
36
|
+
m.add("pickFolder", this.handler)
|
|
37
|
+
m.add("message", this.handler)
|
|
38
|
+
return m
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 命名权限集(v1.1):files 放行三个「挑路径」命令(open / save / pickFolder),
|
|
43
|
+
* message(模态提示框)不在其中。只想让用户挑个文件或目录的应用引用 files 即可,
|
|
44
|
+
* 不必顺带拿到「能弹任意提示框」的能力。
|
|
45
|
+
*/
|
|
46
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
47
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
48
|
+
let files = ArrayList<String>()
|
|
49
|
+
files.add("open")
|
|
50
|
+
files.add("save")
|
|
51
|
+
files.add("pickFolder")
|
|
52
|
+
m.add("files", files)
|
|
53
|
+
let all = ArrayList<String>()
|
|
54
|
+
all.add("open")
|
|
55
|
+
all.add("save")
|
|
56
|
+
all.add("pickFolder")
|
|
57
|
+
all.add("message")
|
|
58
|
+
m.add("default", all)
|
|
59
|
+
return m
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 前端 shim:往桥对象上挂一个 dialog 命名空间(与 FsPlugin 同款,不碰 invoke / listen / emit)
|
|
64
|
+
*/
|
|
65
|
+
public func jsShim(): String {
|
|
66
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
67
|
+
" t.dialog = {" +
|
|
68
|
+
" open: function (args) { return window.__CJ_TAURI__.invoke('dialog:open', args); }," +
|
|
69
|
+
" save: function (args) { return window.__CJ_TAURI__.invoke('dialog:save', args); }," +
|
|
70
|
+
" pickFolder: function (args) { return window.__CJ_TAURI__.invoke('dialog:pickFolder', args); }," +
|
|
71
|
+
" message: function (args) { return window.__CJ_TAURI__.invoke('dialog:message', args); }" +
|
|
72
|
+
" }; })();\n"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 装配宿主:命令要靠它弹原生对话框。
|
|
77
|
+
* 这里只是把宿主对象记下来(宿主在构造 TauriApp 时就存在),真正弹窗要等 start 之后。
|
|
78
|
+
*
|
|
79
|
+
* **两个槽都装**:`app.webviewHost()` 只是默认窗那一个,多窗口下拿它等于
|
|
80
|
+
* 「谁先装配谁收所有弹窗」(第二扇窗的框排进默认窗队列,实测见 docs/鸿蒙ArkWeb接入踩坑.md §10.12);
|
|
81
|
+
* `app.hostOf` 才是按报文里的窗口取正确宿主的那条路,命令执行时用。
|
|
82
|
+
*/
|
|
83
|
+
public func setup(app: TauriApp): Unit {
|
|
84
|
+
this.handler.attachHost(app.webviewHost())
|
|
85
|
+
this.handler.attachHostResolver({ label => app.hostOf(label) })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* dialog 插件的命令处理器(三个命令共用一份,按全名分派,与内置 SystemCommands 同形)
|
|
91
|
+
*/
|
|
92
|
+
// public 是给单测用的(跨包要能直接构造命令处理器,见 AGENTS §2「单测要直接构造的命令 handler 须 public」)
|
|
93
|
+
public class DialogCommands <: CommandHandler {
|
|
94
|
+
// 宿主层(弹原生对话框);未经 DialogPlugin 装配时为 None,命令给出可读的错误而不是崩
|
|
95
|
+
private var host: Option<WebViewHost> = None
|
|
96
|
+
|
|
97
|
+
/* 按 label 解析宿主(多窗口的正路):单槽 `host` 只记着**装配时那一个**宿主,
|
|
98
|
+
多窗口下用它就是「谁先装配谁收所有弹窗」——2026-10-20 鸿蒙真机实测:两扇窗同时弹框,
|
|
99
|
+
hilog 两次 `dialog take label=main`,第二次的 title 却是 second,且 main 的 @State
|
|
100
|
+
被覆盖成对面内容、第二窗自己的框从未显示(判据与过程见 docs/鸿蒙ArkWeb接入踩坑.md §10.12)。
|
|
101
|
+
单测也靠它注入桩宿主,不必构造真实 TauriApp。 */
|
|
102
|
+
private var hostByWindow: Option<(String) -> Option<WebViewHost>> = None
|
|
103
|
+
|
|
104
|
+
public init() {}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 装配**单个**宿主(单窗口 / 单测用)
|
|
108
|
+
*
|
|
109
|
+
* 多窗口一律走 `attachHostResolver`:这个槽不带窗口身份,第二扇窗的命令会落到这里那一个宿主上。
|
|
110
|
+
* 两者同时装时可以共存——解析器优先,`host` 退成兜底(解析器给出 None 时才用)。
|
|
111
|
+
*/
|
|
112
|
+
public func attachHost(h: WebViewHost): Unit {
|
|
113
|
+
this.host = Some(h)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 装配「label → 宿主」解析器;由 DialogPlugin.setup() 传入 `app.hostOf` */
|
|
117
|
+
public func attachHostResolver(resolve: (String) -> Option<WebViewHost>): Unit {
|
|
118
|
+
this.hostByWindow = Some(resolve)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
122
|
+
match (cmd) {
|
|
123
|
+
case "dialog:open" => JsonString(this.pickFile(args, false, ipc))
|
|
124
|
+
case "dialog:save" => JsonString(this.pickFile(args, true, ipc))
|
|
125
|
+
case "dialog:pickFolder" => JsonString(this.pickFolder(args, ipc))
|
|
126
|
+
case "dialog:message" => JsonBool(this.message(args, ipc))
|
|
127
|
+
case _ => throw CommandException("unknown dialog command: ${cmd}")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 取**发起这条命令的窗口**的宿主
|
|
133
|
+
*
|
|
134
|
+
* 多窗口下必须按 `ipc.window` 解析:用装配时那一个宿主,第二扇窗的弹窗会排进默认窗的队列,
|
|
135
|
+
* 表象是「另一扇窗里冒出别人的框」外加「自己那个框永远不出现」。
|
|
136
|
+
* 解析器在场而窗口没登记时**如实报错**,不要悄悄回落到单槽——那正是串台的来源。
|
|
137
|
+
*/
|
|
138
|
+
private func requireHost(ipc: IpcContext): WebViewHost {
|
|
139
|
+
match (this.hostByWindow) {
|
|
140
|
+
case Some(resolve) => match (resolve(ipc.window)) {
|
|
141
|
+
case Some(h) => return h
|
|
142
|
+
// 解析器在场=多窗口正路:这里回落到单槽就是「弹到别人窗口里」,
|
|
143
|
+
// 所以宁可不弹,把窗口 label 原样报出去(少一个框好过串台)。
|
|
144
|
+
case None => throw CommandException(
|
|
145
|
+
"dialog: 窗口 \"${ipc.window}\" 没有登记宿主(addWindow(); 默认窗在 run() 之前也可用)")
|
|
146
|
+
}
|
|
147
|
+
case None => ()
|
|
148
|
+
}
|
|
149
|
+
match (this.host) {
|
|
150
|
+
case Some(h) => h
|
|
151
|
+
case None => throw CommandException(
|
|
152
|
+
"dialog: 宿主未装配(用 .plugin(DialogPlugin()) 装配插件后才可用)")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 取必需的字符串参数;缺失或类型不对时抛业务错误,前端拿到的是可读原文 */
|
|
157
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
158
|
+
match (args.get(key)) {
|
|
159
|
+
case Some(v) => match (v.kind()) {
|
|
160
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
161
|
+
case _ => throw CommandException("dialog: argument \"${key}\" must be a string")
|
|
162
|
+
}
|
|
163
|
+
case None => throw CommandException("dialog: missing argument \"${key}\"")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 取可选字符串参数:缺失或类型不对都退回默认值(标题 / 过滤器都不是必需的) */
|
|
168
|
+
private func optionalString(args: JsonObject, key: String, fallback: String): String {
|
|
169
|
+
match (args.get(key)) {
|
|
170
|
+
case Some(v) => match (v.kind()) {
|
|
171
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
172
|
+
case _ => fallback
|
|
173
|
+
}
|
|
174
|
+
case None => fallback
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func pickFile(args: JsonObject, save: Bool, ipc: IpcContext): String {
|
|
179
|
+
var title = this.optionalString(args, "title", "")
|
|
180
|
+
if (title.size == 0) {
|
|
181
|
+
if (save) {
|
|
182
|
+
title = "保存文件"
|
|
183
|
+
} else {
|
|
184
|
+
title = "打开文件"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let filter = this.optionalString(args, "filter", "")
|
|
188
|
+
return this.requireHost(ipc).showFileDialog(save, title, filter)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 选目录(宿主侧 kind=6):只有 title 一个可选参数;取消回空串,与文件框同语义 */
|
|
192
|
+
private func pickFolder(args: JsonObject, ipc: IpcContext): String {
|
|
193
|
+
var title = this.optionalString(args, "title", "")
|
|
194
|
+
if (title.size == 0) {
|
|
195
|
+
title = "选择文件夹"
|
|
196
|
+
}
|
|
197
|
+
return this.requireHost(ipc).showFolderDialog(title)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func message(args: JsonObject, ipc: IpcContext): Bool {
|
|
201
|
+
let text = this.requireString(args, "message")
|
|
202
|
+
let kind = this.optionalString(args, "kind", "info")
|
|
203
|
+
var title = this.optionalString(args, "title", "")
|
|
204
|
+
if (title.size == 0) {
|
|
205
|
+
title = "提示"
|
|
206
|
+
}
|
|
207
|
+
return this.requireHost(ipc).showMessageDialog(kind, title, text)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -10,12 +10,20 @@ import stdx.encoding.json.*
|
|
|
10
10
|
* 官方插件 fs:文件读写(纯仓颉,零 C 桥改动,RFC-001 §7.3)
|
|
11
11
|
*
|
|
12
12
|
* 命令:fs:readText / fs:writeText / fs:exists / fs:list / fs:remove
|
|
13
|
+
* / fs:mkdir / fs:rename / fs:copy / fs:stat
|
|
13
14
|
* - readText / writeText:整文件读写(writeText 是覆盖写:已存在会截断,不存在则创建)
|
|
14
15
|
* - exists:路径是否存在
|
|
15
16
|
* - list:**平铺一层**列目录,返回 [{ name, dir, size }];不递归,顺序就是文件系统给的顺序(展示排序交给前端)
|
|
16
17
|
* - remove:只删**单个文件**,目标是目录一律拒——std.fs 的 remove 能把**空目录**也删掉,
|
|
17
18
|
* 所以这层挡在调用前(不做递归删除);文件不存在同样报错:调用方本想删的东西没删掉,
|
|
18
19
|
* 静默回 true 等于把失败伪装成成功
|
|
20
|
+
* - mkdir:建目录,入参 { path, recursive? };目标已存在报错(与 std.fs 语义一致,
|
|
21
|
+
* 不静默回落成「已存在即成功」——调用方分不清建成了还是本来就有)
|
|
22
|
+
* - rename / copy:入参 { path, newPath, overwrite? },**两个路径都过 scope 校验**
|
|
23
|
+
* ( newPath 同样是页面可控路径,漏校验等于 scope 形同虚设);overwrite 缺省 false,
|
|
24
|
+
* 覆盖既有文件必须页面显式声明
|
|
25
|
+
* - stat:单路径元信息 { name, dir, size, lastModified }(lastModified 是平台本地时区的
|
|
26
|
+
* 可读字符串,比较排序交给前端)
|
|
19
27
|
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
20
28
|
* "commands": ["fs:readText", "fs:exists"] // 明文逐条(旧写法,一直支持)
|
|
21
29
|
* "permissions": ["fs:readonly"] // 命名权限集(v1.1,一次放行一组)
|
|
@@ -43,6 +51,10 @@ public class FsPlugin <: Plugin {
|
|
|
43
51
|
m.add("exists", FsCommands())
|
|
44
52
|
m.add("list", FsCommands())
|
|
45
53
|
m.add("remove", FsCommands())
|
|
54
|
+
m.add("mkdir", FsCommands())
|
|
55
|
+
m.add("rename", FsCommands())
|
|
56
|
+
m.add("copy", FsCommands())
|
|
57
|
+
m.add("stat", FsCommands())
|
|
46
58
|
return m
|
|
47
59
|
}
|
|
48
60
|
|
|
@@ -59,6 +71,7 @@ public class FsPlugin <: Plugin {
|
|
|
59
71
|
ro.add("readText")
|
|
60
72
|
ro.add("exists")
|
|
61
73
|
ro.add("list") // 列目录也是读操作:只读应用(如文件浏览)要能拿它,但拿不到 remove
|
|
74
|
+
ro.add("stat") // 元信息同样是读操作:不暴露任何写能力
|
|
62
75
|
m.add("readonly", ro)
|
|
63
76
|
let all = ArrayList<String>()
|
|
64
77
|
all.add("readText")
|
|
@@ -66,6 +79,10 @@ public class FsPlugin <: Plugin {
|
|
|
66
79
|
all.add("exists")
|
|
67
80
|
all.add("list")
|
|
68
81
|
all.add("remove")
|
|
82
|
+
all.add("mkdir")
|
|
83
|
+
all.add("rename")
|
|
84
|
+
all.add("copy")
|
|
85
|
+
all.add("stat")
|
|
69
86
|
m.add("default", all)
|
|
70
87
|
return m
|
|
71
88
|
}
|
|
@@ -81,7 +98,11 @@ public class FsPlugin <: Plugin {
|
|
|
81
98
|
" writeText: function (args) { return window.__CJ_TAURI__.invoke('fs:writeText', args); }," +
|
|
82
99
|
" exists: function (args) { return window.__CJ_TAURI__.invoke('fs:exists', args); }," +
|
|
83
100
|
" list: function (args) { return window.__CJ_TAURI__.invoke('fs:list', args); }," +
|
|
84
|
-
" remove: function (args) { return window.__CJ_TAURI__.invoke('fs:remove', args); }" +
|
|
101
|
+
" remove: function (args) { return window.__CJ_TAURI__.invoke('fs:remove', args); }," +
|
|
102
|
+
" mkdir: function (args) { return window.__CJ_TAURI__.invoke('fs:mkdir', args); }," +
|
|
103
|
+
" rename: function (args) { return window.__CJ_TAURI__.invoke('fs:rename', args); }," +
|
|
104
|
+
" copy: function (args) { return window.__CJ_TAURI__.invoke('fs:copy', args); }," +
|
|
105
|
+
" stat: function (args) { return window.__CJ_TAURI__.invoke('fs:stat', args); }" +
|
|
85
106
|
" }; })();\n"
|
|
86
107
|
}
|
|
87
108
|
}
|
|
@@ -99,6 +120,10 @@ class FsCommands <: CommandHandler {
|
|
|
99
120
|
case "fs:exists" => this.exists(args, ipc)
|
|
100
121
|
case "fs:list" => this.listEntries(args, ipc)
|
|
101
122
|
case "fs:remove" => this.removeFile(args, ipc)
|
|
123
|
+
case "fs:mkdir" => this.mkdir(args, ipc)
|
|
124
|
+
case "fs:rename" => this.doRename(args, ipc)
|
|
125
|
+
case "fs:copy" => this.doCopy(args, ipc)
|
|
126
|
+
case "fs:stat" => this.stat(args, ipc)
|
|
102
127
|
case _ => throw CommandException("unknown fs command: ${cmd}")
|
|
103
128
|
}
|
|
104
129
|
}
|
|
@@ -122,6 +147,18 @@ class FsCommands <: CommandHandler {
|
|
|
122
147
|
}
|
|
123
148
|
}
|
|
124
149
|
|
|
150
|
+
/** 取可选布尔参数:缺省回 false;给了但不是布尔当场拒(写 true/false 以外的东西多半是调用方写错) */
|
|
151
|
+
private func optionalBool(args: JsonObject, key: String): Bool {
|
|
152
|
+
match (args.get(key)) {
|
|
153
|
+
case Some(v) => match (v.kind()) {
|
|
154
|
+
case JsonKind.JsBool => return v.asBool().getValue()
|
|
155
|
+
case JsonKind.JsNull => return false
|
|
156
|
+
case _ => throw CommandException("fs: argument \"${key}\" must be a boolean")
|
|
157
|
+
}
|
|
158
|
+
case None => return false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
private func readText(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
126
163
|
let path = this.scopedPath(args, ipc)
|
|
127
164
|
try {
|
|
@@ -191,4 +228,71 @@ class FsCommands <: CommandHandler {
|
|
|
191
228
|
throw CommandException("fs:remove failed: ${e}")
|
|
192
229
|
}
|
|
193
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 建目录。目标已存在是**错误**而不是成功:recursive 也一样(std.fs 的语义),
|
|
234
|
+
* 静默回 true 会让调用方分不清「刚建出来」和「本来就有」,前端要幂等就先 exists 再 mkdir。
|
|
235
|
+
*/
|
|
236
|
+
private func mkdir(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
237
|
+
let path = this.scopedPath(args, ipc)
|
|
238
|
+
let recursive = this.optionalBool(args, "recursive")
|
|
239
|
+
try {
|
|
240
|
+
Directory.create(path, recursive: recursive)
|
|
241
|
+
return JsonBool(true)
|
|
242
|
+
} catch (e: Exception) {
|
|
243
|
+
throw CommandException("fs:mkdir failed: ${e}")
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** rename/copy 共用:目标路径参数同样过 scope 校验(newPath 也是页面可控路径) */
|
|
248
|
+
private func scopedNewPath(args: JsonObject, ipc: IpcContext): String {
|
|
249
|
+
return ipc.resolveScopedPath("fs", this.requireString(args, "newPath"))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 重命名(可移动到同盘另一目录)。overwrite 缺省 false:撞上既有路径即报错。
|
|
254
|
+
* 方法名刻意叫 doRename:成员方法与 std.fs 的顶层 rename 同名时,
|
|
255
|
+
* 方法体内的裸调用会解析回自身而不是库函数。
|
|
256
|
+
*/
|
|
257
|
+
private func doRename(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
258
|
+
let path = this.scopedPath(args, ipc)
|
|
259
|
+
let newPath = this.scopedNewPath(args, ipc)
|
|
260
|
+
let overwrite = this.optionalBool(args, "overwrite")
|
|
261
|
+
try {
|
|
262
|
+
rename(path, to: newPath, overwrite: overwrite)
|
|
263
|
+
return JsonBool(true)
|
|
264
|
+
} catch (e: Exception) {
|
|
265
|
+
throw CommandException("fs:rename failed: ${e}")
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 复制文件或目录(std.fs 的 copy 目录语义是递归拷贝)。overwrite 缺省 false。命名理由同 doRename */
|
|
270
|
+
private func doCopy(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
271
|
+
let path = this.scopedPath(args, ipc)
|
|
272
|
+
let newPath = this.scopedNewPath(args, ipc)
|
|
273
|
+
let overwrite = this.optionalBool(args, "overwrite")
|
|
274
|
+
try {
|
|
275
|
+
copy(path, to: newPath, overwrite: overwrite)
|
|
276
|
+
return JsonBool(true)
|
|
277
|
+
} catch (e: Exception) {
|
|
278
|
+
throw CommandException("fs:copy failed: ${e}")
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 单路径元信息。mtime 给可读字符串:前端只做展示,排序/比较用 size 或自己解析 */
|
|
283
|
+
private func stat(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
284
|
+
let path = this.scopedPath(args, ipc)
|
|
285
|
+
let info: FileInfo
|
|
286
|
+
try {
|
|
287
|
+
info = FileInfo(path)
|
|
288
|
+
} catch (e: Exception) {
|
|
289
|
+
throw CommandException("fs:stat failed: ${e}")
|
|
290
|
+
}
|
|
291
|
+
let obj = JsonObject()
|
|
292
|
+
obj.put("name", JsonString(info.name))
|
|
293
|
+
obj.put("dir", JsonBool(info.isDirectory()))
|
|
294
|
+
obj.put("size", JsonInt(info.size))
|
|
295
|
+
obj.put("lastModified", JsonString(info.lastModificationTime.toString()))
|
|
296
|
+
return obj
|
|
297
|
+
}
|
|
194
298
|
}
|