@netless/window-manager 0.4.60 → 0.4.61

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.
@@ -0,0 +1,636 @@
1
+ import type { Displayer, DisplayerState, Room, RoomState } from "white-web-sdk";
2
+ import type { AppManager } from "../AppManager";
3
+ import type { WindowManager } from "../index";
4
+
5
+ import Emittery from "emittery";
6
+ import { PlayerPhase, AnimationMode, autorun } from "white-web-sdk";
7
+ import { SideEffectManager } from "side-effect-manager";
8
+ import { debounce, noop } from "lodash";
9
+ import { log } from "../Utils/log";
10
+
11
+ // Note: typo below should not be fixed.
12
+ export enum IframeEvents {
13
+ Init = "Init",
14
+ AttributesUpdate = "AttributesUpdate",
15
+ SetAttributes = "SetAttributes",
16
+ RegisterMagixEvent = "RegisterMagixEvent",
17
+ RemoveMagixEvent = "RemoveMagixEvent",
18
+ RemoveAllMagixEvent = "RemoveAllMagixEvent",
19
+ RoomStateChanged = "RoomStateChanged",
20
+ DispatchMagixEvent = "DispatchMagixEvent",
21
+ ReciveMagixEvent = "ReciveMagixEvent",
22
+ NextPage = "NextPage",
23
+ PrevPage = "PrevPage",
24
+ SDKCreate = "SDKCreate",
25
+ OnCreate = "OnCreate",
26
+ SetPage = "SetPage",
27
+ GetAttributes = "GetAttributes",
28
+ Ready = "Ready",
29
+ Destory = "Destory",
30
+ StartCreate = "StartCreate",
31
+ WrapperDidUpdate = "WrapperDidUpdate",
32
+ DispayIframe = "DispayIframe",
33
+ HideIframe = "HideIframe",
34
+ GetRootRect = "GetRootRect",
35
+ ReplayRootRect = "ReplayRootRect",
36
+ PageTo = "PageTo",
37
+ }
38
+
39
+ export enum DomEvents {
40
+ WrapperDidMount = "WrapperDidMount",
41
+ IframeLoad = "IframeLoad",
42
+ }
43
+
44
+ export type IframeBridgeAttributes = {
45
+ readonly url: string;
46
+ readonly width: number;
47
+ readonly height: number;
48
+ readonly displaySceneDir: string;
49
+ readonly lastEvent?: { name: string, payload: any };
50
+ readonly useClicker?: boolean;
51
+ readonly useSelector?: boolean;
52
+ };
53
+
54
+ export type IframeBridgeEvents = {
55
+ created: undefined;
56
+ [IframeEvents.Ready]: undefined;
57
+ [IframeEvents.StartCreate]: undefined;
58
+ [IframeEvents.OnCreate]: IframeBridge;
59
+ [IframeEvents.Destory]: undefined;
60
+ [IframeEvents.GetRootRect]: undefined;
61
+ [IframeEvents.ReplayRootRect]: DOMRect;
62
+ [DomEvents.WrapperDidMount]: undefined;
63
+ [IframeEvents.WrapperDidUpdate]: undefined;
64
+ [DomEvents.IframeLoad]: Event;
65
+ [IframeEvents.HideIframe]: undefined;
66
+ [IframeEvents.DispayIframe]: undefined;
67
+ }
68
+
69
+ export type IframeSize = {
70
+ readonly width: number;
71
+ readonly height: number;
72
+ };
73
+
74
+ type BaseOption = {
75
+ readonly url: string;
76
+ readonly width: number;
77
+ readonly height: number;
78
+ readonly displaySceneDir: string;
79
+ }
80
+
81
+ export type InsertOptions = {
82
+ readonly useClicker?: boolean;
83
+ readonly useSelector?: boolean;
84
+ } & BaseOption;
85
+
86
+ export type OnCreateInsertOption = {
87
+ readonly displayer: Displayer;
88
+ } & BaseOption;
89
+
90
+ const RefreshIDs = {
91
+ Ready: IframeEvents.Ready,
92
+ RootRect: IframeEvents.ReplayRootRect,
93
+ Message: "message",
94
+ ComputeStyle: "computeStyle",
95
+ Load: "load",
96
+ DisplayerState: "displayerState",
97
+ Show: "show",
98
+ Hide: "hide",
99
+ };
100
+
101
+ const times = <T>(number: number, iteratee: (value: number) => T) => {
102
+ return new Array(number).fill(0).map((_, index) => iteratee(index));
103
+ };
104
+
105
+ /**
106
+ * {@link https://github.com/netless-io/netless-iframe-bridge @netless/iframe-bridge}
107
+ */
108
+ export class IframeBridge {
109
+ public static readonly kind = "IframeBridge";
110
+ public static readonly hiddenClass = "netless-iframe-brdige-hidden";
111
+ public static emitter: Emittery<IframeBridgeEvents> = new Emittery();
112
+ private static displayer: Displayer | null = null
113
+ private static alreadyCreate = false;
114
+
115
+ public displayer: Displayer;
116
+ public iframe: HTMLIFrameElement;
117
+
118
+ private readonly magixEventMap = new Map<string, any>();
119
+ private cssList: string[] = [];
120
+ private allowAppliances: string[] = ["clicker"];
121
+ private bridgeDisposer: () => void = noop;
122
+ private rootRect: DOMRect | null = null;
123
+
124
+ private sideEffectManager = new SideEffectManager();
125
+
126
+ constructor(readonly manager: WindowManager, readonly appManager: AppManager) {
127
+ this.displayer = IframeBridge.displayer = appManager.displayer;
128
+
129
+ this.iframe = this._createIframe();
130
+
131
+ this.sideEffectManager.addDisposer(IframeBridge.emitter.on(IframeEvents.ReplayRootRect, rect => {
132
+ this.rootRect = rect;
133
+ }), RefreshIDs.RootRect);
134
+
135
+ this.sideEffectManager.addDisposer(IframeBridge.emitter.on(IframeEvents.HideIframe, () => {
136
+ this.iframe.className = IframeBridge.hiddenClass;
137
+ }), RefreshIDs.Hide);
138
+
139
+ this.sideEffectManager.addDisposer(IframeBridge.emitter.on(IframeEvents.DispayIframe, () => {
140
+ this.iframe.className = "";
141
+ }), RefreshIDs.Show);
142
+
143
+ this.sideEffectManager.addDisposer(IframeBridge.emitter.on("created", () => {
144
+ this.bridgeDisposer();
145
+ this.bridgeDisposer = autorun(() => {
146
+ const attributes = this.attributes;
147
+ if (attributes.url) {
148
+ const iframeSrc = this.iframe?.src;
149
+ if (iframeSrc && iframeSrc !== attributes.url) {
150
+ this.execListenIframe(attributes);
151
+ }
152
+ }
153
+ if (attributes.displaySceneDir) {
154
+ this.computedIframeDisplay(this.displayer.state, attributes);
155
+ }
156
+ if ((attributes.width || attributes.height) && this.iframe) {
157
+ this.iframe.width = `${attributes.width}px`;
158
+ this.iframe.height = `${attributes.height}px`;
159
+ }
160
+ this.postMessage({ kind: IframeEvents.AttributesUpdate, payload: attributes });
161
+ });
162
+ }));
163
+
164
+ this.sideEffectManager.addDisposer(manager.emitter.on("cameraStateChange", () => {
165
+ this.computedStyle(this.displayer.state);
166
+ }));
167
+
168
+ IframeBridge.onCreate(this);
169
+ }
170
+
171
+ public static onCreate(plugin: IframeBridge): void {
172
+ IframeBridge.emitter.emit(IframeEvents.StartCreate);
173
+ IframeBridge.emitter.emit(IframeEvents.OnCreate, plugin);
174
+ IframeBridge.emitter.emit("created");
175
+ }
176
+
177
+ public insert(options: InsertOptions): this {
178
+ const initAttributes: IframeBridgeAttributes = {
179
+ url: options.url,
180
+ width: options.width,
181
+ height: options.height,
182
+ displaySceneDir: options.displaySceneDir,
183
+ useClicker: options.useClicker || false,
184
+ useSelector: options.useSelector,
185
+ };
186
+ this.setAttributes(initAttributes);
187
+
188
+ const wrapperDidMountListener = () => {
189
+ this.getIframe()
190
+ this.listenIframe(this.attributes);
191
+ this.listenDisplayerState();
192
+ IframeBridge.emitter.emit(IframeEvents.GetRootRect);
193
+ };
194
+
195
+ if (this.getIframe()) {
196
+ wrapperDidMountListener();
197
+ }
198
+ // Code below will never be executed, just copying the old code...
199
+ else {
200
+ const didMount = this.sideEffectManager.addDisposer(IframeBridge.emitter.on(DomEvents.WrapperDidMount, () => {
201
+ wrapperDidMountListener();
202
+ this.sideEffectManager.flush(didMount);
203
+ }));
204
+ const didUpdate = this.sideEffectManager.addDisposer(IframeBridge.emitter.on(IframeEvents.WrapperDidUpdate, () => {
205
+ wrapperDidMountListener();
206
+ this.sideEffectManager.flush(didUpdate);
207
+ }));
208
+ }
209
+ if (this.attributes.useSelector) {
210
+ this.allowAppliances.push("selector");
211
+ }
212
+
213
+ this.computedStyle(this.displayer.state);
214
+ this.listenDisplayerCallbacks();
215
+ this.getComputedIframeStyle();
216
+ this.sideEffectManager.addEventListener(window, "message",
217
+ this.messageListener.bind(this), void 0, RefreshIDs.Message);
218
+
219
+ IframeBridge.alreadyCreate = true;
220
+ return this;
221
+ }
222
+
223
+ // 在某些安卓机型中会遇到 iframe 嵌套计算 bug,需要手动延迟触发一下重绘
224
+ private getComputedIframeStyle(): void {
225
+ this.sideEffectManager.setTimeout(() => {
226
+ if (this.iframe) {
227
+ getComputedStyle(this.iframe);
228
+ }
229
+ }, 200, RefreshIDs.ComputeStyle);
230
+ }
231
+
232
+ public destroy() {
233
+ this.sideEffectManager.flushAll();
234
+ IframeBridge.emitter.emit(IframeEvents.Destory);
235
+ IframeBridge.alreadyCreate = false;
236
+ IframeBridge.emitter.clearListeners();
237
+ }
238
+
239
+ private getIframe(): HTMLIFrameElement {
240
+ this.iframe || (this.iframe = this._createIframe());
241
+ return this.iframe;
242
+ }
243
+
244
+ public setIframeSize(params: IframeSize): void {
245
+ if (this.iframe) {
246
+ this.iframe.width = `${params.width}px`;
247
+ this.iframe.height = `${params.height}px`;
248
+ this.setAttributes({ width: params.width, height: params.height });
249
+ }
250
+ }
251
+
252
+ public get attributes(): Partial<IframeBridgeAttributes> {
253
+ return this.appManager.store.getIframeBridge();
254
+ }
255
+
256
+ public setAttributes(data: Partial<IframeBridgeAttributes>): void {
257
+ this.appManager.store.setIframeBridge(data);
258
+ }
259
+
260
+ private _createIframe() {
261
+ const iframe = document.createElement("iframe");
262
+ iframe.id = "IframeBridge";
263
+ iframe.className = IframeBridge.hiddenClass;
264
+ if (this.appManager.mainView.divElement) {
265
+ this.appManager.mainView.divElement.appendChild(iframe);
266
+ }
267
+ return iframe;
268
+ }
269
+
270
+ public scaleIframeToFit(animationMode: AnimationMode = AnimationMode.Immediately) {
271
+ if (!this.inDisplaySceneDir) {
272
+ return;
273
+ }
274
+ const { width = 1280, height = 720 } = this.attributes;
275
+ const x = width ? -width / 2 : 0;
276
+ const y = height ? -height / 2 : 0;
277
+
278
+ this.manager.moveCameraToContain({
279
+ originX: x,
280
+ originY: y,
281
+ width,
282
+ height,
283
+ animationMode,
284
+ });
285
+ }
286
+
287
+ public get isReplay(): boolean {
288
+ return this.manager.isReplay;
289
+ }
290
+
291
+ private handleSetPage(data: any): void {
292
+ if (this.isReplay || !this.attributes.displaySceneDir) {
293
+ return;
294
+ }
295
+ const page = data.payload;
296
+ const room = this.displayer as Room;
297
+ const scenes = room.entireScenes()[this.attributes.displaySceneDir];
298
+ if (!scenes || scenes.length !== page) {
299
+ const genScenes = times<{ name: string }>(page, (index: number) => ({ name: String(index + 1) }));
300
+ room.putScenes(this.attributes.displaySceneDir, genScenes);
301
+ this.manager.setMainViewScenePath(this.attributes.displaySceneDir);
302
+ }
303
+ }
304
+
305
+ private execListenIframe = debounce((options: Partial<IframeBridgeAttributes>) => {
306
+ this.listenIframe(options);
307
+ }, 50);
308
+
309
+ private src_url_equal_anchor?: HTMLAnchorElement
310
+ private listenIframe(options: Partial<IframeBridgeAttributes>): void {
311
+ const loadListener = (ev: Event) => {
312
+ this.postMessage({
313
+ kind: IframeEvents.Init, payload: {
314
+ attributes: this.attributes,
315
+ roomState: IframeBridge.displayer?.state,
316
+ currentPage: this.currentPage,
317
+ observerId: this.displayer.observerId
318
+ }
319
+ })
320
+ IframeBridge.emitter.emit(DomEvents.IframeLoad, ev);
321
+ this.sideEffectManager.addDisposer(IframeBridge.emitter.on(IframeEvents.Ready, () => {
322
+ this.postMessage(this.attributes.lastEvent?.payload);
323
+ }), RefreshIDs.Ready)
324
+ this.computedStyleAndIframeDisplay();
325
+ // if ((this.displayer as Room).isWritable) {
326
+ // this.manager.moveCamera({
327
+ // scale: this.manager.camera.scale + 1e-6,
328
+ // animationMode: AnimationMode.Immediately,
329
+ // })
330
+ // }
331
+ };
332
+ if (options.url && this.iframe.src !== options.url) {
333
+ if (!this.src_url_equal_anchor) this.src_url_equal_anchor = document.createElement('a');
334
+ this.src_url_equal_anchor.href = options.url
335
+ if (this.src_url_equal_anchor.href !== this.iframe.src) {
336
+ this.iframe.src = options.url;
337
+ }
338
+ }
339
+ this.iframe.width = `${options.width}px`;
340
+ this.iframe.height = `${options.height}px`;
341
+ this.sideEffectManager.addEventListener(this.iframe, "load", loadListener, void 0, RefreshIDs.Load);
342
+ }
343
+
344
+ private onPhaseChangedListener = (phase: PlayerPhase) => {
345
+ if (phase === PlayerPhase.Playing) {
346
+ this.computedStyleAndIframeDisplay();
347
+ }
348
+ }
349
+
350
+ private listenDisplayerState(): void {
351
+ if (this.isReplay) {
352
+ if ((this.displayer as any)._phase === PlayerPhase.Playing) {
353
+ this.computedStyleAndIframeDisplay();
354
+ }
355
+ this.sideEffectManager.add(() => {
356
+ this.displayer.callbacks.on("onPhaseChanged", this.onPhaseChangedListener);
357
+ return () => this.displayer.callbacks.off("onPhaseChanged", this.onPhaseChangedListener);
358
+ }, RefreshIDs.DisplayerState);
359
+ }
360
+ this.computedStyleAndIframeDisplay();
361
+ }
362
+
363
+ private computedStyleAndIframeDisplay(): void {
364
+ this.computedStyle(this.displayer.state);
365
+ this.computedIframeDisplay(this.displayer.state, this.attributes);
366
+ }
367
+
368
+ private listenDisplayerCallbacks(): void {
369
+ this.displayer.callbacks.on(this.callbackName as any, this.stateChangeListener);
370
+ }
371
+
372
+ private get callbackName(): string {
373
+ return this.isReplay ? "onPlayerStateChanged" : "onRoomStateChanged";
374
+ }
375
+
376
+ private stateChangeListener = (state: RoomState) => {
377
+ state = { ...state };
378
+ state.cameraState = this.manager.cameraState;
379
+ this.postMessage({ kind: IframeEvents.RoomStateChanged, payload: state });
380
+ if (state.cameraState) {
381
+ IframeBridge.emitter.emit(IframeEvents.GetRootRect);
382
+ this.computedStyle(state);
383
+ }
384
+ if (state.memberState) {
385
+ this.computedZindex();
386
+ this.updateStyle();
387
+ }
388
+ if (state.sceneState) {
389
+ this.computedIframeDisplay(state, this.attributes);
390
+ }
391
+ }
392
+
393
+ private computedStyle(_state: DisplayerState): void {
394
+ const cameraState = this.manager.cameraState;
395
+ const setWidth = this.attributes.width || 1280;
396
+ const setHeight = this.attributes.height || 720;
397
+ if (this.iframe) {
398
+ const { width, height, scale, centerX, centerY } = cameraState;
399
+ const rootRect = this.rootRect || { x: 0, y: 0 }
400
+ const transformOriginX = `${(width / 2) + rootRect.x}px`;
401
+ const transformOriginY = `${(height / 2) + rootRect.y}px`;
402
+ const transformOrigin = `transform-origin: ${transformOriginX} ${transformOriginY};`;
403
+ const iframeXDiff = ((width - setWidth) / 2) * scale;
404
+ const iframeYDiff = ((height - setHeight) / 2) * scale;
405
+ const x = - (centerX * scale) + iframeXDiff;
406
+ const y = - (centerY * scale) + iframeYDiff;
407
+ const transform = `transform: translate(${x}px,${y}px) scale(${scale}, ${scale});`;
408
+ const position = "position: absolute;";
409
+ // 在某些安卓机型, border-width 不为 0 时,才能正确计算 iframe 里嵌套 iframe 的大小
410
+ const borderWidth = "border: 0.1px solid rgba(0,0,0,0);";
411
+ const left = `left: 0px;`;
412
+ const top = `top: 0px;`;
413
+ const cssList = [position, borderWidth, top, left, transformOrigin, transform];
414
+ this.cssList = cssList;
415
+ this.computedZindex();
416
+ this.updateStyle();
417
+ }
418
+ }
419
+
420
+ private computedIframeDisplay(_state: DisplayerState, _attributes: Partial<IframeBridgeAttributes>): void {
421
+ if (this.inDisplaySceneDir) {
422
+ IframeBridge.emitter.emit(IframeEvents.DispayIframe);
423
+ } else {
424
+ IframeBridge.emitter.emit(IframeEvents.HideIframe);
425
+ }
426
+ }
427
+
428
+ public computedZindex(): void {
429
+ const zIndexString = "z-index: -1;";
430
+ const index = this.cssList.findIndex(css => css === zIndexString);
431
+ if (index !== -1) {
432
+ this.cssList.splice(index, 1);
433
+ }
434
+ if (!this.isClicker() || this.isDisableInput) {
435
+ this.cssList.push(zIndexString);
436
+ }
437
+ }
438
+
439
+ private updateStyle(): void {
440
+ this.iframe.style.cssText = this.cssList.join(" ");
441
+ }
442
+
443
+ private get iframeOrigin(): string | undefined {
444
+ if (this.iframe) {
445
+ try {
446
+ return new URL(this.iframe.src).origin;
447
+ } catch (err) {
448
+ console.warn(err);
449
+ }
450
+ }
451
+ }
452
+
453
+ private messageListener(event: MessageEvent): void {
454
+ log("<<<", JSON.stringify(event.data));
455
+ if (event.origin !== this.iframeOrigin) {
456
+ return;
457
+ }
458
+ const data = event.data;
459
+ switch (data.kind) {
460
+ case IframeEvents.SetAttributes: {
461
+ this.handleSetAttributes(data);
462
+ break;
463
+ }
464
+ case IframeEvents.RegisterMagixEvent: {
465
+ this.handleRegisterMagixEvent(data);
466
+ break;
467
+ }
468
+ case IframeEvents.RemoveMagixEvent: {
469
+ this.handleRemoveMagixEvent(data);
470
+ break;
471
+ }
472
+ case IframeEvents.DispatchMagixEvent: {
473
+ this.handleDispatchMagixEvent(data);
474
+ break;
475
+ }
476
+ case IframeEvents.RemoveAllMagixEvent: {
477
+ this.handleRemoveAllMagixEvent();
478
+ break;
479
+ }
480
+ case IframeEvents.NextPage: {
481
+ this.handleNextPage();
482
+ break;
483
+ }
484
+ case IframeEvents.PrevPage: {
485
+ this.handlePrevPage();
486
+ break;
487
+ }
488
+ case IframeEvents.SDKCreate: {
489
+ this.handleSDKCreate();
490
+ break;
491
+ }
492
+ case IframeEvents.SetPage: {
493
+ this.handleSetPage(data);
494
+ break;
495
+ }
496
+ case IframeEvents.GetAttributes: {
497
+ this.handleGetAttributes();
498
+ break;
499
+ }
500
+ case IframeEvents.PageTo: {
501
+ this.handlePageTo(data);
502
+ break
503
+ }
504
+ default: {
505
+ log(`${data.kind} not allow event.`);
506
+ break;
507
+ }
508
+ }
509
+ }
510
+
511
+ private handleSDKCreate(): void {
512
+ this.postMessage({
513
+ kind: IframeEvents.Init, payload: {
514
+ attributes: this.attributes,
515
+ roomState: this.displayer.state,
516
+ currentPage: this.currentPage,
517
+ observerId: this.displayer.observerId
518
+ }
519
+ });
520
+ }
521
+
522
+ private handleDispatchMagixEvent(data: any): void {
523
+ const eventPayload: { event: string, payload: any } = data.payload;
524
+ this.appManager.safeDispatchMagixEvent(eventPayload.event, eventPayload.payload);
525
+ }
526
+
527
+ private handleSetAttributes(data: any): void {
528
+ this.setAttributes(data.payload);
529
+ }
530
+
531
+ private handleRegisterMagixEvent(data: any): void {
532
+ const eventName = data.payload as string;
533
+ const listener = (event: any) => {
534
+ if (event.authorId === this.displayer.observerId) {
535
+ return;
536
+ }
537
+ this.postMessage({ kind: IframeEvents.ReciveMagixEvent, payload: event });
538
+ };
539
+ this.magixEventMap.set(eventName, listener);
540
+ this.displayer.addMagixEventListener(eventName, listener);
541
+ }
542
+
543
+ private handleRemoveMagixEvent(data: any): void {
544
+ const eventName = data.payload as string;
545
+ const listener = this.magixEventMap.get(eventName);
546
+ this.displayer.removeMagixEventListener(eventName, listener);
547
+ }
548
+
549
+ private handleNextPage(): void {
550
+ if (this.manager.canOperate) {
551
+ this.manager.nextPage();
552
+ this.dispatchMagixEvent(IframeEvents.NextPage, {});
553
+ }
554
+ }
555
+
556
+ private handlePrevPage(): void {
557
+ if (this.manager.canOperate) {
558
+ this.manager.prevPage();
559
+ this.dispatchMagixEvent(IframeEvents.PrevPage, {});
560
+ }
561
+ }
562
+
563
+ private handlePageTo(data: any): void {
564
+ if (this.manager.canOperate) {
565
+ const page = data.payload as number;
566
+ if (!Number.isSafeInteger(page) || page <= 0) {
567
+ return;
568
+ }
569
+ this.manager.setMainViewSceneIndex(page - 1);
570
+ this.dispatchMagixEvent(IframeEvents.PageTo, page - 1);
571
+ }
572
+ }
573
+
574
+ private handleRemoveAllMagixEvent(): void {
575
+ this.magixEventMap.forEach((listener, event) => {
576
+ this.displayer.removeMagixEventListener(event, listener);
577
+ });
578
+ this.magixEventMap.clear();
579
+ }
580
+
581
+ private handleGetAttributes(): void {
582
+ this.postMessage({
583
+ kind: IframeEvents.GetAttributes,
584
+ payload: this.attributes,
585
+ });
586
+ }
587
+
588
+ public postMessage(message: any): void {
589
+ if (this.iframe) {
590
+ this.iframe.contentWindow?.postMessage(JSON.parse(JSON.stringify(message)), "*");
591
+ }
592
+ }
593
+
594
+ public dispatchMagixEvent(event: string, payload: any): void {
595
+ if (this.manager.canOperate) {
596
+ this.setAttributes({ lastEvent: { name: event, payload } });
597
+ (this.displayer as Room).dispatchMagixEvent(event, payload);
598
+ }
599
+ }
600
+
601
+ private get currentIndex(): number {
602
+ return this.manager.mainViewSceneIndex;
603
+ }
604
+
605
+ private get currentPage(): number {
606
+ return this.currentIndex + 1;
607
+ }
608
+
609
+ private get totalPage(): number {
610
+ return this.manager.mainViewScenesLength;
611
+ }
612
+
613
+ private get readonly(): boolean {
614
+ return !(this.displayer as any).isWritable;
615
+ }
616
+
617
+ public get inDisplaySceneDir(): boolean {
618
+ return this.manager.mainViewSceneDir === this.attributes.displaySceneDir;
619
+ }
620
+
621
+ private isClicker(): boolean {
622
+ if (this.readonly) {
623
+ return false;
624
+ }
625
+ const currentApplianceName = (this.displayer as Room).state.memberState.currentApplianceName;
626
+ return this.allowAppliances.includes(currentApplianceName);
627
+ }
628
+
629
+ private get isDisableInput(): boolean {
630
+ if ("disableDeviceInputs" in this.displayer) {
631
+ return (this.displayer as Room).disableDeviceInputs;
632
+ } else {
633
+ return true;
634
+ }
635
+ }
636
+ }
@@ -6,7 +6,7 @@ import { emitter } from "../InternalEmitter";
6
6
  import { Fields } from "../AttributesDelegate";
7
7
  import { setViewFocusScenePath } from "../Utils/Common";
8
8
  import { SideEffectManager } from "side-effect-manager";
9
- import type { Camera, Size, View } from "white-web-sdk";
9
+ import type { Camera, Room, Size, View } from "white-web-sdk";
10
10
  import type { AppManager } from "../AppManager";
11
11
  import { Events } from "../constants";
12
12
 
@@ -223,10 +223,23 @@ export class MainViewProxy {
223
223
  this.view.callbacks.off("onSizeUpdated", this.onCameraOrSizeUpdated);
224
224
  }
225
225
 
226
+ private _syncMainViewTimer = 0;
226
227
  private onCameraOrSizeUpdated = () => {
227
228
  callbacks.emit("cameraStateChange", this.cameraState);
229
+ // sdk >= 2.16.43 的 syncMainView() 可以写入当前 main view 的 camera, 以修复复制粘贴元素的位置
230
+ // 注意到这个操作会发送信令,应当避免频繁调用
231
+ if (this.manager.room && (this.manager.room as any).syncMainView) {
232
+ clearTimeout(this._syncMainViewTimer);
233
+ this._syncMainViewTimer = setTimeout(this.syncMainView, 100, this.manager.room);
234
+ }
228
235
  };
229
236
 
237
+ private syncMainView = (room: Room) => {
238
+ if (room.isWritable) {
239
+ room.syncMainView(this.mainView);
240
+ }
241
+ }
242
+
230
243
  public moveCameraToContian(size: Size): void {
231
244
  if (!isEmpty(size)) {
232
245
  this.view.moveCameraToContain({