import { Whiteboard, WhiteboardApplication, WhiteboardPermissionFlag } from '@netless/forge-whiteboard'; import { ISlideConfig, Slide, SLIDE_EVENTS } from '@netless/slide'; import { AbstractApplication, kvStore } from '@netless/forge-room'; import { SlideForge } from './Slide'; import { ForgeSlidePermissionFlag, ForgeSlidePermissions } from './ForgeSlidePermession'; import { FooterView } from './FooterView'; import * as Y from 'yjs'; import { SideBarView } from './SiderBarView'; import { deepEqual } from './utils'; import { SlidePool } from './SlidePool'; export interface SlideApplicationOption { prefix: string; taskId: string; options?: ISlideConfig; /** * 从指定的白板 id 继承白板权限以及工具栏配置 */ inheritWhiteboardId?: string; } interface PreviewImage { url: string; src: string; width: number; height: number; } export const Slide_APP_NAME = 'forge_slide'; export class SlideApplication extends AbstractApplication { static applicationName = Slide_APP_NAME; static slidePool: SlidePool = new SlidePool(); public readonly name: string = Slide_APP_NAME; public readonly emitter: SlideForge = new SlideForge(); private whiteboardApp!: WhiteboardApplication; private whiteboard!: Whiteboard; private rootView: HTMLDivElement = document.createElement('div'); private contentContainer: HTMLDivElement = document.createElement('div'); private whiteboardContainer: HTMLDivElement = document.createElement('div'); private slideContainer: HTMLDivElement = document.createElement('div'); private permissions!: ForgeSlidePermissions; private footer: FooterView; private sideBar: SideBarView; private slide!: Slide; private currentSlideIndex: number = 0; private taskId: string = ''; private prefix: string = ''; private slideCount: number = 0; private lastDispatchUuid: string = ''; private syncMessageQueue: { state: any, dispatch: any }[] = []; private isSyncing: boolean = false; constructor() { super(); (window as any).emitter = this.emitter; this.rootView.setAttribute('data-forge-app', Slide_APP_NAME); this.rootView.style.background = '#f9f9fc'; this.rootView.style.overflow = 'hidden'; this.contentContainer.style.width = '100%'; this.contentContainer.style.height = '100%'; this.contentContainer.style.display = 'flex'; this.contentContainer.style.flexDirection = 'column'; this.slideContainer.style.width = '100%'; this.slideContainer.style.height = '100%'; this.footer = new FooterView(); this.sideBar = new SideBarView(); this.sideBar.on('pageChange', index => { if (index > 0 && index <= this.slideCount) { this.slide.renderSlide(index); } }); this.footer.on('prevStep', () => { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changeStep)) { return; } if (this.slide.mainSeqStep > 0 || this.slide.mainSeqState !== 'idle') { this.slide.prevStep(); } else if (this.currentSlideIndex > 1){ if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changePage)) { return; } this.slide.renderSlide(this.currentSlideIndex - 1); } }); this.footer.on('nextStep', () => { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changeStep)) { return; } if (this.slide.mainSeqStep < this.slide.mainSeqLength) { this.slide.nextStep(); } else { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changePage)) { return; } this.slide.renderSlide(this.currentSlideIndex + 1); } }); this.footer.on('prevPage', () => { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changePage)) { return; } if (this.currentSlideIndex > 1) { this.slide.renderSlide(this.currentSlideIndex - 1); } }); this.footer.on('nextPage', () => { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changePage)) { return; } if (this.currentSlideIndex < this.slideCount) { this.slide.renderSlide(this.currentSlideIndex + 1); } }); this.footer.on('sideBarToggle', () => { if (!this.permissions.hasPermission(ForgeSlidePermissionFlag.changePage)) { return; } if (this.sideBar.isShowSideBar) { this.sideBar.hidden(); } else { this.sideBar.show(); } }); this.rootView.appendChild(this.contentContainer); this.emitter.on('renderStart', pageIndex => { this.footer.prevPageState(pageIndex !== 0); }); // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; Object.defineProperty(this.emitter, 'view', { get(): any { return that.rootView; }, }); Object.defineProperty(this.emitter, 'permissions', { get(): any { return that.permissions; }, }); Object.defineProperty(this.emitter, 'footView', { get(): any { return that.footer.root; }, }); Object.defineProperty(this.emitter, 'sidebarView', { get(): any { return that.sideBar.root; }, }); Object.defineProperty(this.emitter, 'pageIndex', { get(): any { return that.currentSlideIndex; }, }); Object.defineProperty(this.emitter, 'pageCount', { get(): any { return that.slideCount; }, }); Object.defineProperty(this.emitter, 'goto', { writable: false, enumerable: false, value: (pageIndex: number) => { this.sideBar.emit('pageChange', pageIndex); }, }); Object.defineProperty(this.emitter, 'nextStep', { writable: false, enumerable: false, value: () => { this.footer.emit('nextStep'); }, }); Object.defineProperty(this.emitter, 'prevStep', { writable: false, enumerable: false, value: () => { this.footer.emit('prevStep'); }, }); Object.defineProperty(this.emitter, 'nextPage', { writable: false, enumerable: false, value: () => { this.footer.emit('nextPage'); }, }); Object.defineProperty(this.emitter, 'prevPage', { writable: false, enumerable: false, value: () => { this.footer.emit('prevPage'); }, }); Object.defineProperty(this.emitter, 'sideBarToggle', { writable: false, enumerable: false, value: () => { this.footer.emit('sideBarToggle'); }, }); Object.defineProperty(this.emitter, 'imgContent', { writable: false, enumerable: false, value: (pageIndex: number) => { return this.getImageContent(pageIndex); }, }); Object.defineProperty(this.emitter, 'imgUrl', { writable: false, enumerable: false, value: (pageIndex: number) => { return this.getImageUrl(pageIndex); }, }); Object.defineProperty(this.emitter, 'imgSize', { writable: false, enumerable: false, value: (pageIndex: number) => { return this.getImageSize(pageIndex); }, }); Object.defineProperty(this.emitter, 'frozen', { writable: false, enumerable: false, value: (callback?: () => void) => { return this.slide.frozen(callback); }, }); Object.defineProperty(this.emitter, 'release', { writable: false, enumerable: false, value: (callback?: () => void) => { return this.slide.release(callback); }, }); } private getPreviewImageUrl (pageIndex: number) { if (pageIndex < 1 || pageIndex > this.slideCount) { throw new Error('pageIndex out of range'); } return `${this.prefix}/${this.taskId}/preview/${pageIndex}.png`; } private async getImageUrl(pageIndex: number) { return this.getPreviewImageUrl(pageIndex); } private async getImageSize(pageIndex: number) { const imageUrl = this.getPreviewImageUrl(pageIndex); let preview: PreviewImage | null = null; try { const result = await kvStore.getItem(imageUrl); preview = result ? JSON.parse(result) : null; } catch (e) { console.warn('kvStore getItem error', e); } if (preview) { return { width: preview.width, height: preview.height }; } preview = await this.sideBar.getPreviewImage(imageUrl); await kvStore.setItem(imageUrl, JSON.stringify(preview)); return { width: preview.width, height: preview.height }; } private async getImageContent(pageIndex: number) { const imageUrl = this.getPreviewImageUrl(pageIndex); let preview: PreviewImage | null = null; try { const result = await kvStore.getItem(imageUrl); preview = result ? JSON.parse(result) : null; } catch (e) { console.warn('kvStore getItem error', e); } if (preview) { return preview.src; } preview = await this.sideBar.getPreviewImage(imageUrl); await kvStore.setItem(imageUrl, JSON.stringify(preview)); return preview.src; } private applySlideState = async (): Promise => { if (this.isSyncing) { requestAnimationFrame(this.applySlideState); return; } const lastSyncMessage = this.syncMessageQueue.pop(); if (!lastSyncMessage) { return; } this.isSyncing = true; this.syncMessageQueue = []; const { state, dispatch } = lastSyncMessage; let ignoreKeys: string[] | undefined = undefined; if ( dispatch.type === 'mediaPlay' || dispatch.type === 'mediaPause' || dispatch.type === 'mediaFullscreen') { ignoreKeys = [dispatch.id]; } if (this.slide.slideState.currentSlideIndex < 0 || state.currentSlideIndex < 0) { // @ts-ignore await this.slide.receiveSyncHandler(dispatch); } else if (!deepEqual(this.slide.slideState, state, ignoreKeys)) { await this.slide.setSlideState(state); // @ts-ignore await this.slide.receiveSyncHandler(dispatch); } else { this.slide.emit(SLIDE_EVENTS.syncReceive, dispatch); } this.isSyncing = false; }; private onSlideEventHandler = async (event: Y.YMapEvent) => { for (const [key, value] of event.changes.keys.entries()) { if (key === 'syncSlide') { if (value.action === 'add' || value.action === 'update') { const { slideState: slideStateFromServer, dispatch: dispatchFromServer } = this.getMap(this.name).get('syncSlide'); if (this.lastDispatchUuid === dispatchFromServer.uuid) { return; } this.lastDispatchUuid = dispatchFromServer.uuid; this.syncMessageQueue.push({ state: slideStateFromServer, dispatch: dispatchFromServer, }); this.applySlideState(); } } } }; private keyBoardEvents = (event: KeyboardEvent) => { if (event.key === 'ArrowLeft') { this.footer.emit('prevStep'); } else if (event.key === 'ArrowRight') { this.footer.emit('nextStep'); } else if (event.key === 'ArrowUp') { this.footer.emit('prevPage'); } else if (event.key === 'ArrowDown') { this.footer.emit('nextPage'); } }; private bindKeyBoardEvent() { document.addEventListener('keydown', this.keyBoardEvents); } private unbindKeyBoardEvent() { document.removeEventListener('keydown', this.keyBoardEvents); } private async onFocusInstance() { this.bindKeyBoardEvent(); await SlideApplication.slidePool.active(this.appId, this.slide); } private onRefocusInstance() { this.unbindKeyBoardEvent(); } public async initialize(option: SlideApplicationOption): Promise { this.prefix = option.prefix; this.taskId = option.taskId; const whiteboardApp = new WhiteboardApplication(); // @ts-ignore whiteboardApp.appDoc = this.appDoc; // @ts-ignore whiteboardApp.appId = `${this.appId}_wb`; // @ts-ignore whiteboardApp.userId = this.userId; // @ts-ignore whiteboardApp.userManager = this.userManager; // @ts-ignore whiteboardApp.deleteSubDoc = this.deleteSubDoc; // @ts-ignore whiteboardApp.writableManager = this.writableManager; const json = await fetch(`${option.prefix}/${option.taskId}/jsonOutput/slide-1.json`).then(res => res.json()); this.slideCount = json.slideCount; this.footer.setTotalPageIndex(this.slideCount); await whiteboardApp.initialize({ width: json.width, height: json.height, maxScaleRatio: 1, }); this.whiteboardApp = whiteboardApp; this.whiteboard = whiteboardApp.emitter; this.whiteboard.enableCameraBoundaryHighlight = false; this.whiteboard.enableCameraByMouse = false; this.whiteboard.enableCameraByTouch = false; this.whiteboard.view.style.width = '100%'; this.whiteboard.view.style.height = '100%'; this.whiteboard.view.style.position = 'absolute'; this.whiteboard.view.style.top = '0'; this.whiteboard.view.style.left = '0'; this.whiteboard.view.style.zIndex = '4'; this.whiteboard.view.classList.add('slide-whiteboard'); this.whiteboard.permissions.addPermission(WhiteboardPermissionFlag.all); this.whiteboard.setCanvasBackgroundColor('#f0f0f000'); this.whiteboardContainer.style.position = 'relative'; this.whiteboardContainer.style.flex = '0 0 auto'; this.whiteboardContainer.style.height = 'calc(100% - 24px)'; this.whiteboardContainer.classList.add('forge-slide-whiteboard-container'); this.whiteboardContainer.appendChild(this.whiteboard.view); this.whiteboardContainer.appendChild(this.slideContainer); this.contentContainer.appendChild(this.whiteboardContainer); this.contentContainer.appendChild(this.footer.root); this.contentContainer.appendChild(this.sideBar.root); if (option.inheritWhiteboardId) { whiteboardApp.linkToWhiteboard(option.inheritWhiteboardId); } this.whiteboard.setViewModeToMain(); for (let i = 0; i < json.slideCount; i++) { this.whiteboardApp.addPage(String(i), true); } this.whiteboard.setViewModeToFree(); this.slideContainer.setAttribute('builder', 'slide-builder'); await SlideApplication.slidePool.waitUntilReady(this.appId); this.slide = new Slide({ ...option.options, interactive: true, anchor: this.slideContainer, mode: 'interactive', clientId: Math.random().toString(36).substring(2, 15), timestamp: () => { console.log('timestamp'); console.log(Date.now, this.calibrationTimestamp, Date.now() - this.calibrationTimestamp); return this.calibrationTimestamp; }, // logger: { // info: console.log, // warn: console.warn, // error: console.error, // }, }); this.slide.setResource(option.taskId, option.prefix); this.sideBar.initialize(json.slideCount, option); this.slide.on(SLIDE_EVENTS.syncDispatch, event => { this.getMap(this.name).set('syncSlide', { slideState: this.slide.slideState, dispatch: event, }); }); this.slide.on(SLIDE_EVENTS.mainSeqStepStart, (animateIndex: number) => { this.emitter.emit('mainSeqStepStart', animateIndex); this.footer.changeIconToPause(); }); this.slide.on(SLIDE_EVENTS.mainSeqStepEnd, (animateIndex: number) => { this.emitter.emit('mainSeqStepEnd', animateIndex); this.footer.changeIconToNextStep(); }); this.slide.on(SLIDE_EVENTS.renderError, ({ error, index }) => { if (error.errorType === 'CANVAS_CRASH') { this.slide.renderSlide(index); } }); this.slide.on(SLIDE_EVENTS.renderStart, (slideIndex) => { if (slideIndex >= 0) { this.sideBar.pauseGetPreviewImageSchedule(); this.whiteboardApp.emitter.view.style.opacity = '0'; this.whiteboardApp.emitter.gotoPage(String(slideIndex)); this.sideBar.hidden(); this.footer.changeIconToPause(); this.emitter.emit('renderStart', slideIndex); } }); this.slide.on(SLIDE_EVENTS.renderEnd, (slideIndex) => { if (slideIndex >= 0) { this.sideBar.startGetPreviewImageSchedule(); this.currentSlideIndex = slideIndex; this.whiteboardApp.emitter.view.style.opacity = '1'; this.footer.setCurrentPageIndex(slideIndex); SlideApplication.slidePool.active(this.appId, this.slide); SlideApplication.slidePool.onRenderEnd(this.appId, this.window?.focused ?? false); this.footer.changeIconToNextStep(); this.emitter.emit('renderEnd', slideIndex); } }); this.slide.on(SLIDE_EVENTS.stateChange, (state) => { this.getMap(this.name).set('slideState', state); }); this.getMap(this.name).observe(this.onSlideEventHandler); (window as any).slide = this.slide; (window as any).slideWhiteboard = this.whiteboardApp; (window as any).forgeSlide = this; this.whiteboardContainer.addEventListener('click', () => { this.sideBar.hidden(); }, false); const syncSlide = this.getMap(this.name).get('slideState'); if (syncSlide && syncSlide.taskId === option.taskId) { this.slide.setSlideState(syncSlide); } else { this.slide.renderSlide(1); } this.permissions = new ForgeSlidePermissions(this.writableManager, this.userManager, (userId: string) => { return this.userMap(userId); }); this.permissions.on('change', (userId, flags, value) => { this.emitter.emit('permissionChange', userId, flags, value); if (this.userId === userId) { if (flags.includes(ForgeSlidePermissionFlag.clickAnim)) { this.slideContainer.style.pointerEvents = 'auto'; } else { this.slideContainer.style.pointerEvents = 'none'; } } }); this.permissions.setPermission(ForgeSlidePermissionFlag.all); this.whiteboardApp.disableViewModel(); if (this.window) { let prevStatus = 'normal'; this.window.on('statusChange', (status) => { if (prevStatus === 'minimized') { this.onFocusInstance(); prevStatus = status; return; } prevStatus = status; if (status === 'normal') { this.onFocusInstance(); } else if (status === 'minimized') { this.onRefocusInstance(); } }); this.window.on('focusedChange', (status) => { if (status) { this.onFocusInstance(); } else { this.onRefocusInstance(); } }); this.bindKeyBoardEvent(); } return Promise.resolve(undefined); } private userMap(userId: string): Y.Map { return this.getMap(`user/${userId}`); } public async dispose(removeSubDoc: boolean): Promise { if (removeSubDoc) { this.deleteSubDoc(this.appId); } await this.whiteboardApp.dispose(removeSubDoc); this.rootView.parentElement?.removeChild(this.rootView); this.slide.destroy(); this.sideBar.dispose(); this.getMap(this.name).unobserve(this.onSlideEventHandler); this.permissions.dispose(); this.footer.dispose(); SlideApplication.slidePool.remove(this.appId); } }