@netless/window-manager 1.0.0-canary.9 → 1.0.1

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.
Files changed (132) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +90 -64
  3. package/README.zh-cn.md +224 -0
  4. package/dist/index.d.ts +1133 -40
  5. package/dist/index.js +62 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/{index.es.js → index.mjs} +7954 -5445
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/style.css +1 -1
  10. package/docs/advanced.md +55 -55
  11. package/docs/api.md +126 -113
  12. package/docs/app-context.md +248 -209
  13. package/docs/basic.md +25 -26
  14. package/docs/camera.md +21 -21
  15. package/docs/cn/advanced.md +137 -0
  16. package/docs/cn/api.md +311 -0
  17. package/docs/cn/app-context.md +369 -0
  18. package/docs/cn/basic.md +64 -0
  19. package/docs/cn/camera.md +53 -0
  20. package/docs/cn/concept.md +9 -0
  21. package/docs/cn/custom-max-bar.md +31 -0
  22. package/docs/cn/develop-app.md +94 -0
  23. package/docs/cn/export-pdf.md +48 -0
  24. package/docs/cn/migrate.md +60 -0
  25. package/docs/cn/replay.md +40 -0
  26. package/docs/concept.md +6 -5
  27. package/docs/custom-max-bar.md +31 -0
  28. package/docs/develop-app.md +22 -19
  29. package/docs/export-pdf.md +48 -0
  30. package/docs/migrate.md +25 -27
  31. package/docs/quickstart.md +50 -0
  32. package/docs/replay.md +20 -20
  33. package/package.json +32 -22
  34. package/src/App/AppContext.ts +105 -73
  35. package/src/App/AppPageStateImpl.ts +6 -25
  36. package/src/App/AppProxy.ts +41 -166
  37. package/src/App/MagixEvent/index.ts +38 -38
  38. package/src/App/Storage/StorageEvent.ts +13 -13
  39. package/src/App/Storage/index.ts +269 -245
  40. package/src/App/Storage/typings.ts +4 -2
  41. package/src/App/Storage/utils.ts +3 -3
  42. package/src/App/index.ts +0 -1
  43. package/src/AppListener.ts +8 -8
  44. package/src/AppManager.ts +88 -77
  45. package/src/AttributesDelegate.ts +42 -22
  46. package/src/BoxEmitter.ts +12 -6
  47. package/src/BoxManager.ts +128 -108
  48. package/src/ContainerResizeObserver.ts +75 -0
  49. package/src/Cursor/Cursor.svelte +16 -5
  50. package/src/Cursor/Cursor.svelte.d.ts +21 -0
  51. package/src/Cursor/Cursor.ts +77 -13
  52. package/src/Cursor/icons.ts +6 -0
  53. package/src/Cursor/icons2.ts +66 -0
  54. package/src/Cursor/index.ts +127 -26
  55. package/src/Helper.ts +94 -14
  56. package/src/InternalEmitter.ts +2 -7
  57. package/src/Page/index.ts +1 -1
  58. package/src/PageState.ts +6 -5
  59. package/src/ReconnectRefresher.ts +9 -4
  60. package/src/RedoUndo.ts +3 -3
  61. package/src/Register/index.ts +22 -17
  62. package/src/Register/loader.ts +26 -22
  63. package/src/Register/storage.ts +13 -13
  64. package/src/Utils/Common.ts +18 -14
  65. package/src/Utils/Reactive.ts +26 -25
  66. package/src/Utils/RoomHacker.ts +4 -4
  67. package/src/Utils/error.ts +0 -1
  68. package/src/View/IframeBridge.ts +680 -0
  69. package/src/View/MainView.ts +127 -53
  70. package/src/callback.ts +21 -1
  71. package/src/constants.ts +0 -2
  72. package/src/image/pencil-eraser-1.svg +3 -0
  73. package/src/image/pencil-eraser-2.svg +3 -0
  74. package/src/image/pencil-eraser-3.svg +3 -0
  75. package/src/index.ts +220 -83
  76. package/src/style.css +27 -10
  77. package/src/typings.ts +20 -10
  78. package/.prettierignore +0 -7
  79. package/.prettierrc.json +0 -9
  80. package/CHANGELOG.md +0 -196
  81. package/__mocks__/white-web-sdk.ts +0 -50
  82. package/dist/App/AppContext.d.ts +0 -76
  83. package/dist/App/AppPageStateImpl.d.ts +0 -21
  84. package/dist/App/AppProxy.d.ts +0 -86
  85. package/dist/App/AppViewSync.d.ts +0 -11
  86. package/dist/App/MagixEvent/index.d.ts +0 -29
  87. package/dist/App/Storage/StorageEvent.d.ts +0 -8
  88. package/dist/App/Storage/index.d.ts +0 -39
  89. package/dist/App/Storage/typings.d.ts +0 -22
  90. package/dist/App/Storage/utils.d.ts +0 -5
  91. package/dist/App/WhiteboardView.d.ts +0 -22
  92. package/dist/App/index.d.ts +0 -3
  93. package/dist/AppListener.d.ts +0 -21
  94. package/dist/AppManager.d.ts +0 -107
  95. package/dist/AttributesDelegate.d.ts +0 -80
  96. package/dist/BoxEmitter.d.ts +0 -34
  97. package/dist/BoxManager.d.ts +0 -99
  98. package/dist/BuiltinApps.d.ts +0 -5
  99. package/dist/Cursor/Cursor.d.ts +0 -39
  100. package/dist/Cursor/icons.d.ts +0 -3
  101. package/dist/Cursor/index.d.ts +0 -46
  102. package/dist/Helper.d.ts +0 -17
  103. package/dist/InternalEmitter.d.ts +0 -39
  104. package/dist/Page/PageController.d.ts +0 -21
  105. package/dist/Page/index.d.ts +0 -3
  106. package/dist/PageState.d.ts +0 -9
  107. package/dist/ReconnectRefresher.d.ts +0 -24
  108. package/dist/RedoUndo.d.ts +0 -18
  109. package/dist/Register/index.d.ts +0 -28
  110. package/dist/Register/loader.d.ts +0 -4
  111. package/dist/Register/storage.d.ts +0 -8
  112. package/dist/Utils/AppCreateQueue.d.ts +0 -15
  113. package/dist/Utils/Common.d.ts +0 -23
  114. package/dist/Utils/Reactive.d.ts +0 -6
  115. package/dist/Utils/RoomHacker.d.ts +0 -3
  116. package/dist/Utils/error.d.ts +0 -27
  117. package/dist/Utils/log.d.ts +0 -1
  118. package/dist/View/CameraSynchronizer.d.ts +0 -16
  119. package/dist/View/MainView.d.ts +0 -47
  120. package/dist/View/ViewManager.d.ts +0 -13
  121. package/dist/callback.d.ts +0 -24
  122. package/dist/constants.d.ts +0 -49
  123. package/dist/index.cjs.js +0 -46
  124. package/dist/index.umd.js +0 -46
  125. package/dist/typings.d.ts +0 -82
  126. package/jest.config.js +0 -27
  127. package/pnpm-lock.yaml +0 -6302
  128. package/src/App/AppViewSync.ts +0 -68
  129. package/src/App/WhiteboardView.ts +0 -83
  130. package/src/View/CameraSynchronizer.ts +0 -56
  131. package/vite.config.js +0 -51
  132. /package/docs/{qickstart.md → cn/quickstart.md} +0 -0
@@ -0,0 +1,369 @@
1
+ ## AppContext
2
+
3
+ `AppContext` 是插件运行时传入的上下文
4
+ 你可以通过此对象操作 APP 的 ui, 获取当前房间的状态, 以及订阅状态的变化
5
+
6
+ - [api](#api)
7
+ - [view](#view)
8
+ - [page](#page)
9
+ - [storage](#storage)
10
+ - [ui(box)](#box)
11
+ - [events](#events)
12
+ - [Advanced](#Advanced)
13
+
14
+ <h2 id="api">API</h2>
15
+
16
+ - **context.appId**
17
+
18
+ 插入 `app` 时生成的唯一 ID
19
+
20
+ ```ts
21
+ const appId = context.appId;
22
+ ```
23
+
24
+ - **context.isReplay**
25
+
26
+ 类型: `boolean`
27
+
28
+ 当前是否回放模式
29
+
30
+ - **context.getDisplayer()**
31
+
32
+ 在默认情况下 `Displayer` 为白板的 `room` 实例
33
+
34
+ 回放时则为 `Player` 实例
35
+
36
+ ```ts
37
+ const displayer = context.getDisplayer();
38
+
39
+ assert(displayer, room); // 互动房间
40
+ assert(displayer, player); // 回放房间
41
+ ```
42
+
43
+
44
+ - **context.getIsWritable()**
45
+
46
+ 获取当前状态是否可写\
47
+ 可以通过监听 `writableChange` 事件获取可写状态的改变
48
+
49
+ ```ts
50
+ const isWritable = context.getIsWritable();
51
+ ```
52
+
53
+ - **context.getBox()**
54
+
55
+ 获取当前 app 的 box
56
+
57
+ ```ts
58
+ const box = context.getBox();
59
+
60
+ box.$content; // box 的 main element
61
+ box.$footer;
62
+ ```
63
+
64
+ <h3 id="view">挂载白板</h3>
65
+
66
+ 当应用想要一个可以涂画的白板,可以使用以下接口
67
+
68
+ - **context.mountView()**
69
+
70
+ 挂载白板到指定 dom
71
+
72
+ ```ts
73
+ context.mountView(element);
74
+ ```
75
+
76
+ **注意** 在调用 `manager` 的 `addApp` 时必须填写 `scenePath` 才可以使用 `view`
77
+ ```ts
78
+ manager.addApp({
79
+ kind: "xxx",
80
+ options: { // 可选配置
81
+ scenePath: "/example-path"
82
+ }
83
+ })
84
+ ```
85
+
86
+ <h3 id="page">Page</h3>
87
+
88
+ 白板有多页的概念, 可以通过以下接口添加,切换,以及删除
89
+
90
+ - **context.addPage()**
91
+
92
+ 添加一页至 `view`
93
+
94
+ ```ts
95
+ context.addPage() // 默认在最后添加一页
96
+ context.addPage({ after: true }) // 在当前页后添加一页
97
+ context.addPage({ scene: { name: "page2" } }) // 传入 page 信息
98
+ ```
99
+
100
+ - **context.nextPage()**
101
+
102
+ 上一页
103
+
104
+ ```ts
105
+ context.nextPage();
106
+ ```
107
+
108
+ - **context.prevPage()**
109
+
110
+ 下一页
111
+
112
+ ```ts
113
+ context.prevPage();
114
+ ```
115
+ - **context.removePage()**
116
+
117
+ 删除一页
118
+
119
+ ```ts
120
+ context.removePage() // 默认删除当前页
121
+ context.removePage(1) // 也可以指定 index 删除
122
+ ```
123
+
124
+ - **context.pageState**
125
+
126
+ 获取当前所在的 `index` 和一共有多少页\
127
+ 当想要监听 `pageState` 的变化时, 可以监听 `pageStateChange` 事件获取最新的 `pageState`
128
+
129
+ ```ts
130
+ context.pageState;
131
+ // {
132
+ // index: number,
133
+ // length: number,
134
+ // }
135
+ ```
136
+
137
+ <h3 id="storage">storage</h3>
138
+
139
+ 存储和同步状态,以及发送事件的一系列集合
140
+
141
+ - **context.storage**
142
+
143
+ 默认创建的 storage 实例
144
+
145
+ ```ts
146
+ context.storage
147
+ ```
148
+
149
+ - **context.createStorage(namespace)**
150
+
151
+ 同时你也可以创建多个 `storage` 实例
152
+
153
+ 返回: `Storage<State>`
154
+
155
+ ```ts
156
+ type State = { count: number };
157
+ const defaultState = { count: 0 };
158
+ const storage = context.createStorage<State>("store1", defaultState);
159
+ ```
160
+
161
+ - **storage.state**
162
+
163
+ 类型: `State`\
164
+ 默认值: `defaultState`
165
+
166
+ 在所有客户端之间同步的状态,调用 `storage.setState()` 来改变它。
167
+
168
+ - **storage.ensureState(partialState)**
169
+
170
+ 确保 `storage.state` 包含某些初始值,类似于执行了:
171
+
172
+ ```js
173
+ // 这段代码不能直接运行,因为 app.state 是只读的
174
+ storage.state = { ...partialState, ...storage.state };
175
+ ```
176
+
177
+ **partialState**
178
+
179
+ 类型: `Partial<State>`
180
+
181
+ ```js
182
+ storage.state; // { a: 1 }
183
+ storage.ensureState({ a: 0, b: 0 });
184
+ storage.state; // { a: 1, b: 0 }
185
+ ```
186
+
187
+ - **storage.setState(partialState)**
188
+
189
+ 和 React 的 `setState` 类似,更新 `storage.state` 并同步到所有客户端。
190
+
191
+ 当设置某个字段为 `undefined` 时,它会被从 `storage.state` 里删除。
192
+
193
+ > - 状态同步所需的时间和网络状态与数据大小有关,建议只在 state 里存储必须的数据。
194
+
195
+ **partialState**
196
+
197
+ 类型: `Partial<State>`
198
+
199
+ ```js
200
+ storage.state; //=> { count: 0, a: 1 }
201
+ storage.setState({ count: storage.state.count + 1, b: 2 });
202
+ storage.state; //=> { count: 1, a: 1, b: 2 }
203
+ ```
204
+
205
+ - **storage.addStateChangedListener(listener)**
206
+
207
+ 它在有人调用 `storage.setState()` 后触发 (包含当前 `storage`)
208
+
209
+ 返回: `() => void`
210
+
211
+ ```js
212
+ const disposer = storage.addStateChangedListener(diff => {
213
+ console.log("state changed", diff.oldValue, diff.newValue);
214
+ disposer(); // remove listener by calling disposer
215
+ });
216
+ ```
217
+
218
+ - **context.dispatchMagixEvent(event, payload)**
219
+
220
+ 向其他客户端广播事件消息
221
+
222
+ ```js
223
+ context.dispatchMagixEvent("click", { data: "data" });
224
+ ```
225
+
226
+ - **context.addMagixEventListener(event, listener)**
227
+
228
+ 当接收来自其他客户端的消息时(当其他客户端调用'context.dispatchMagixEvent()`时), 它会被触发
229
+
230
+ 返回: `() => void` a disposer function.
231
+
232
+ ```js
233
+ const disposer = context.addMagixEventListener("click", ({ payload }) => {
234
+ console.log(payload.data);
235
+ disposer();
236
+ });
237
+
238
+ context.dispatchMagixEvent("click", { data: "data" });
239
+ ```
240
+
241
+ <h2>UI (box)</h2>
242
+
243
+ box 是白板为所有应用默认创建的 UI
244
+ 应用所有可以操作的 UI 部分都在 box 范围内
245
+
246
+ - **context.getBox()**
247
+
248
+ 获取 box
249
+ 返回类型: `ReadonlyTeleBox`
250
+
251
+ - **box.mountStyles()**
252
+
253
+ 挂载样式到 `box`
254
+ 参数: `string | HTMLStyleElement`
255
+
256
+ ```js
257
+ const box = context.getBox();
258
+ box.mountStyles(`
259
+ .app-span {
260
+ color: red;
261
+ }
262
+ `)
263
+ ```
264
+
265
+ - **box.mountContent()**
266
+
267
+ 挂载元素到 `box`
268
+ 参数: `HTMLElement`
269
+
270
+ ```js
271
+ const box = context.getBox();
272
+ const content = document.createElement("div");
273
+ box.mountContent(context);
274
+ ```
275
+
276
+ - **box.mountFooter()**
277
+
278
+ 挂载元素到 `box` 的 `footer`
279
+ 参数: `HTMLElement`
280
+
281
+ ```js
282
+ const box = context.getBox();
283
+ const footer = document.createElement("div");
284
+ box.mountFooter(context);
285
+ ```
286
+
287
+ <h2 id="events">events</h2>
288
+
289
+ - **destroy**
290
+
291
+ app 被关闭时发送
292
+
293
+ ```ts
294
+ context.emitter.on("destroy", () => {
295
+ // release your listeners
296
+ });
297
+ ```
298
+
299
+ - **writableChange**
300
+
301
+ 白板可写状态切换时触发
302
+
303
+ ```ts
304
+ context.emitter.on("writableChange", isWritable => {
305
+ //
306
+ });
307
+ ```
308
+
309
+ - **focus**
310
+
311
+ 当前 app 获得焦点或者失去焦点时触发
312
+
313
+ ```ts
314
+ context.emitter.on("focus", focus => {
315
+ //
316
+ });
317
+ ```
318
+
319
+ - **pageStateChange**
320
+
321
+ `PageState`
322
+
323
+ ```ts
324
+ type PateState {
325
+ index: number;
326
+ length: number;
327
+ }
328
+ ```
329
+
330
+ 当前页数和总页数变化时触发
331
+
332
+ ```ts
333
+ context.emitter.on("pageStateChange", pageState => {
334
+ // { index: 0, length: 1 }
335
+ });
336
+ ```
337
+ - **roomStageChange**
338
+
339
+ 房间的状态变化时触发\
340
+ 比如当教具切换时
341
+
342
+ ```js
343
+ context.emitter.on("roomStageChange", stage => {
344
+ if (state.memberState) {
345
+ console.log("appliance change to", state.memberState.currentApplianceName);
346
+ }
347
+ });
348
+ ```
349
+
350
+ 或者是当前房间人数变化时
351
+
352
+ ```js
353
+ context.emitter.on("roomStageChange", stage => {
354
+ if (state.roomMembers) {
355
+ console.log("current room members change", state.roomMembers);
356
+ }
357
+ });
358
+ ```
359
+ 详细状态的介绍请参考 https://developer.netless.link/javascript-zh/home/business-state-management
360
+
361
+ <h2 id="Advanced">Advanced</h2>
362
+
363
+ - **context.getView()**
364
+
365
+ 获取 `view` 实例
366
+
367
+ ```ts
368
+ const view = context.getView();
369
+ ```
@@ -0,0 +1,64 @@
1
+ # 基础教程
2
+ `WindowManager` 内置了 `DocsViewer` 和 `MediaPlayer` 用来播放 PPT 和音视频
3
+
4
+ ## 打开动态/静态 PPT
5
+ ```typescript
6
+ import { BuiltinApps } from "@netless/window-manager";
7
+
8
+ const appId = await manager.addApp({
9
+ kind: BuiltinApps.DocsViewer,
10
+ options: {
11
+ scenePath: "/docs-viewer", // 定义 ppt 所在的 scenePath
12
+ title: "docs1", // 可选
13
+ scenes: [], // SceneDefinition[] 静态/动态 Scene 数据
14
+ },
15
+ });
16
+ ```
17
+
18
+ ## 打开音视频
19
+ ```typescript
20
+ import { BuiltinApps } from "@netless/window-manager";
21
+
22
+ const appId = await manager.addApp({
23
+ kind: BuiltinApps.MediaPlayer,
24
+ options: {
25
+ title: "test.mp3", // 可选
26
+ },
27
+ attributes: {
28
+ src: "xxxx", // 音视频 url
29
+ },
30
+ });
31
+ ```
32
+
33
+
34
+ ## 查询所有的 App
35
+ ```typescript
36
+ const apps = manager.queryAll();
37
+ ```
38
+
39
+ ## 查询单个 APP
40
+ ```typescript
41
+ const app = manager.queryOne(appId);
42
+ ```
43
+
44
+ ## 关闭 App
45
+ ```typescript
46
+ manager.closeApp(appId);
47
+ ```
48
+
49
+ ## events
50
+
51
+ ### 窗口最小化最大化
52
+ ```typescript
53
+ manager.emitter.on("boxStateChange", state => {
54
+ // maximized | minimized | normal
55
+ });
56
+ ```
57
+
58
+ ### 视角跟随模式
59
+ ```typescript
60
+ manager.emitter.on("broadcastChange", state => {
61
+ // state: number | undefined
62
+ });
63
+ ```
64
+
@@ -0,0 +1,53 @@
1
+ # 视角
2
+ 在多窗口模式下, 可以同时存在多块白板,但是大多数情况下用户都只需要对主白板也就是 `mainView` 进行操作
3
+
4
+
5
+ ## 获取 `mainView` 的 `camera`
6
+ ```typescript
7
+ manager.mainView.camera
8
+ ```
9
+
10
+ ## 获取 `mainView` 的 `size`
11
+ ```typescript
12
+ manager.mainView.size
13
+ ```
14
+
15
+ ## 监听 `mainView` 的 `camera` 变化
16
+ ```typescript
17
+ manager.mainView.callbacks.on("onCameraUpdated", camera => {
18
+ // 更新后的 camera
19
+ })
20
+ ```
21
+
22
+ ## 监听 `mainView` 的 `size` 的变化
23
+ ```typescript
24
+ manager.mainView.callbacks.on("onSizeUpdated", camera => {
25
+ // 更新后的 size
26
+ })
27
+ ```
28
+
29
+ ## 通过 `api` 移动 `camera`
30
+ ```typescript
31
+ manager.moveCamera(camera)
32
+ ```
33
+
34
+ ## 设置视角边界
35
+ 把所有人的视角限制在以世界坐标 (0, 0) 为中心,宽为 1024,高为 768 的矩形之中。
36
+ ```typescript
37
+ manager.setCameraBound({
38
+ centerX: 0,
39
+ centerY: 0,
40
+ width: 1024,
41
+ height: 768,
42
+ })
43
+ ```
44
+
45
+ ## 禁止/允许 `mainView` `camera` 的移动,缩放
46
+ ```typescript
47
+ // 禁止 `camera` 移动,缩放
48
+ manager.mainView.disableCameraTransform = true
49
+
50
+ // 恢复 `camera` 移动,缩放
51
+ manager.mainView.disableCameraTransform = false
52
+ ```
53
+ **注意**,该属性为 `true` 时,仅仅禁止设备操作。你依然可以用 `moveCamera` 方法主动调整视角。
@@ -0,0 +1,9 @@
1
+ # 概念
2
+
3
+ ## 同步区域
4
+
5
+ 在不同分辨率的设备上,想要看到相同的区域和窗口,我们就需要在所有设备保持一个相同的比例。
6
+
7
+ 所以 `WindowManager` 有一个 `containerSizeRatio` 的选项来配置白板的宽高比,默认为 `9 / 16`
8
+
9
+ 如果外层给到 `WindowManager` 宽高并不是完美适配这个宽高比的话, `WindowManger` 会自动在内部算出一个适配这个比例的最大宽高,然后填充上去,这时在内部就会有一些区域不能操作
@@ -0,0 +1,31 @@
1
+ ## 如何自定义最大化 `titleBar`
2
+
3
+ 获取并订阅所有的 `box`
4
+
5
+ ```js
6
+ manager.boxManager.teleboxManager.boxes$.subscribe(boxes => {
7
+ // boxes 为所有的窗口,当窗口添加和删除时都会触发
8
+ })
9
+ ```
10
+
11
+ 切换 `box` 的 `focus`
12
+ ```js
13
+ manager.boxManager.teleBoxManager.focusBox(box)
14
+ ```
15
+
16
+ 关闭某个 `box`
17
+ ```js
18
+ manager.boxManager.teleBoxManager.remove(box)
19
+ ```
20
+
21
+ 切换最大化状态
22
+ ```js
23
+ manager.boxManager.teleBoxManager.setMaximized(false)
24
+ manager.boxManager.teleBoxManager.setMaximized(true)
25
+ ```
26
+
27
+ 切换最小化状态
28
+ ```js
29
+ manager.boxManager.teleBoxManager.setMinimized(true)
30
+ manager.boxManager.teleBoxManager.setMaximized(false)
31
+ ```
@@ -0,0 +1,94 @@
1
+ # 开发自定义 APP
2
+
3
+ - [AppContext](./app-context.md)
4
+
5
+ ## official apps https://github.com/netless-io/netless-app
6
+
7
+ ## app-with-whiteboard
8
+
9
+ 如果需要 app 中挂载白板请参考 [board.tsx](https://github.com/netless-io/window-manager/blob/master/example/app/board.tsx)
10
+
11
+ <br>
12
+
13
+ ```ts
14
+ import type { NetlessApp, AppContext } from "@netless/window-manager";
15
+
16
+ const HelloWorld: NetlessApp = {
17
+ kind: "HelloWorld",
18
+ setup: (context: AppContext) => {
19
+ context.mountView(context.getBox().$content); // 可选: 挂载 View 到 box 上
20
+ },
21
+ };
22
+
23
+ WindowManager.register({
24
+ kind: HelloWorld.kind,
25
+ src: HelloWorld,
26
+ });
27
+
28
+ manager.addApp({
29
+ kind: "HelloWorld",
30
+ options: {
31
+ scenePath: "/hello-world", // 如果需要在 App 中使用白板则必须声明 scenePath
32
+ },
33
+ });
34
+ ```
35
+
36
+ ## Counter
37
+
38
+ ```ts
39
+ const Counter: NetlessApp<{ count: number }> = {
40
+ kind: "Counter",
41
+ setup: (context) => {
42
+ const storage = context.storage;
43
+ storage.ensureState({ count: 0 });
44
+
45
+ const box = context.getBox(); // box 为这个应用打开的窗口
46
+ const $content = box.$content // 获取窗口的 content
47
+
48
+ const countDom = document.createElement("div");
49
+ countDom.innerText = storage.state.count.toString();
50
+ $content.appendChild(countDom);
51
+
52
+ // 监听 state 变化回调
53
+ storage.addStateChangedListener(diff => {
54
+ if (diff.count) {
55
+ // diff 会给出 newValue 和 oldValue
56
+ console.log(diff.count.newValue);
57
+ console.log(diff.count.oldValue);
58
+ countDom.innerText = diff.count.newValue.toString();
59
+ }
60
+ });
61
+
62
+ const incButton = document.createElement("button");
63
+ incButton.innerText = "Inc";
64
+ const incButtonOnClick = () => {
65
+ storage.setState({ count: storage.state.count + 1 });
66
+ }
67
+ incButton.addEventListener("click", incButtonOnClick);
68
+ $content.appendChild(incButton);
69
+
70
+ const decButton = document.createElement("button");
71
+ decButton.innerText = "Dec";
72
+ const decButtonOnClick = () => {
73
+ storage.setState({ count: storage.state.count - 1 });
74
+ }
75
+ decButton.addEventListener("click", decButtonOnClick);
76
+ $content.appendChild(decButton);
77
+
78
+ // 监听事件
79
+ const event1Disposer = context.addMagixEventListener("event1", msg => {
80
+ console.log("event1", msg);
81
+ });
82
+
83
+ // 向打开 app 的其他人发送消息
84
+ context.dispatchMagixEvent("event1", { count: 10 });
85
+
86
+ // 应用销毁时, 注意清理掉监听器
87
+ context.emitter.on("destroy", () => {
88
+ incButton.removeEventListener("click", incButtonOnClick);
89
+ decButton.removeEventListener("click", decButtonOnClick);
90
+ event1Disposer();
91
+ });
92
+ }
93
+ }
94
+ ```
@@ -0,0 +1,48 @@
1
+ # 导出 PDF
2
+
3
+ 此功能需要额外安装 `jspdf` 依赖才能使用
4
+
5
+ ```
6
+ npm install jspdf@2.5.1
7
+ ```
8
+
9
+ ### 支持的 app 及版本
10
+
11
+ 1. @netless/app-slide `0.2.23` 及以上版本支持保存动态 ppt 板书
12
+
13
+ 2. @netless/app-docs-viewer `0.2.10` 及以上版本支持保存 pdf 板书, **注意** app-docs-viewer 中可以展示静态 ppt, pdf, 动态 ppt. 其中只有 pdf 文件支持保存板书
14
+
15
+ 对应 @netless/window-manager `0.4.50` 及以上
16
+
17
+ 3. white-web-sdk `2.16.37` 及以上
18
+
19
+ ### 发起保存板书任务
20
+
21
+ 通过 `window.postMessage` 发事件来发起保存板书任务, 注意不要在任务尚未完成之前重复发送该事件.
22
+
23
+ ```js
24
+ window.postMessage({
25
+ type: "@netless/_request_save_pdf_",
26
+ appId: /* windowManager.addApp 返回的值, 指定要保存哪个窗口的板书, */
27
+ })
28
+ ```
29
+
30
+ ### 获取任务进度
31
+
32
+ 任务进度也通过 message 事件传递, 你需要在发起任务之前监听任务进度事件, 实例代码如下所示.
33
+ 其中 data.result 只有在任务成功时候才有值, 任务失败或者任务进行中都为 null.
34
+ **如果下载任务失败, 则 progress 为 100 但是 result 为 null.**
35
+
36
+ ```js
37
+ window.addEventListener("message", evt => {
38
+ if (evt.data.type === "@netless/_result_save_pdf_") {
39
+ console.log(evt.data);
40
+ // data 包含如下属性
41
+ // data.type: 固定值 "@netless/_result_save_pdf_"
42
+ // data.appId: 指明是哪次下载任务, 与发起保存板书时候传递的 appId 值一致
43
+ // data.progress: 下载进度, 0 ~ 100
44
+ // data.result: { pdf: ArrayBuffer {}, title: "a.pptx" } 或者 null, 为板书的 pdf 文件内容,
45
+ // 仅当下载进度 100 时才有值. 获取到 ArrayBuffer 后需要自行完成下载到本地的逻辑.
46
+ }
47
+ });
48
+ ```