@netless/app-presentation 0.1.0 → 0.1.2
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/index.d.ts +27 -3
- package/dist/index.global.js +114 -55
- package/dist/index.js +114 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +114 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/src/app-presentation.ts +93 -27
- package/src/presentation.ts +15 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netless/app-presentation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Netless App for presentation slides (images)",
|
|
5
5
|
"author": "netless",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
"@hyrious/configs": "^0.1.1",
|
|
28
28
|
"@hyrious/dts": "^0.2.1",
|
|
29
29
|
"@hyrious/esbuild-dev": "^0.10.5",
|
|
30
|
-
"@netless/fastboard": "^0.3.
|
|
31
|
-
"@netless/window-manager": "^0.4.
|
|
32
|
-
"@types/node": "^20.11.
|
|
33
|
-
"@wopjs/disposable": "^0.1.
|
|
30
|
+
"@netless/fastboard": "^0.3.14",
|
|
31
|
+
"@netless/window-manager": "^0.4.70",
|
|
32
|
+
"@types/node": "^20.11.30",
|
|
33
|
+
"@wopjs/disposable": "^0.1.5",
|
|
34
34
|
"@wopjs/dom": "^0.1.3",
|
|
35
|
-
"esbuild": "^0.
|
|
35
|
+
"esbuild": "^0.20.2",
|
|
36
36
|
"jspdf": "^2.5.1",
|
|
37
|
-
"rollup": "^4.
|
|
38
|
-
"sass": "^1.
|
|
39
|
-
"vanilla-lazyload": "^
|
|
40
|
-
"vite": "^5.
|
|
37
|
+
"rollup": "^4.13.1",
|
|
38
|
+
"sass": "^1.72.0",
|
|
39
|
+
"vanilla-lazyload": "^18.0.0",
|
|
40
|
+
"vite": "^5.2.6"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"dev": "vite",
|
package/src/app-presentation.ts
CHANGED
|
@@ -8,13 +8,32 @@ import { Presentation, type PresentationConfig, type PresentationPage } from "./
|
|
|
8
8
|
|
|
9
9
|
export type Logger = (...data: any[]) => void
|
|
10
10
|
|
|
11
|
+
interface Viewport {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export interface PresentationAppOptions {
|
|
12
19
|
/** Disables user move / scale the image and whiteboard. */
|
|
13
20
|
disableCameraTransform?: boolean;
|
|
14
21
|
/** Max scale = `maxCameraScale` * default scale. Not working when `disableCameraTransform` is true. Default: 3 */
|
|
15
22
|
maxCameraScale?: number;
|
|
16
|
-
/** Custom logger. Default:
|
|
23
|
+
/** Custom logger. Default: a logger that reports to the whiteboard server. */
|
|
17
24
|
log?: Logger;
|
|
25
|
+
/** Custom thumbnail generator. Default is appending `"?x-oss-process=image/resize,l_50"` to `src`. */
|
|
26
|
+
thumbnail?: (src: string) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Custom viewport to set on the first time the presentation was added. Default is full page.
|
|
29
|
+
* Numbers range in 0 to 1 is considered a ratio to multiply the real page size.
|
|
30
|
+
* Example settings:
|
|
31
|
+
*
|
|
32
|
+
* - Full page: `{ x: 0, y: 0, width: 1, height: 1 }`
|
|
33
|
+
* - Half page: `{ x: 0, y: 0, width: 1, height: 0.5 }`
|
|
34
|
+
* - Absolute top-left area of the page: `{ x: 0, y: 0, width: 100, height: 100 }`
|
|
35
|
+
*/
|
|
36
|
+
viewport?: Viewport | ((page: PresentationPage) => Viewport);
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
export interface PresentationController {
|
|
@@ -48,7 +67,7 @@ const createLogger = (room: Room | undefined): Logger => {
|
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
export const NetlessAppPresentation: NetlessApp<{},
|
|
70
|
+
export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions, PresentationController> = {
|
|
52
71
|
kind: "Presentation",
|
|
53
72
|
setup(context) {
|
|
54
73
|
const view = context.getView()
|
|
@@ -66,8 +85,10 @@ export const NetlessAppPresentation: NetlessApp<{}, unknown, PresentationAppOpti
|
|
|
66
85
|
const scenePath = context.getInitScenePath()!
|
|
67
86
|
|
|
68
87
|
const options = context.getAppOptions() || {}
|
|
69
|
-
|
|
88
|
+
let maxCameraScale = options.maxCameraScale ?? 3
|
|
89
|
+
if (!(Number.isFinite(maxCameraScale) && maxCameraScale! > 0)) {
|
|
70
90
|
console.warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`)
|
|
91
|
+
maxCameraScale = 3
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
const log = options.log || createLogger(context.getRoom())
|
|
@@ -96,6 +117,7 @@ export const NetlessAppPresentation: NetlessApp<{}, unknown, PresentationAppOpti
|
|
|
96
117
|
dispose.add(() => log(`[Presentation] dispose ${context.appId}`))
|
|
97
118
|
|
|
98
119
|
const page$$ = context.createStorage('page', { index: 0 })
|
|
120
|
+
const view$$ = context.createStorage('view', { uid: "", originX: 0, originY: 0, width: 0, height: 0 })
|
|
99
121
|
|
|
100
122
|
let lastIndex = -1
|
|
101
123
|
const syncPage = async (index: number) => {
|
|
@@ -159,7 +181,7 @@ export const NetlessAppPresentation: NetlessApp<{}, unknown, PresentationAppOpti
|
|
|
159
181
|
originX: -width / 2, originY: -height / 2, width, height,
|
|
160
182
|
animationMode: 'immediately' as AnimationMode.Immediately
|
|
161
183
|
})
|
|
162
|
-
const maxScale = view.camera.scale * (options.disableCameraTransform ? 1 :
|
|
184
|
+
const maxScale = view.camera.scale * (options.disableCameraTransform ? 1 : maxCameraScale)
|
|
163
185
|
const minScale = view.camera.scale
|
|
164
186
|
view.setCameraBound({
|
|
165
187
|
damping: 1,
|
|
@@ -167,32 +189,51 @@ export const NetlessAppPresentation: NetlessApp<{}, unknown, PresentationAppOpti
|
|
|
167
189
|
minContentMode: () => minScale,
|
|
168
190
|
centerX: 0, centerY: 0, width, height
|
|
169
191
|
})
|
|
192
|
+
syncViewFromRemote(true)
|
|
170
193
|
}
|
|
171
194
|
}
|
|
172
195
|
|
|
173
|
-
|
|
174
|
-
dispose.add(page$$.addStateChangedListener(() => syncPage(page$$.state.index)))
|
|
196
|
+
const me = context.getRoom()?.uid || context.getDisplayer().observerId + ''
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
let throttleSyncView = 0
|
|
199
|
+
const syncView = () => {
|
|
200
|
+
if (throttleSyncView > 0) return
|
|
201
|
+
const { width, height } = app.page() || {}
|
|
202
|
+
if (width && height && context.getIsWritable()) {
|
|
203
|
+
const { camera, size } = view
|
|
204
|
+
const fixedW = Math.min(size.width, size.height * width / height)
|
|
205
|
+
const fixedH = Math.min(size.height, size.width * height / width)
|
|
206
|
+
const w = fixedW / camera.scale
|
|
207
|
+
const h = fixedH / camera.scale
|
|
208
|
+
const x = camera.centerX - w / 2
|
|
209
|
+
const y = camera.centerY - h / 2
|
|
210
|
+
throttleSyncView = setTimeout(() => {
|
|
211
|
+
throttleSyncView = 0
|
|
212
|
+
view$$.setState({ uid: me, originX: x, originY: y, width: w, height: h })
|
|
213
|
+
}, 50)
|
|
191
214
|
}
|
|
192
|
-
}
|
|
215
|
+
}
|
|
216
|
+
dispose.add(() => {
|
|
217
|
+
clearTimeout(throttleSyncView)
|
|
218
|
+
throttleSyncView = 0
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const syncViewFromRemote = (force = false, animate = false) => {
|
|
222
|
+
const { uid, originX, originY, width, height } = view$$.state
|
|
223
|
+
if ((force || uid !== me) && width > 0 && height > 0) {
|
|
224
|
+
view.moveCameraToContain({
|
|
225
|
+
originX, originY, width, height,
|
|
226
|
+
animationMode: (animate ? 'continuous' : 'immediately') as AnimationMode
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
syncPage(page$$.state.index)
|
|
232
|
+
dispose.add(page$$.addStateChangedListener(() => syncPage(page$$.state.index)))
|
|
233
|
+
dispose.add(view$$.addStateChangedListener(() => syncViewFromRemote(false, true)))
|
|
193
234
|
|
|
194
235
|
const box = context.getBox()
|
|
195
|
-
const app = dispose.add(createPresentation(box, pages, jumpPage, page
|
|
236
|
+
const app = dispose.add(createPresentation(box, pages, jumpPage, page$$, options.thumbnail))
|
|
196
237
|
app.contentDOM.dataset.appPresentationVersion = __VERSION__
|
|
197
238
|
app.scaleDocsToFit = scaleDocsToFit
|
|
198
239
|
app.log = log
|
|
@@ -207,6 +248,26 @@ export const NetlessAppPresentation: NetlessApp<{}, unknown, PresentationAppOpti
|
|
|
207
248
|
return () => view.callbacks.off('onSizeUpdated', scaleDocsToFit)
|
|
208
249
|
})
|
|
209
250
|
|
|
251
|
+
// Init viewport if provided `viewport`.
|
|
252
|
+
if (options.viewport && context.isAddApp && app.page()) {
|
|
253
|
+
const page = app.page()!
|
|
254
|
+
const viewport = typeof options.viewport === 'function' ? options.viewport(page) : options.viewport
|
|
255
|
+
const fix = (i: number, x: number) => i == 0 ? i : i <= 1 ? i * x : i
|
|
256
|
+
view$$.setState({
|
|
257
|
+
uid: me,
|
|
258
|
+
originX: fix(viewport.x, page.width) - page.width / 2,
|
|
259
|
+
originY: fix(viewport.y, page.height) - page.height / 2,
|
|
260
|
+
width: fix(viewport.width, page.width),
|
|
261
|
+
height: fix(viewport.height, page.height),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
dispose.make(() => {
|
|
266
|
+
view.callbacks.on('onCameraUpdatedByDevice', syncView)
|
|
267
|
+
return () => view.callbacks.off('onCameraUpdatedByDevice', syncView)
|
|
268
|
+
})
|
|
269
|
+
syncViewFromRemote(true)
|
|
270
|
+
|
|
210
271
|
dispose.add(context.emitter.on("writableChange", (isWritable: boolean): void => {
|
|
211
272
|
app.setReadonly(!isWritable)
|
|
212
273
|
}))
|
|
@@ -366,15 +427,16 @@ class AppPresentation extends Presentation {
|
|
|
366
427
|
}
|
|
367
428
|
}
|
|
368
429
|
|
|
369
|
-
|
|
430
|
+
function createPresentation(
|
|
370
431
|
box: ReadonlyTeleBox,
|
|
371
432
|
pages: PresentationPage[],
|
|
372
433
|
jumpPage: (index: number) => void,
|
|
373
434
|
page$$: Storage<{ index: number }>,
|
|
374
|
-
) =>
|
|
435
|
+
thumbnail?: (src: string) => string,
|
|
436
|
+
): AppPresentation {
|
|
375
437
|
box.mountStyles(styles)
|
|
376
438
|
|
|
377
|
-
const app = new AppPresentation({ pages, readonly: box.readonly, jumpPage })
|
|
439
|
+
const app = new AppPresentation({ pages, readonly: box.readonly, jumpPage, thumbnail })
|
|
378
440
|
app.box = box
|
|
379
441
|
box.mountContent(app.contentDOM)
|
|
380
442
|
box.mountFooter(app.footerDOM)
|
|
@@ -397,6 +459,10 @@ export interface InstallOptions {
|
|
|
397
459
|
* @example "DocsViewer"
|
|
398
460
|
*/
|
|
399
461
|
as?: string
|
|
462
|
+
/**
|
|
463
|
+
* Options to customize the local app (not synced to others).
|
|
464
|
+
*/
|
|
465
|
+
appOptions?: PresentationAppOptions
|
|
400
466
|
}
|
|
401
467
|
|
|
402
468
|
/**
|
|
@@ -410,5 +476,5 @@ export const install = (register: RegisterFn, options: InstallOptions = {}): Pro
|
|
|
410
476
|
if (options.as) {
|
|
411
477
|
app = Object.assign({}, app, { kind: options.as })
|
|
412
478
|
}
|
|
413
|
-
return register({ kind: app.kind, src: app })
|
|
479
|
+
return register({ kind: app.kind, src: app, appOptions: options.appOptions })
|
|
414
480
|
}
|
package/src/presentation.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface PresentationPage {
|
|
|
16
16
|
export interface PresentationConfig {
|
|
17
17
|
readonly pages: PresentationPage[]
|
|
18
18
|
readonly readonly?: boolean
|
|
19
|
+
readonly thumbnail?: (src: string) => string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -76,6 +77,7 @@ export class Presentation implements IDisposable<void> {
|
|
|
76
77
|
constructor(config: PresentationConfig) {
|
|
77
78
|
this.pages = config.pages
|
|
78
79
|
this.preload = new Preload(this.pages)
|
|
80
|
+
if (config.thumbnail) this.thumbnail = config.thumbnail
|
|
79
81
|
this.dispose.add(this.preload)
|
|
80
82
|
this.readonly = config.readonly ?? false
|
|
81
83
|
this.dom = document.createElement('div')
|
|
@@ -120,7 +122,7 @@ export class Presentation implements IDisposable<void> {
|
|
|
120
122
|
const c_previewPageName = this.c('preview-page-name')
|
|
121
123
|
for (let index = 0; index < this.pages.length; ++index) {
|
|
122
124
|
const page = this.pages[index]
|
|
123
|
-
const previewSRC = page.thumbnail || this.
|
|
125
|
+
const previewSRC = page.thumbnail || this.thumbnail(page.src)
|
|
124
126
|
|
|
125
127
|
const previewPage = document.createElement('a')
|
|
126
128
|
previewPage.className = `${c_previewPage} ${this.c(`preview-page-${index}`)}`
|
|
@@ -294,17 +296,8 @@ export class Presentation implements IDisposable<void> {
|
|
|
294
296
|
this.preload.touch(this.pageIndex)
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private isEditable(el: EventTarget | null): boolean {
|
|
302
|
-
if (!el) return false
|
|
303
|
-
const { tagName } = el as HTMLElement
|
|
304
|
-
return tagName == 'INPUT' || tagName == 'TEXTAREA' || tagName == 'SELECT'
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private x_oss_process(src: string): string {
|
|
299
|
+
/** Returns a modified URL that points to the thumbnail image. */
|
|
300
|
+
thumbnail(src: string): string {
|
|
308
301
|
try {
|
|
309
302
|
const url = new URL(src)
|
|
310
303
|
url.searchParams.set('x-oss-process', 'image/resize,l_50')
|
|
@@ -314,4 +307,14 @@ export class Presentation implements IDisposable<void> {
|
|
|
314
307
|
return src
|
|
315
308
|
}
|
|
316
309
|
}
|
|
310
|
+
|
|
311
|
+
private c(className: string): string {
|
|
312
|
+
return `${this.namespace}-${className}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private isEditable(el: EventTarget | null): boolean {
|
|
316
|
+
if (!el) return false
|
|
317
|
+
const { tagName } = el as HTMLElement
|
|
318
|
+
return tagName == 'INPUT' || tagName == 'TEXTAREA' || tagName == 'SELECT'
|
|
319
|
+
}
|
|
317
320
|
}
|