@netless/app-slide 0.0.26 → 0.0.30
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/dist/SlideController/index.d.ts +11 -4
- package/dist/SlideDocsViewer/index.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/main.cjs.js +221 -221
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +1883 -1283
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +153 -153
- package/dist/main.iife.js.map +1 -1
- package/dist/utils/bgcolor.d.ts +3 -0
- package/dist/utils/freezer.d.ts +21 -0
- package/dist/utils/logger.d.ts +20 -0
- package/package.json +4 -2
- package/src/SlideController/index.ts +96 -39
- package/src/SlideDocsViewer/index.ts +16 -9
- package/src/index.ts +24 -5
- package/src/utils/bgcolor.ts +45 -0
- package/src/utils/freezer.ts +69 -0
- package/src/utils/logger.ts +39 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RegisterParams } from "@netless/window-manager";
|
|
2
|
+
export interface FreezableSlide {
|
|
3
|
+
freeze: () => void;
|
|
4
|
+
unfreeze: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const FreezerLength = 2;
|
|
7
|
+
export declare const apps: {
|
|
8
|
+
map: Map<string, FreezableSlide>;
|
|
9
|
+
queue: string[];
|
|
10
|
+
validateQueue(): void;
|
|
11
|
+
set(appId: string, slide: FreezableSlide): void;
|
|
12
|
+
delete(appId: string): void;
|
|
13
|
+
focus(appId: string): void;
|
|
14
|
+
};
|
|
15
|
+
export declare type AddHooks = NonNullable<RegisterParams["addHooks"]>;
|
|
16
|
+
/**
|
|
17
|
+
* Put this function in your register code:
|
|
18
|
+
* `WindowManager.register({ addHooks })`
|
|
19
|
+
* So that it will automatically freeze the app when it is not in focus.
|
|
20
|
+
*/
|
|
21
|
+
export declare const addHooks: AddHooks;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SlideController } from "../SlideController";
|
|
2
|
+
declare class Logger {
|
|
3
|
+
enable: boolean;
|
|
4
|
+
apps: Record<string, {
|
|
5
|
+
context?: any;
|
|
6
|
+
controller?: SlideController;
|
|
7
|
+
}>;
|
|
8
|
+
level: "debug" | "verbose";
|
|
9
|
+
constructor(enable: boolean);
|
|
10
|
+
log(...args: Parameters<Console["log"]>): void;
|
|
11
|
+
verbose(...args: Parameters<Console["log"]>): void;
|
|
12
|
+
dispose(appId: string): void;
|
|
13
|
+
_onMessage: (ev: MessageEvent<{
|
|
14
|
+
slide: boolean | "__instance";
|
|
15
|
+
}>) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const logger: Logger;
|
|
18
|
+
export declare const log: (...args: Parameters<Console["log"]>) => void;
|
|
19
|
+
export declare const verbose: (...args: Parameters<Console["log"]>) => void;
|
|
20
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netless/app-slide",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"main": "dist/main.cjs.js",
|
|
5
5
|
"module": "dist/main.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
],
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@netless/app-shared": "^0.1.1",
|
|
14
|
-
"@netless/slide": "^0.1.
|
|
14
|
+
"@netless/slide": "^0.1.35",
|
|
15
|
+
"@types/color-string": "^1.5.1",
|
|
16
|
+
"color-string": "^1.7.4",
|
|
15
17
|
"side-effect-manager": "^0.1.5",
|
|
16
18
|
"vanilla-lazyload": "^17.5.0"
|
|
17
19
|
},
|
|
@@ -16,6 +16,8 @@ import type { Attributes, MagixPayload, SlideState } from "../typings";
|
|
|
16
16
|
import { SideEffectManager } from "side-effect-manager";
|
|
17
17
|
import { Slide, SLIDE_EVENTS } from "@netless/slide";
|
|
18
18
|
import { clamp, deepClone, isObj } from "../utils/helpers";
|
|
19
|
+
import { cachedGetBgColor } from "../utils/bgcolor";
|
|
20
|
+
import { logger, log, verbose } from "../utils/logger";
|
|
19
21
|
export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
|
|
20
22
|
|
|
21
23
|
export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
|
|
@@ -38,7 +40,6 @@ export interface SlideControllerOptions {
|
|
|
38
40
|
export class SlideController {
|
|
39
41
|
public readonly context: SlideControllerOptions["context"];
|
|
40
42
|
public readonly slide: Slide;
|
|
41
|
-
public readonly debug: boolean;
|
|
42
43
|
|
|
43
44
|
private readonly channel: string;
|
|
44
45
|
private readonly room?: Room;
|
|
@@ -68,13 +69,22 @@ export class SlideController {
|
|
|
68
69
|
this.channel = `channel-${context.appId}`;
|
|
69
70
|
this.room = context.getRoom();
|
|
70
71
|
this.player = this.room ? undefined : (context.getDisplayer() as Player);
|
|
71
|
-
this.debug = import.meta.env.DEV || !!context.getAppOptions()?.debug;
|
|
72
72
|
this.slide = this.createSlide(anchor);
|
|
73
73
|
// the adder does not need to sync state
|
|
74
74
|
this.syncStateOnceFlag = !this.context.isAddApp;
|
|
75
75
|
this.initialize();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
public ready = false;
|
|
79
|
+
private resolveReady!: () => void;
|
|
80
|
+
public readonly readyPromise = new Promise<void>(resolve => {
|
|
81
|
+
this.resolveReady = () =>
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
this.ready = true;
|
|
84
|
+
resolve();
|
|
85
|
+
}, 1000);
|
|
86
|
+
});
|
|
87
|
+
|
|
78
88
|
public jumpToPage(page: number) {
|
|
79
89
|
if (this.ready) {
|
|
80
90
|
page = clamp(page, 1, this.pageCount);
|
|
@@ -93,16 +103,12 @@ export class SlideController {
|
|
|
93
103
|
slide.setResource(taskId, url || DefaultUrl);
|
|
94
104
|
if (state) {
|
|
95
105
|
// if we already have state, try restore from it
|
|
96
|
-
|
|
97
|
-
console.log("[Slide] init with state", deepClone(state));
|
|
98
|
-
}
|
|
106
|
+
log("[Slide] init with state", deepClone(state));
|
|
99
107
|
this.syncStateOnceFlag = false;
|
|
100
108
|
slide.setSlideState(deepClone(state));
|
|
101
109
|
} else if (context.isAddApp) {
|
|
102
110
|
// otherwise, maybe this slide is just added, let the adder kick start first render
|
|
103
|
-
|
|
104
|
-
console.log("[Slide] init by renderSlide", 1);
|
|
105
|
-
}
|
|
111
|
+
log("[Slide] init by renderSlide", 1);
|
|
106
112
|
slide.renderSlide(1);
|
|
107
113
|
}
|
|
108
114
|
// there's still some risk that the adder is left and no first render
|
|
@@ -131,6 +137,11 @@ export class SlideController {
|
|
|
131
137
|
return disposer;
|
|
132
138
|
});
|
|
133
139
|
|
|
140
|
+
this.sideEffect.add(() => {
|
|
141
|
+
context.emitter.on("seek", this.onSeeked);
|
|
142
|
+
return () => context.emitter.off("seek", this.onSeeked);
|
|
143
|
+
});
|
|
144
|
+
|
|
134
145
|
this.sideEffect.add(() => {
|
|
135
146
|
const displayer = context.getDisplayer();
|
|
136
147
|
displayer.addMagixEventListener(this.channel, this.magixEventListener, {
|
|
@@ -149,6 +160,8 @@ export class SlideController {
|
|
|
149
160
|
slide.on(SLIDE_EVENTS.renderError, this.onError);
|
|
150
161
|
slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
|
|
151
162
|
slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
|
|
163
|
+
|
|
164
|
+
slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
private onSyncDispatch = (event: SyncEvent) => {
|
|
@@ -157,9 +170,7 @@ export class SlideController {
|
|
|
157
170
|
type: SLIDE_EVENTS.syncDispatch,
|
|
158
171
|
payload: event,
|
|
159
172
|
};
|
|
160
|
-
|
|
161
|
-
console.log("[Slide] dispatch", event);
|
|
162
|
-
}
|
|
173
|
+
log("[Slide] dispatch", event);
|
|
163
174
|
this.room.dispatchMagixEvent(this.channel, payload);
|
|
164
175
|
}
|
|
165
176
|
};
|
|
@@ -169,9 +180,7 @@ export class SlideController {
|
|
|
169
180
|
const { type, payload } = ev.payload as MagixPayload;
|
|
170
181
|
if (type === SLIDE_EVENTS.syncDispatch) {
|
|
171
182
|
this.syncStateOnce();
|
|
172
|
-
|
|
173
|
-
console.log("[Slide] receive", payload);
|
|
174
|
-
}
|
|
183
|
+
log("[Slide] receive", payload);
|
|
175
184
|
// only emit the event if the slide state is sync-ed
|
|
176
185
|
if (!this.syncStateOnceFlag) {
|
|
177
186
|
this.slide.emit(SLIDE_EVENTS.syncReceive, payload);
|
|
@@ -185,9 +194,7 @@ export class SlideController {
|
|
|
185
194
|
if (this.syncStateOnceFlag) {
|
|
186
195
|
const { state } = { ...EmptyAttributes, ...this.context.getAttributes() };
|
|
187
196
|
if (state) {
|
|
188
|
-
|
|
189
|
-
console.log("[Slide] sync with state (once)", deepClone(state));
|
|
190
|
-
}
|
|
197
|
+
log("[Slide] sync with state (once)", deepClone(state));
|
|
191
198
|
this.slide.setSlideState(deepClone(state));
|
|
192
199
|
this.syncStateOnceFlag = false;
|
|
193
200
|
}
|
|
@@ -196,40 +203,44 @@ export class SlideController {
|
|
|
196
203
|
|
|
197
204
|
private onStateChange = (state: SlideState) => {
|
|
198
205
|
if (this.context.getIsWritable()) {
|
|
199
|
-
|
|
200
|
-
console.log("[Slide] state change", JSON.stringify(state, null, 2));
|
|
201
|
-
}
|
|
206
|
+
verbose("[Slide] state change", JSON.stringify(state, null, 2));
|
|
202
207
|
this.context.updateAttributes(["state"], state);
|
|
203
208
|
}
|
|
204
209
|
};
|
|
205
210
|
|
|
206
|
-
private
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
private onSeeked = () => {
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
213
|
+
const state = this.context.getAttributes()!.state;
|
|
214
|
+
if (state) {
|
|
215
|
+
this.slide.setSlideState(deepClone(state));
|
|
216
|
+
}
|
|
217
|
+
};
|
|
210
218
|
|
|
211
219
|
private pollCount = 0;
|
|
212
220
|
private pollReadyState = () => {
|
|
213
221
|
if (this.ready) {
|
|
214
|
-
this.
|
|
222
|
+
if (this._toFreeze === 1) {
|
|
223
|
+
this.freeze();
|
|
224
|
+
} else if (this._toFreeze === -1) {
|
|
225
|
+
this.unfreeze();
|
|
226
|
+
}
|
|
215
227
|
} else if (this.pollCount < MaxPollCount) {
|
|
216
228
|
this.pollCount++;
|
|
217
229
|
setTimeout(this.pollReadyState, 500);
|
|
218
230
|
} else {
|
|
219
231
|
this.pollCount = 0;
|
|
220
|
-
|
|
221
|
-
console.log("[Slide] renderSlide (retry after timeout)", 1);
|
|
222
|
-
}
|
|
232
|
+
log("[Slide] renderSlide (retry after timeout)", 1);
|
|
223
233
|
this.slide.renderSlide(1);
|
|
224
234
|
}
|
|
225
235
|
};
|
|
226
236
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
// cache `slideCount`, because once the slide is frozen,
|
|
238
|
+
// the `slideCount` will be 0
|
|
239
|
+
private _pageCount = 0;
|
|
231
240
|
public get pageCount() {
|
|
232
|
-
return this.
|
|
241
|
+
if (this._pageCount > 0) return this._pageCount;
|
|
242
|
+
this._pageCount = this.slide.slideCount;
|
|
243
|
+
return this._pageCount;
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
public get page() {
|
|
@@ -242,18 +253,18 @@ export class SlideController {
|
|
|
242
253
|
interactive: true,
|
|
243
254
|
mode: "interactive",
|
|
244
255
|
resize: true,
|
|
245
|
-
controller:
|
|
256
|
+
controller: logger.enable,
|
|
246
257
|
renderOptions: {
|
|
247
258
|
minFPS: 25,
|
|
248
259
|
maxFPS: 30,
|
|
249
260
|
autoFPS: true,
|
|
250
261
|
autoResolution: true,
|
|
251
262
|
resolution: this.context.getAppOptions()?.resolution,
|
|
252
|
-
transactionBgColor: this.context.getAppOptions()?.bgColor ||
|
|
263
|
+
transactionBgColor: this.context.getAppOptions()?.bgColor || cachedGetBgColor(anchor),
|
|
253
264
|
},
|
|
254
265
|
timestamp: this.timestamp,
|
|
255
266
|
});
|
|
256
|
-
if (
|
|
267
|
+
if (import.meta.env.DEV) {
|
|
257
268
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
269
|
(window as any).slide = slide;
|
|
259
270
|
}
|
|
@@ -265,9 +276,7 @@ export class SlideController {
|
|
|
265
276
|
public destroy() {
|
|
266
277
|
this.sideEffect.flushAll();
|
|
267
278
|
if (!this.destroyed) {
|
|
268
|
-
|
|
269
|
-
console.log("[Slide] destroy slide (once)");
|
|
270
|
-
}
|
|
279
|
+
log("[Slide] destroy slide (once)");
|
|
271
280
|
this.slide.destroy();
|
|
272
281
|
this.destroyed = true;
|
|
273
282
|
}
|
|
@@ -282,4 +291,52 @@ export class SlideController {
|
|
|
282
291
|
return Date.now();
|
|
283
292
|
}
|
|
284
293
|
};
|
|
294
|
+
|
|
295
|
+
public isFrozen = false;
|
|
296
|
+
private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
297
|
+
private freezePromise: Promise<void> | null = null;
|
|
298
|
+
|
|
299
|
+
private afterFreeze(from: -1 | 1) {
|
|
300
|
+
if (from === 1) {
|
|
301
|
+
this.isFrozen = true;
|
|
302
|
+
this.freezePromise = null;
|
|
303
|
+
if (this._toFreeze === -1) {
|
|
304
|
+
this.unfreeze();
|
|
305
|
+
}
|
|
306
|
+
} else if (from === -1) {
|
|
307
|
+
this.isFrozen = false;
|
|
308
|
+
this.freezePromise = null;
|
|
309
|
+
if (this._toFreeze === 1) {
|
|
310
|
+
this.freeze();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
public freeze = () => {
|
|
316
|
+
if (this.ready) {
|
|
317
|
+
log("[Slide] freeze", this.context.appId);
|
|
318
|
+
if (this.freezePromise) {
|
|
319
|
+
this._toFreeze = 1;
|
|
320
|
+
} else if (!this.isFrozen) {
|
|
321
|
+
this._toFreeze = 0;
|
|
322
|
+
this.freezePromise = this.slide.frozen().then(this.afterFreeze.bind(this, 1));
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
this._toFreeze = 1;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
public unfreeze = async () => {
|
|
330
|
+
if (this.ready) {
|
|
331
|
+
log("[Slide] unfreeze", this.context.appId);
|
|
332
|
+
if (this.freezePromise) {
|
|
333
|
+
this._toFreeze = -1;
|
|
334
|
+
} else if (this.isFrozen) {
|
|
335
|
+
this._toFreeze = 0;
|
|
336
|
+
this.freezePromise = this.slide.release().then(this.afterFreeze.bind(this, -1));
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
this._toFreeze = -1;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
285
342
|
}
|
|
@@ -5,7 +5,7 @@ import { SideEffectManager } from "side-effect-manager";
|
|
|
5
5
|
import { createDocsViewerPages } from "../SlideController";
|
|
6
6
|
import { DocsViewer } from "../DocsViewer";
|
|
7
7
|
|
|
8
|
-
export const ClickThroughAppliances = new Set(["clicker"
|
|
8
|
+
export const ClickThroughAppliances = new Set(["clicker"]);
|
|
9
9
|
|
|
10
10
|
export type MountSlideOptions = Omit<SlideControllerOptions, "context" | "onPageChanged"> & {
|
|
11
11
|
onReady: () => void;
|
|
@@ -26,6 +26,7 @@ export class SlideDocsViewer {
|
|
|
26
26
|
protected readonly whiteboardView: SlideDocsViewerConfig["view"];
|
|
27
27
|
protected readonly mountSlideController: SlideDocsViewerConfig["mountSlideController"];
|
|
28
28
|
protected readonly mountWhiteboard: SlideDocsViewerConfig["mountWhiteboard"];
|
|
29
|
+
private isViewMounted = false;
|
|
29
30
|
|
|
30
31
|
public constructor({ box, view, mountSlideController, mountWhiteboard }: SlideDocsViewerConfig) {
|
|
31
32
|
this.box = box;
|
|
@@ -92,7 +93,6 @@ export class SlideDocsViewer {
|
|
|
92
93
|
if (!this.$whiteboardView) {
|
|
93
94
|
this.$whiteboardView = document.createElement("div");
|
|
94
95
|
this.$whiteboardView.className = this.wrapClassName("wb-view");
|
|
95
|
-
this.mountWhiteboard(this.$whiteboardView);
|
|
96
96
|
}
|
|
97
97
|
return this.$whiteboardView;
|
|
98
98
|
}
|
|
@@ -161,13 +161,20 @@ export class SlideDocsViewer {
|
|
|
161
161
|
protected scaleDocsToFit = () => {
|
|
162
162
|
if (this.slideController) {
|
|
163
163
|
const { width, height } = this.slideController.slide;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
if (width && height) {
|
|
165
|
+
this.whiteboardView.moveCameraToContain({
|
|
166
|
+
originX: -width / 2,
|
|
167
|
+
originY: -height / 2,
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
animationMode: "immediately" as AnimationMode.Immediately,
|
|
171
|
+
});
|
|
172
|
+
if (!this.isViewMounted) {
|
|
173
|
+
this.isViewMounted = true;
|
|
174
|
+
console.log("[Slide] mount whiteboard view");
|
|
175
|
+
this.mountWhiteboard(this.$whiteboardView);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
};
|
|
173
180
|
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { NetlessApp } from "@netless/window-manager";
|
|
|
2
2
|
import type { RoomState } from "white-web-sdk";
|
|
3
3
|
import type { MountSlideOptions } from "./SlideDocsViewer";
|
|
4
4
|
import type { Attributes } from "./typings";
|
|
5
|
+
import type { AddHooks, FreezableSlide } from "./utils/freezer";
|
|
5
6
|
|
|
6
7
|
import { SideEffectManager } from "side-effect-manager";
|
|
7
8
|
import { ensureAttributes } from "@netless/app-shared";
|
|
@@ -12,11 +13,13 @@ import {
|
|
|
12
13
|
SlideController,
|
|
13
14
|
} from "./SlideController";
|
|
14
15
|
import { SlideDocsViewer } from "./SlideDocsViewer";
|
|
16
|
+
import { apps, FreezerLength, addHooks } from "./utils/freezer";
|
|
17
|
+
import { log, logger } from "./utils/logger";
|
|
15
18
|
import styles from "./style.scss?inline";
|
|
16
19
|
|
|
17
|
-
export type { Attributes };
|
|
20
|
+
export type { Attributes, AddHooks, FreezableSlide };
|
|
18
21
|
|
|
19
|
-
export { DefaultUrl };
|
|
22
|
+
export { DefaultUrl, apps, FreezerLength, addHooks };
|
|
20
23
|
|
|
21
24
|
const SlideApp: NetlessApp<Attributes> = {
|
|
22
25
|
kind: "Slide",
|
|
@@ -46,8 +49,13 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
46
49
|
|
|
47
50
|
const onPageChanged = (page: number) => {
|
|
48
51
|
const room = context.getRoom();
|
|
49
|
-
if (docsViewer && docsViewer.slideController
|
|
50
|
-
|
|
52
|
+
if (docsViewer && docsViewer.slideController) {
|
|
53
|
+
let synced = false;
|
|
54
|
+
if (room && context.getIsWritable()) {
|
|
55
|
+
syncSceneWithSlide(room, context, docsViewer.slideController.slide, baseScenePath);
|
|
56
|
+
synced = true;
|
|
57
|
+
}
|
|
58
|
+
log("[Slide] page to", page, synced);
|
|
51
59
|
docsViewer.viewer.setPageIndex(page - 1);
|
|
52
60
|
}
|
|
53
61
|
};
|
|
@@ -58,6 +66,8 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
58
66
|
...options,
|
|
59
67
|
onPageChanged,
|
|
60
68
|
});
|
|
69
|
+
apps.set(context.appId, slideController);
|
|
70
|
+
(logger.apps[context.appId] ||= {}).controller = slideController;
|
|
61
71
|
if (import.meta.env.DEV) {
|
|
62
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
73
|
(window as any).slideController = slideController;
|
|
@@ -80,6 +90,14 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
80
90
|
|
|
81
91
|
const room = context.getRoom();
|
|
82
92
|
const sideEffect = new SideEffectManager();
|
|
93
|
+
|
|
94
|
+
sideEffect.add(() => {
|
|
95
|
+
(logger.apps[context.appId] ||= {}).context = context;
|
|
96
|
+
logger.enable = context.getAppOptions()?.debug || import.meta.env.DEV;
|
|
97
|
+
logger.level = import.meta.env.DEV ? "verbose" : "debug";
|
|
98
|
+
return () => logger.dispose(context.appId);
|
|
99
|
+
});
|
|
100
|
+
|
|
83
101
|
if (room) {
|
|
84
102
|
docsViewer.toggleClickThrough(room.state.memberState.currentApplianceName);
|
|
85
103
|
sideEffect.add(() => {
|
|
@@ -94,7 +112,8 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
context.emitter.on("destroy", () => {
|
|
97
|
-
|
|
115
|
+
log("[Slide]: destroy");
|
|
116
|
+
apps.delete(context.appId);
|
|
98
117
|
sideEffect.flushAll();
|
|
99
118
|
if (docsViewer) {
|
|
100
119
|
docsViewer.destroy();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ColorString from "color-string";
|
|
2
|
+
import { log } from "./logger";
|
|
3
|
+
|
|
4
|
+
export function guessBgColor(el: HTMLElement): string {
|
|
5
|
+
try {
|
|
6
|
+
const bg = window.getComputedStyle(el).backgroundColor;
|
|
7
|
+
if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
|
8
|
+
return bg;
|
|
9
|
+
}
|
|
10
|
+
if (el.parentElement) {
|
|
11
|
+
return guessBgColor(el.parentElement);
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// ignore any error
|
|
15
|
+
}
|
|
16
|
+
return "#ffffff";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toHex(color: string): string {
|
|
20
|
+
const result = ColorString.get(color);
|
|
21
|
+
if (result && result.model === "rgb") {
|
|
22
|
+
const args = result.value;
|
|
23
|
+
|
|
24
|
+
// https://github.com/Qix-/color-convert/blob/8dfdbbc6b46fa6a305bf394d942cc1b08e23fca5/conversions.js#L616
|
|
25
|
+
const integer =
|
|
26
|
+
((Math.round(args[0]) & 0xff) << 16) +
|
|
27
|
+
((Math.round(args[1]) & 0xff) << 8) +
|
|
28
|
+
(Math.round(args[2]) & 0xff);
|
|
29
|
+
|
|
30
|
+
const string = integer.toString(16);
|
|
31
|
+
return "#" + "000000".substring(string.length) + string;
|
|
32
|
+
} else {
|
|
33
|
+
return color;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let cachedBgColor = "";
|
|
38
|
+
|
|
39
|
+
export function cachedGetBgColor(el: HTMLElement): string {
|
|
40
|
+
if (!cachedBgColor) {
|
|
41
|
+
cachedBgColor = toHex(guessBgColor(el));
|
|
42
|
+
log("[Slide] guess bg color", cachedBgColor);
|
|
43
|
+
}
|
|
44
|
+
return cachedBgColor;
|
|
45
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { RegisterParams } from "@netless/window-manager";
|
|
2
|
+
import { log } from "./logger";
|
|
3
|
+
|
|
4
|
+
export interface FreezableSlide {
|
|
5
|
+
freeze: () => void;
|
|
6
|
+
unfreeze: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const FreezerLength = 2;
|
|
10
|
+
|
|
11
|
+
export const apps = {
|
|
12
|
+
map: new Map<string, FreezableSlide>(),
|
|
13
|
+
queue: [] as string[],
|
|
14
|
+
validateQueue() {
|
|
15
|
+
log("[Slide] freezer: validate", this.queue);
|
|
16
|
+
while (this.queue.length > FreezerLength) {
|
|
17
|
+
const appId = this.queue.pop() as string;
|
|
18
|
+
const slide = this.map.get(appId);
|
|
19
|
+
if (slide) {
|
|
20
|
+
log("[Slide] freezer: validate-freeze", appId, this.queue);
|
|
21
|
+
slide.freeze();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
set(appId: string, slide: FreezableSlide) {
|
|
26
|
+
log("[Slide] freezer: add", appId, this.queue);
|
|
27
|
+
this.map.set(appId, slide);
|
|
28
|
+
if (!this.queue.includes(appId)) {
|
|
29
|
+
this.queue.unshift(appId);
|
|
30
|
+
}
|
|
31
|
+
this.validateQueue();
|
|
32
|
+
},
|
|
33
|
+
delete(appId: string) {
|
|
34
|
+
log("[Slide] freezer: delete", appId, this.queue);
|
|
35
|
+
this.map.delete(appId);
|
|
36
|
+
this.queue = this.queue.filter(id => id !== appId);
|
|
37
|
+
},
|
|
38
|
+
focus(appId: string) {
|
|
39
|
+
const slide = this.map.get(appId);
|
|
40
|
+
const index = this.queue.indexOf(appId);
|
|
41
|
+
if (index > -1) {
|
|
42
|
+
this.queue.splice(index, 1);
|
|
43
|
+
}
|
|
44
|
+
this.queue.unshift(appId);
|
|
45
|
+
this.validateQueue();
|
|
46
|
+
log("[Slide] freezer: focus", appId, this.queue);
|
|
47
|
+
if (slide) {
|
|
48
|
+
slide.unfreeze();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (import.meta.env.DEV) {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
(window as any).freezer = apps;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AddHooks = NonNullable<RegisterParams["addHooks"]>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Put this function in your register code:
|
|
62
|
+
* `WindowManager.register({ addHooks })`
|
|
63
|
+
* So that it will automatically freeze the app when it is not in focus.
|
|
64
|
+
*/
|
|
65
|
+
export const addHooks: AddHooks = emitter => {
|
|
66
|
+
emitter.on("focus", ({ appId }) => {
|
|
67
|
+
apps.focus(appId);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SlideController } from "../SlideController";
|
|
2
|
+
import { isObj } from "./helpers";
|
|
3
|
+
|
|
4
|
+
class Logger {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
public apps: Record<string, { context?: any; controller?: SlideController }> = {};
|
|
7
|
+
public level: "debug" | "verbose" = import.meta.env.DEV ? "verbose" : "debug";
|
|
8
|
+
constructor(public enable: boolean) {
|
|
9
|
+
window.addEventListener("message", this._onMessage);
|
|
10
|
+
}
|
|
11
|
+
log(...args: Parameters<Console["log"]>) {
|
|
12
|
+
if (this.enable) {
|
|
13
|
+
console.log(...args);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
verbose(...args: Parameters<Console["log"]>) {
|
|
17
|
+
if (this.enable && this.level === "verbose") {
|
|
18
|
+
console.log(...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
dispose(appId: string) {
|
|
22
|
+
this.enable = false;
|
|
23
|
+
delete this.apps[appId];
|
|
24
|
+
window.removeEventListener("message", this._onMessage);
|
|
25
|
+
}
|
|
26
|
+
_onMessage = (ev: MessageEvent<{ slide: boolean | "__instance" }>) => {
|
|
27
|
+
if (isObj(ev.data)) {
|
|
28
|
+
if (typeof ev.data.slide === "boolean") {
|
|
29
|
+
this.enable = ev.data.slide;
|
|
30
|
+
} else if (ev.data.slide === "__instance") {
|
|
31
|
+
console.log(this);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const logger = new Logger(import.meta.env.DEV);
|
|
38
|
+
export const log = logger.log.bind(logger);
|
|
39
|
+
export const verbose = logger.verbose.bind(logger);
|