From 7c9001006c1c7ffc10ab0bf0fe5a8b4af85f987f Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Tue, 14 Jun 2022 13:06:13 -0400 Subject: got init views based on the current tabs open - able to remove my code in collection free form view. --- .../collectionFreeForm/CollectionFreeFormView.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5534ddd35..33474bc4b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1006,14 +1006,17 @@ export class CollectionFreeFormView extends CollectionSubView Date: Tue, 14 Jun 2022 17:03:53 -0400 Subject: got the tab to open, but can't replay the movements on the tab --- src/client/util/RecordingApi.ts | 64 +++++++++++++++------- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- 2 files changed, 44 insertions(+), 22 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts index 2b8d11818..44dc4593c 100644 --- a/src/client/util/RecordingApi.ts +++ b/src/client/util/RecordingApi.ts @@ -10,13 +10,15 @@ import { DocumentManager } from "./DocumentManager"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { Id } from "../../fields/FieldSymbols"; import { returnAll } from "../../Utils"; +import { ContextExclusionPlugin } from "webpack"; +import { DocServer } from "../DocServer"; type Movement = { time: number, panX: number, panY: number, scale: number, - docId: String, + docId: string, } @@ -88,20 +90,20 @@ export class RecordingApi { } // in the case where only one tab was changed (updates not across dashboards), set only one to true - private updateRecordingFFViewsFromTabs = (docList: Doc[], onlyOne = false) => { + private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => { if (this.recordingFFViews === null) return; // so that the size comparisons are correct, we must filter to only the FFViews const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform'; - const tabbedFFViews = new Set(); - for (const DashDoc of docList) { + const tabbedFFViews = new Set(); + for (const DashDoc of tabbedDocs) { if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]); } // new tab was added - need to add it if (tabbedFFViews.size > this.recordingFFViews.size) { - for (const DashDoc of docList) { + for (const DashDoc of tabbedDocs) { if (!this.recordingFFViews.has(DashDoc[Id])) { isFFView(DashDoc) && this.addRecordingFFView(DashDoc); @@ -184,10 +186,9 @@ export class RecordingApi { // update the presentation mode now that we are done tracking Doc.UserDoc().presentationMode = 'none'; } + // clear presenation data this.currentPresentation = RecordingApi.NULL_PRESENTATION - // clear isRecording - // this.tracking = false // clear absoluteStart this.absoluteStart = -1 } @@ -201,7 +202,7 @@ export class RecordingApi { } } - private trackMovements = (panX: number, panY: number, docId: String, scale: number = 0) => { + private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => { // ensure we are recording if (!this.tracking) { console.error('[recordingApi.ts] trackMovements(): tracking is false') @@ -281,25 +282,46 @@ export class RecordingApi { let preScale = -1; const zoomAndPan = (movement: Movement) => { const { panX, panY, scale } = movement; - (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); + (scale !== 0 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); document.Document._panX = panX; document.Document._panY = panY; preScale = scale; } - // set the first frame to be at the start of the pres - zoomAndPan(filteredMovements[0]); - - // make timers that will execute each movement at the correct replay time - this.timers = filteredMovements.map(movement => { - const timeDiff = movement.time - timeViewed * 1000 - return setTimeout(() => { - // replay the movement - zoomAndPan(movement) - // if last movement, presentation is done -> set the instance var - if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; - }, timeDiff) + // zoomAndPan(filteredMovements[0]); + + // generate a set of all unique docIds + const docIds = new Set(filteredMovements.map(movement => movement.docId)) + // TODO: optimize only ons-first load + // TODO: make async await + // TODO: make sure the cahce still hs the id + // this will load the cache, so getCachedREfFields won't have to reach server + DocServer.GetRefFields([...docIds]).then(refFields => { + console.log('refFields', refFields) + const openTab = (docId: string) => { + const isInView = DocumentManager.Instance.getDocumentViewById(docId); + if (isInView) { return isInView; } + + const doc = DocServer.GetCachedRefField(docId); + CollectionDockingView.AddSplit(doc, 'right'); + return DocumentManager.Instance.getDocumentView(doc); + } + + // make timers that will execute each movement at the correct replay time + this.timers = filteredMovements.map(movement => { + const timeDiff = movement.time - timeViewed * 1000 + return setTimeout(() => { + // open tab if it is not already open + const view = openTab(movement.docId); + // const ffView = view.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + console.log('dview', view, 'ffView', view?.props) + // replay the movement + zoomAndPan(movement); + // if last movement, presentation is done -> set the instance var + if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; + }, timeDiff) + }); }) } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 33474bc4b..a0e0f4f8d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1011,7 +1011,7 @@ export class CollectionFreeFormView extends CollectionSubView Date: Tue, 14 Jun 2022 17:48:45 -0400 Subject: done for day - get the functionality down. will make code more readable and fix some small bugs tomorrow --- src/client/util/RecordingApi.ts | 43 +++++++++++++++------- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts index 99085e658..4bbaa9a79 100644 --- a/src/client/util/RecordingApi.ts +++ b/src/client/util/RecordingApi.ts @@ -12,6 +12,7 @@ import { Id } from "../../fields/FieldSymbols"; import { returnAll } from "../../Utils"; import { ContextExclusionPlugin } from "webpack"; import { DocServer } from "../DocServer"; +import { DocumentView } from "../views/nodes/DocumentView"; type Movement = { time: number, @@ -105,10 +106,12 @@ export class RecordingApi { if (tabbedFFViews.size > this.recordingFFViews.size) { for (const DashDoc of tabbedDocs) { if (!this.recordingFFViews.has(DashDoc[Id])) { - isFFView(DashDoc) && this.addRecordingFFView(DashDoc); + if (isFFView(DashDoc)) { + this.addRecordingFFView(DashDoc); - // only one max change, so return - if (onlyOne) return; + // only one max change, so return + if (onlyOne) return; + } } } } @@ -134,6 +137,7 @@ export class RecordingApi { reaction(() => CollectionDockingView.Instance.props.Document.data, (change) => { // TODO: consider changing between dashboards + console.log('change in tabs', change); this.updateRecordingFFViewsFromTabs(DocListCast(change), true); }); @@ -197,20 +201,23 @@ export class RecordingApi { if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; } for (const [id, disposeFunc] of this.recordingFFViews) { + console.log('calling dispose func : docId', id); disposeFunc(); this.recordingFFViews.delete(id); } } private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => { - // ensure we are recording + // ensure we are recording to track if (!this.tracking) { console.error('[recordingApi.ts] trackMovements(): tracking is false') return; } - // check to see if the presetation is init + // check to see if the presetation is init - if not, we are between segments + // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres) + // bacuse tracking should be false inbetween segments high key if (this.nullPresentation) { - console.error('[recordingApi.ts] trackMovements(): no presentation') + console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments') return; } @@ -236,7 +243,7 @@ export class RecordingApi { // play movemvents will recreate them when the user resumes the presentation public pauseMovements = (): undefined | Error => { if (this.playFFView === null) { - return new Error('[recordingApi.ts] pauseMovements() failed: no view') + // return new Error('[recordingApi.ts] pauseMovements() failed: no view') } if (!this._isPlaying) { @@ -263,7 +270,7 @@ export class RecordingApi { public _isPlaying = false; public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => { - if (presentation.movements === null || this.playFFView === null) { + if (presentation.movements === null) {å //|| this.playFFView === null) { return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') } if (this._isPlaying) return; @@ -296,14 +303,20 @@ export class RecordingApi { // TODO: optimize only ons-first load // TODO: make async await // TODO: make sure the cahce still hs the id + // TODO: if they are open, set them to their first move // this will load the cache, so getCachedREfFields won't have to reach server DocServer.GetRefFields([...docIds]).then(refFields => { console.log('refFields', refFields) - const openTab = (docId: string) => { + + const openTab = (docId: string) : DocumentView | undefined => { const isInView = DocumentManager.Instance.getDocumentViewById(docId); if (isInView) { return isInView; } - const doc = DocServer.GetCachedRefField(docId); + const doc = DocServer.GetCachedRefField(docId) as Doc; + if (doc == undefined) { + console.warn('Doc server cache did not contain docId', docId) + return undefined; + } CollectionDockingView.AddSplit(doc, 'right'); return DocumentManager.Instance.getDocumentView(doc); } @@ -314,10 +327,12 @@ export class RecordingApi { return setTimeout(() => { // open tab if it is not already open const view = openTab(movement.docId); - // const ffView = view.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - const collectionFFView = view?.ComponentView; - // replay the movement - zoomAndPan(movement, collectionFFView as CollectionFreeFormView); + if (view) { + const collectionFFView = view.ComponentView; + console.log(collectionFFView instanceof CollectionFreeFormView) + // replay the movement + zoomAndPan(movement, collectionFFView as CollectionFreeFormView); + } // if last movement, presentation is done -> set the instance var if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false; }, timeDiff) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a0e0f4f8d..3c5a42b7d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1011,7 +1011,7 @@ export class CollectionFreeFormView extends CollectionSubView NumCast(this.layoutDoc._currentTimecode), time => { !this._playing && (vref.currentTime = time); - console.log("vref time = " + vref.currentTime) + // console.log("vref time = " + vref.currentTime) }, { fireImmediately: true }); } } -- cgit v1.2.3-70-g09d2 From 5972716e64fd9a006fa2139a40b03c21b503dd04 Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Wed, 15 Jun 2022 13:50:33 -0400 Subject: fix small disposer bug and reintroduce pausing on interaction --- src/client/util/RecordingApi.ts | 26 +++++++++++++--------- .../collectionFreeForm/CollectionFreeFormView.tsx | 4 +++- 2 files changed, 18 insertions(+), 12 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts index 48ea12fd9..ae5431a03 100644 --- a/src/client/util/RecordingApi.ts +++ b/src/client/util/RecordingApi.ts @@ -57,7 +57,7 @@ export class RecordingApi { this.absoluteStart = -1; // used for tracking movements in the view frame - this.recordingFFViews = null; + this.recordingFFViews = new Map(); this.tabChangeDisposeFunc = null; // for now, set playFFView @@ -126,9 +126,8 @@ export class RecordingApi { } } - public start = (meta?: Object) => { + public initTabTracker = () => { // init the dispose funcs on the page - this.recordingFFViews = new Map(); const docList = DocListCast(CollectionDockingView.Instance.props.Document.data); this.updateRecordingFFViewsFromTabs(docList); @@ -140,7 +139,9 @@ export class RecordingApi { console.log('change in tabs', change); this.updateRecordingFFViewsFromTabs(DocListCast(change), true); }); + } + public start = (meta?: Object) => { // update the presentation mode Doc.UserDoc().presentationMode = 'recording'; @@ -238,10 +239,7 @@ export class RecordingApi { // play movemvents will recreate them when the user resumes the presentation public pauseMovements = (): undefined | Error => { - if (!this._isPlaying) { - //return new Error('[recordingApi.ts] pauseMovements() failed: not playing') - return - } + if (!this._isPlaying) { console.warn('[recordingApi.ts] pauseMovements(): already on paused'); return;} this._isPlaying = false // TODO: set userdoc presentMode to browsing this.timers?.map(timer => clearTimeout(timer)) @@ -250,6 +248,7 @@ export class RecordingApi { } private videoBoxDisposeFunc: IReactionDisposer | null = null; + private videoBox: VideoBox | null = null; setVideoBox = (videoBox: VideoBox) => { console.log('setVideoBox', videoBox); @@ -258,20 +257,25 @@ export class RecordingApi { reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }), ({ playing, timeViewed }) => playing ? this.playMovements(videoBox.presentation, timeViewed) : this.pauseMovements() - ); + ); + this.videoBox = videoBox; } removeVideoBox = () => { if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; } this.videoBoxDisposeFunc(); + + this.videoBox = null; + this.videoBoxDisposeFunc = null; } // by calling pause on the VideoBox, the pauseMovements will be called - public pauseVideoAndMovements = () => { - // this.videoBox?.Pause() + public pauseFromInteraction = () => { + Doc.UserDoc().presentationMode = 'none'; + this.videoBox?.Pause(); - this.pauseMovements() + this.pauseMovements(); // return this.videoBox == null } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 3c5a42b7d..7db3b1482 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1009,7 +1009,7 @@ export class CollectionFreeFormView extends CollectionSubView will talk with Bob about using mobx to do this to remove this line of code. + if (Doc.UserDoc()?.presentationMode === 'watching') RecordingApi.Instance.pauseFromInteraction(); if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds -- cgit v1.2.3-70-g09d2 From 98fba8bdb0fe81d6f71d0ae6018fcaaf7d8897df Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Wed, 15 Jun 2022 15:39:55 -0400 Subject: Refactor RecordingApi into two main files - TrackMovements and ReplayMovements. TrackMovements is for recording presentations and ReplayMovments is for replaying them. --- src/client/util/RecordingApi.ts | 461 --------------------- src/client/util/ReplayMovements.ts | 204 +++++++++ src/client/util/TrackMovements.ts | 257 ++++++++++++ src/client/views/Main.tsx | 6 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 16 +- .../views/nodes/RecordingBox/RecordingBox.tsx | 2 +- .../views/nodes/RecordingBox/RecordingView.tsx | 10 +- src/client/views/nodes/VideoBox.tsx | 8 +- 8 files changed, 477 insertions(+), 487 deletions(-) delete mode 100644 src/client/util/RecordingApi.ts create mode 100644 src/client/util/ReplayMovements.ts create mode 100644 src/client/util/TrackMovements.ts (limited to 'src/client/views/collections') diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts deleted file mode 100644 index 87cb85497..000000000 --- a/src/client/util/RecordingApi.ts +++ /dev/null @@ -1,461 +0,0 @@ -import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; -import { IReactionDisposer, observable, observe, reaction } from "mobx"; -import { NumCast } from "../../fields/Types"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { VideoBox } from "../views/nodes/VideoBox"; -import { isArray } from "lodash"; -import { SelectionManager } from "./SelectionManager"; -import { DocumentDecorations } from "../views/DocumentDecorations"; -import { DocumentManager } from "./DocumentManager"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { Id } from "../../fields/FieldSymbols"; -import { returnAll } from "../../Utils"; -import { ContextExclusionPlugin } from "webpack"; -import { DocServer } from "../DocServer"; -import { DocumentView } from "../views/nodes/DocumentView"; - -type Movement = { - time: number, - panX: number, - panY: number, - scale: number, - docId: string, -} - - -export type Presentation = { - movements: Movement[] | null, - totalTime: number, - meta: Object | Object[], -} - -export class RecordingApi { - - private static get NULL_PRESENTATION(): Presentation { - return { movements: null, meta: {}, totalTime: -1, } - } - - // instance variables - private currentPresentation: Presentation; - private tracking: boolean; - private absoluteStart: number; - // instance variable for holding the FFViews and their disposers - private recordingFFViews: Map | null; - private tabChangeDisposeFunc: IReactionDisposer | null; - - - // create static instance and getter for global use - @observable static _instance: RecordingApi; - public static get Instance(): RecordingApi { return RecordingApi._instance } - public constructor() { - // init the global instance - RecordingApi._instance = this; - - // init the instance variables - this.currentPresentation = RecordingApi.NULL_PRESENTATION - this.tracking = false; - this.absoluteStart = -1; - - // used for tracking movements in the view frame - this.recordingFFViews = null; - this.tabChangeDisposeFunc = null; - - // for now, set playFFView - // this.playFFView = null; - this.timers = null; - } - - // little helper :) - private get nullPresentation(): boolean { - return this.currentPresentation.movements === null - } - - private addRecordingFFView(doc: Doc, key: string = doc[Id]): void { - console.info('adding dispose func : docId', key, 'doc', doc); - - if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; } - if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; } - - const disposeFunc = reaction( - () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}), - (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovements(res.x, res.y, key, res.scale), - ); - this.recordingFFViews?.set(key, disposeFunc); - } - - private removeRecordingFFView = (key: string) => { - console.info('removing dispose func : docId', key); - if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; } - this.recordingFFViews.get(key)?.(); - this.recordingFFViews.delete(key); - } - - // in the case where only one tab was changed (updates not across dashboards), set only one to true - private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => { - if (this.recordingFFViews === null) return; - - // so that the size comparisons are correct, we must filter to only the FFViews - const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform'; - const tabbedFFViews = new Set(); - for (const DashDoc of tabbedDocs) { - if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]); - } - - - // new tab was added - need to add it - if (tabbedFFViews.size > this.recordingFFViews.size) { - for (const DashDoc of tabbedDocs) { - if (!this.recordingFFViews.has(DashDoc[Id])) { - if (isFFView(DashDoc)) { - this.addRecordingFFView(DashDoc); - - // only one max change, so return - if (onlyOne) return; - } - } - } - } - // tab was removed - need to remove it from recordingFFViews - else if (tabbedFFViews.size < this.recordingFFViews.size) { - for (const [key] of this.recordingFFViews) { - if (!tabbedFFViews.has(key)) { - this.removeRecordingFFView(key); - if (onlyOne) return; - } - } - } - } - - public initTabTracker = () => { - if (this.recordingFFViews === null) { - this.recordingFFViews = new Map(); - } - - // init the dispose funcs on the page - const docList = DocListCast(CollectionDockingView.Instance.props.Document.data); - this.updateRecordingFFViewsFromTabs(docList); - - // create a reaction to monitor changes in tabs - this.tabChangeDisposeFunc = - reaction(() => CollectionDockingView.Instance.props.Document.data, - (change) => { - // TODO: consider changing between dashboards - console.log('change in tabs', change); - this.updateRecordingFFViewsFromTabs(DocListCast(change), true); - }); - } - - public start = (meta?: Object) => { - this.initTabTracker(); - - // update the presentation mode - Doc.UserDoc().presentationMode = 'recording'; - - // (1a) get start date for presenation - const startDate = new Date(); - // (1b) set start timestamp to absolute timestamp - this.absoluteStart = startDate.getTime(); - - // (2) assign meta content if it exists - this.currentPresentation.meta = meta || {} - // (3) assign start date to currentPresenation - this.currentPresentation.movements = [] - // (4) set tracking true to allow trackMovements - this.tracking = true - } - - /* stops the video and returns the presentatation; if no presentation, returns undefined */ - public yieldPresentation(clearData: boolean = true): Presentation | null { - // if no presentation or done tracking, return null - if (this.nullPresentation || !this.tracking) return null; - - // set the previus recording view to the play view - // this.playFFView = this.recordingFFView; - - // ensure we add the endTime now that they are done recording - const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; - - // reset the current presentation - clearData && this.clear(); - - console.log('yieldPresentation', cpy); - return cpy; - } - - public finish = (): void => { - // make is tracking false - this.tracking = false - // reset the RecordingApi instance - this.clear(); - } - - public clear = (): void => { - // clear the disposeFunc if we are done (not tracking) - if (!this.tracking) { - this.removeAllRecordingFFViews(); - this.tabChangeDisposeFunc?.(); - // update the presentation mode now that we are done tracking - Doc.UserDoc().presentationMode = 'none'; - - this.recordingFFViews = null; - this.tabChangeDisposeFunc = null; - } - - // clear presenation data - this.currentPresentation = RecordingApi.NULL_PRESENTATION - // clear absoluteStart - this.absoluteStart = -1 - } - - private removeAllRecordingFFViews = () => { - if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; } - - for (const [id, disposeFunc] of this.recordingFFViews) { - console.log('calling dispose func : docId', id); - disposeFunc(); - this.recordingFFViews.delete(id); - } - } - - private trackMovements = (panX: number, panY: number, docId: string, scale: number = 0) => { - // ensure we are recording to track - if (!this.tracking) { - console.error('[recordingApi.ts] trackMovements(): tracking is false') - return; - } - // check to see if the presetation is init - if not, we are between segments - // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres) - // bacuse tracking should be false inbetween segments high key - if (this.nullPresentation) { - console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments') - return; - } - - // get the time - const time = new Date().getTime() - this.absoluteStart - // make new movement object - const movement: Movement = { time, panX, panY, scale, docId } - - // add that movement to the current presentation data's movement array - this.currentPresentation.movements && this.currentPresentation.movements.push(movement) - } - - // TODO: extract this into different class with pause and resume recording - // TODO: store the FFview with the movements - private timers: NodeJS.Timeout[] | null; - - // pausing movements will dispose all timers that are planned to replay the movements - // play movemvents will recreate them when the user resumes the presentation - public pauseMovements = (): undefined | Error => { - - if (!this._isPlaying) { console.warn('[recordingApi.ts] pauseMovements(): already on paused'); return;} - this._isPlaying = false - // TODO: set userdoc presentMode to browsing - this.timers?.map(timer => clearTimeout(timer)) - - // this.videoBox = null; - } - - private videoBoxDisposeFunc: IReactionDisposer | null = null; - private videoBox: VideoBox | null = null; - - setVideoBox = async (videoBox: VideoBox) => { - console.log('setVideoBox', videoBox); - if (videoBox !== null) { console.warn('setVideoBox on already videoBox'); } - if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); } - - - const { presentation } = videoBox; - if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; } - - let docIdtoDoc: Map = new Map(); - try { - docIdtoDoc = await this.loadPresentation(presentation); - } catch { - console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements'); - throw 'error loading docs from server'; - } - - - this.videoBoxDisposeFunc = - reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }), - ({ playing, timeViewed }) => - playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements() - ); - this.videoBox = videoBox; - } - - removeVideoBox = () => { - if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; } - this.videoBoxDisposeFunc(); - - this.videoBox = null; - this.videoBoxDisposeFunc = null; - } - - - // by calling pause on the VideoBox, the pauseMovements will be called - public pauseFromInteraction = () => { - Doc.UserDoc().presentationMode = 'none'; - this.videoBox?.Pause(); - - this.pauseMovements(); - // return this.videoBox == null - } - - - - public _isPlaying = false; - - loadPresentation = async (presentation: Presentation) => { - const { movements } = presentation; - if (movements === null) { - throw '[recordingApi.ts] followMovements() failed: no presentation data'; - } - - // generate a set of all unique docIds - const docIds = new Set(); - for (const {docId} of movements) { - if (!docIds.has(docId)) docIds.add(docId); - } - - const docIdtoDoc = new Map(); - - let refFields = await DocServer.GetRefFields([...docIds.keys()]); - for (const docId in refFields) { - if (!refFields[docId]) { - throw `one field was undefined`; - } - docIdtoDoc.set(docId, refFields[docId] as Doc); - } - console.log('loadPresentation refFields', refFields, docIdtoDoc); - - return docIdtoDoc; - } - - // returns undefined if the docView isn't open on the screen - getCollectionFFView = (docId: string) => { - const isInView = DocumentManager.Instance.getDocumentViewById(docId); - if (isInView) { return isInView.ComponentView as CollectionFreeFormView; } - } - - // will open the doc in a tab then return the CollectionFFView that holds it - openTab = (docId: string, docIdtoDoc: Map) => { - const doc = docIdtoDoc.get(docId); - if (doc == undefined) { - console.error(`docIdtoDoc did not contain docId ${docId}`) - return undefined; - } - CollectionDockingView.AddSplit(doc, 'right'); - const docView = DocumentManager.Instance.getDocumentViewById(docId); - return docView?.ComponentView as CollectionFreeFormView; - } - - // helper to replay a movement - private preScale = -1; - zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => { - const { panX, panY, scale } = movement; - (scale !== 0 && this.preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); - document.Document._panX = panX; - document.Document._panY = panY; - - this.preScale = scale; - } - - getFirstMovements = (movements: Movement[], timeViewed: number): Map => { - if (movements === null) return new Map(); - // generate a set of all unique docIds - const docIdtoFirstMove = new Map(); - for (const move of movements) { - const { docId } = move; - if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move); - } - return docIdtoFirstMove; - } - - endPlayingPresentation = () => { - this.preScale = -1; - RecordingApi.Instance._isPlaying = false; - } - - public playMovements = (presentation: Presentation, docIdtoDoc: Map, timeViewed: number = 0) => { - console.log('playMovements', presentation, timeViewed, docIdtoDoc); - - if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) { - return new Error('[recordingApi.ts] followMovements() failed: no presentation data') - } - if (this._isPlaying) return; - - this._isPlaying = true; - Doc.UserDoc().presentationMode = 'watching'; - - // only get the movements that are remaining in the video time left - const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) - - const handleFirstMovements = () => { - // if the first movement is a closed tab, open it - const firstMovement = filteredMovements[0]; - const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined; - if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc); - - // for the open tabs, set it to the first move - const docIdtoFirstMove = this.getFirstMovements(filteredMovements, timeViewed); - for (const [docId, firstMove] of docIdtoFirstMove) { - const colFFView = this.getCollectionFFView(docId); - if (colFFView) this.zoomAndPan(firstMove, colFFView); - } - } - handleFirstMovements(); - - - // make timers that will execute each movement at the correct replay time - this.timers = filteredMovements.map(movement => { - const timeDiff = movement.time - timeViewed * 1000 - - return setTimeout(() => { - const collectionFFView = this.getCollectionFFView(movement.docId); - if (collectionFFView) { - this.zoomAndPan(movement, collectionFFView); - } else { - // tab wasn't open - open it and play the movement - const openedColFFView = this.openTab(movement.docId, docIdtoDoc); - openedColFFView && this.zoomAndPan(movement, openedColFFView); - } - - // if last movement, presentation is done -> cleanup :) - if (movement === filteredMovements[filteredMovements.length - 1]) { - this.endPlayingPresentation(); - } - }, timeDiff); - }); - } - - // method that concatenates an array of presentatations into one - public concatPresentations = (presentations: Presentation[]): Presentation => { - // these three will lead to the combined presentation - let combinedMovements: Movement[] = []; - let sumTime = 0; - let combinedMetas: any[] = []; - - presentations.forEach((presentation) => { - const { movements, totalTime, meta } = presentation; - - // update movements if they had one - if (movements) { - // add the summed time to the movements - const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); - // concat the movements already in the combined presentation with these new ones - combinedMovements.push(...addedTimeMovements); - } - - // update the totalTime - sumTime += totalTime; - - // concatenate the metas - combinedMetas.push(meta); - }); - - // return the combined presentation with the updated total summed time - return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; - } -} diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts new file mode 100644 index 000000000..e46810b52 --- /dev/null +++ b/src/client/util/ReplayMovements.ts @@ -0,0 +1,204 @@ +import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; +import { IReactionDisposer, observable, observe, reaction } from "mobx"; +import { Doc } from "../../fields/Doc"; +import { VideoBox } from "../views/nodes/VideoBox"; +import { DocumentManager } from "./DocumentManager"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { DocServer } from "../DocServer"; +import { Movement, Presentation } from "./TrackMovements"; + +export class ReplayMovements { + private timers: NodeJS.Timeout[] | null; + private videoBoxDisposeFunc: IReactionDisposer | null; + private videoBox: VideoBox | null; + private isPlaying: boolean; + + + // create static instance and getter for global use + @observable static _instance: ReplayMovements; + static get Instance(): ReplayMovements { return ReplayMovements._instance } + constructor() { + // init the global instance + ReplayMovements._instance = this; + + // instance vars for replaying + this.timers = null; + this.videoBoxDisposeFunc = null; + this.videoBox = null; + this.isPlaying = false; + } + + // pausing movements will dispose all timers that are planned to replay the movements + // play movemvents will recreate them when the user resumes the presentation + pauseMovements = (): undefined | Error => { + if (!this.isPlaying) { + console.warn('[recordingApi.ts] pauseMovements(): already on paused'); + return; + } + + this.isPlaying = false + // TODO: set userdoc presentMode to browsing + this.timers?.map(timer => clearTimeout(timer)) + } + + setVideoBox = async (videoBox: VideoBox) => { + console.log('setVideoBox', videoBox); + if (videoBox !== null) { console.warn('setVideoBox on already videoBox'); } + if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); } + + + const { presentation } = videoBox; + if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; } + + let docIdtoDoc: Map = new Map(); + try { + docIdtoDoc = await this.loadPresentation(presentation); + } catch { + console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements'); + throw 'error loading docs from server'; + } + + + this.videoBoxDisposeFunc = + reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }), + ({ playing, timeViewed }) => + playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements() + ); + this.videoBox = videoBox; + } + + removeVideoBox = () => { + if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; } + this.videoBoxDisposeFunc(); + + this.videoBox = null; + this.videoBoxDisposeFunc = null; + } + + // should be called from interacting with the screen + pauseFromInteraction = () => { + Doc.UserDoc().presentationMode = 'none'; + this.videoBox?.Pause(); + + this.pauseMovements(); + } + + loadPresentation = async (presentation: Presentation) => { + const { movements } = presentation; + if (movements === null) { + throw '[recordingApi.ts] followMovements() failed: no presentation data'; + } + + // generate a set of all unique docIds + const docIds = new Set(); + for (const {docId} of movements) { + if (!docIds.has(docId)) docIds.add(docId); + } + + const docIdtoDoc = new Map(); + + let refFields = await DocServer.GetRefFields([...docIds.keys()]); + for (const docId in refFields) { + if (!refFields[docId]) { + throw `one field was undefined`; + } + docIdtoDoc.set(docId, refFields[docId] as Doc); + } + console.log('loadPresentation refFields', refFields, docIdtoDoc); + + return docIdtoDoc; + } + + // returns undefined if the docView isn't open on the screen + getCollectionFFView = (docId: string) => { + const isInView = DocumentManager.Instance.getDocumentViewById(docId); + if (isInView) { return isInView.ComponentView as CollectionFreeFormView; } + } + + // will open the doc in a tab then return the CollectionFFView that holds it + openTab = (docId: string, docIdtoDoc: Map) => { + const doc = docIdtoDoc.get(docId); + if (doc == undefined) { + console.error(`docIdtoDoc did not contain docId ${docId}`) + return undefined; + } + CollectionDockingView.AddSplit(doc, 'right'); + const docView = DocumentManager.Instance.getDocumentViewById(docId); + return docView?.ComponentView as CollectionFreeFormView; + } + + // helper to replay a movement + zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => { + const { panX, panY, scale } = movement; + scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); + document.Document._panX = panX; + document.Document._panY = panY; + } + + getFirstMovements = (movements: Movement[], timeViewed: number): Map => { + if (movements === null) return new Map(); + // generate a set of all unique docIds + const docIdtoFirstMove = new Map(); + for (const move of movements) { + const { docId } = move; + if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move); + } + return docIdtoFirstMove; + } + + endPlayingPresentation = () => { + this.isPlaying = false; + } + + public playMovements = (presentation: Presentation, docIdtoDoc: Map, timeViewed: number = 0) => { + console.log('playMovements', presentation, timeViewed, docIdtoDoc); + + if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) { + return new Error('[recordingApi.ts] followMovements() failed: no presentation data') + } + if (this.isPlaying) return; + + this.isPlaying = true; + Doc.UserDoc().presentationMode = 'watching'; + + // only get the movements that are remaining in the video time left + const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) + + const handleFirstMovements = () => { + // if the first movement is a closed tab, open it + const firstMovement = filteredMovements[0]; + const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined; + if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc); + + // for the open tabs, set it to the first move + const docIdtoFirstMove = this.getFirstMovements(filteredMovements, timeViewed); + for (const [docId, firstMove] of docIdtoFirstMove) { + const colFFView = this.getCollectionFFView(docId); + if (colFFView) this.zoomAndPan(firstMove, colFFView); + } + } + handleFirstMovements(); + + + // make timers that will execute each movement at the correct replay time + this.timers = filteredMovements.map(movement => { + const timeDiff = movement.time - timeViewed * 1000 + + return setTimeout(() => { + const collectionFFView = this.getCollectionFFView(movement.docId); + if (collectionFFView) { + this.zoomAndPan(movement, collectionFFView); + } else { + // tab wasn't open - open it and play the movement + const openedColFFView = this.openTab(movement.docId, docIdtoDoc); + openedColFFView && this.zoomAndPan(movement, openedColFFView); + } + + // if last movement, presentation is done -> cleanup :) + if (movement === filteredMovements[filteredMovements.length - 1]) { + this.endPlayingPresentation(); + } + }, timeDiff); + }); + } +} diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts new file mode 100644 index 000000000..342bb440e --- /dev/null +++ b/src/client/util/TrackMovements.ts @@ -0,0 +1,257 @@ +import { IReactionDisposer, observable, observe, reaction } from "mobx"; +import { NumCast } from "../../fields/Types"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { Id } from "../../fields/FieldSymbols"; + +export type Movement = { + time: number, + panX: number, + panY: number, + scale: number, + docId: string, +} + +export type Presentation = { + movements: Movement[] | null, + totalTime: number, + meta: Object | Object[], +} + +export class TrackMovements { + + private static get NULL_PRESENTATION(): Presentation { + return { movements: null, meta: {}, totalTime: -1, } + } + + // instance variables + private currentPresentation: Presentation; + private tracking: boolean; + private absoluteStart: number; + // instance variable for holding the FFViews and their disposers + private recordingFFViews: Map | null; + private tabChangeDisposeFunc: IReactionDisposer | null; + + + // create static instance and getter for global use + @observable static _instance: TrackMovements; + static get Instance(): TrackMovements { return TrackMovements._instance } + constructor() { + // init the global instance + TrackMovements._instance = this; + + // init the instance variables + this.currentPresentation = TrackMovements.NULL_PRESENTATION + this.tracking = false; + this.absoluteStart = -1; + + // used for tracking movements in the view frame + this.recordingFFViews = null; + this.tabChangeDisposeFunc = null; + } + + // little helper :) + private get nullPresentation(): boolean { + return this.currentPresentation.movements === null + } + + private addRecordingFFView(doc: Doc, key: string = doc[Id]): void { + console.info('adding dispose func : docId', key, 'doc', doc); + + if (this.recordingFFViews === null) { console.warn('addFFView on null RecordingApi'); return; } + if (this.recordingFFViews.has(key)) { console.warn('addFFView : key already in map'); return; } + + const disposeFunc = reaction( + () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0)}), + (res) => (res.x !== -1 && res.y !== -1 && this.tracking) && this.trackMovement(res.x, res.y, key, res.scale), + ); + this.recordingFFViews?.set(key, disposeFunc); + } + + private removeRecordingFFView = (key: string) => { + console.info('removing dispose func : docId', key); + if (this.recordingFFViews === null) { console.warn('removeFFView on null RecordingApi'); return; } + this.recordingFFViews.get(key)?.(); + this.recordingFFViews.delete(key); + } + + // in the case where only one tab was changed (updates not across dashboards), set only one to true + private updateRecordingFFViewsFromTabs = (tabbedDocs: Doc[], onlyOne = false) => { + if (this.recordingFFViews === null) return; + + // so that the size comparisons are correct, we must filter to only the FFViews + const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform'; + const tabbedFFViews = new Set(); + for (const DashDoc of tabbedDocs) { + if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc[Id]); + } + + + // new tab was added - need to add it + if (tabbedFFViews.size > this.recordingFFViews.size) { + for (const DashDoc of tabbedDocs) { + if (!this.recordingFFViews.has(DashDoc[Id])) { + if (isFFView(DashDoc)) { + this.addRecordingFFView(DashDoc); + + // only one max change, so return + if (onlyOne) return; + } + } + } + } + // tab was removed - need to remove it from recordingFFViews + else if (tabbedFFViews.size < this.recordingFFViews.size) { + for (const [key] of this.recordingFFViews) { + if (!tabbedFFViews.has(key)) { + this.removeRecordingFFView(key); + if (onlyOne) return; + } + } + } + } + + private initTabTracker = () => { + if (this.recordingFFViews === null) { + this.recordingFFViews = new Map(); + } + + // init the dispose funcs on the page + const docList = DocListCast(CollectionDockingView.Instance.props.Document.data); + this.updateRecordingFFViewsFromTabs(docList); + + // create a reaction to monitor changes in tabs + this.tabChangeDisposeFunc = + reaction(() => CollectionDockingView.Instance.props.Document.data, + (change) => { + // TODO: consider changing between dashboards + console.log('change in tabs', change); + this.updateRecordingFFViewsFromTabs(DocListCast(change), true); + }); + } + + start = (meta?: Object) => { + this.initTabTracker(); + + // update the presentation mode + Doc.UserDoc().presentationMode = 'recording'; + + // (1a) get start date for presenation + const startDate = new Date(); + // (1b) set start timestamp to absolute timestamp + this.absoluteStart = startDate.getTime(); + + // (2) assign meta content if it exists + this.currentPresentation.meta = meta || {} + // (3) assign start date to currentPresenation + this.currentPresentation.movements = [] + // (4) set tracking true to allow trackMovements + this.tracking = true + } + + /* stops the video and returns the presentatation; if no presentation, returns undefined */ + yieldPresentation(clearData: boolean = true): Presentation | null { + // if no presentation or done tracking, return null + if (this.nullPresentation || !this.tracking) return null; + + // set the previus recording view to the play view + // this.playFFView = this.recordingFFView; + + // ensure we add the endTime now that they are done recording + const cpy = { ...this.currentPresentation, totalTime: new Date().getTime() - this.absoluteStart }; + + // reset the current presentation + clearData && this.clear(); + + console.log('yieldPresentation', cpy); + return cpy; + } + + finish = (): void => { + // make is tracking false + this.tracking = false + // reset the RecordingApi instance + this.clear(); + } + + private clear = (): void => { + // clear the disposeFunc if we are done (not tracking) + if (!this.tracking) { + this.removeAllRecordingFFViews(); + this.tabChangeDisposeFunc?.(); + // update the presentation mode now that we are done tracking + Doc.UserDoc().presentationMode = 'none'; + + this.recordingFFViews = null; + this.tabChangeDisposeFunc = null; + } + + // clear presenation data + this.currentPresentation = TrackMovements.NULL_PRESENTATION + // clear absoluteStart + this.absoluteStart = -1 + } + + removeAllRecordingFFViews = () => { + if (this.recordingFFViews === null) { console.warn('removeAllFFViews on null RecordingApi'); return; } + + for (const [id, disposeFunc] of this.recordingFFViews) { + console.log('calling dispose func : docId', id); + disposeFunc(); + this.recordingFFViews.delete(id); + } + } + + private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => { + // ensure we are recording to track + if (!this.tracking) { + console.error('[recordingApi.ts] trackMovements(): tracking is false') + return; + } + // check to see if the presetation is init - if not, we are between segments + // TODO: make this more clear - tracking should be "live tracking", not always true when the recording api being used (between start and yieldPres) + // bacuse tracking should be false inbetween segments high key + if (this.nullPresentation) { + console.warn('[recordingApi.ts] trackMovements(): trying to store movemetns between segments') + return; + } + + // get the time + const time = new Date().getTime() - this.absoluteStart + // make new movement object + const movement: Movement = { time, panX, panY, scale, docId } + + // add that movement to the current presentation data's movement array + this.currentPresentation.movements && this.currentPresentation.movements.push(movement) + } + + + // method that concatenates an array of presentatations into one + public concatPresentations = (presentations: Presentation[]): Presentation => { + // these three will lead to the combined presentation + let combinedMovements: Movement[] = []; + let sumTime = 0; + let combinedMetas: any[] = []; + + presentations.forEach((presentation) => { + const { movements, totalTime, meta } = presentation; + + // update movements if they had one + if (movements) { + // add the summed time to the movements + const addedTimeMovements = movements.map(move => { return { ...move, time: move.time + sumTime } }); + // concat the movements already in the combined presentation with these new ones + combinedMovements.push(...addedTimeMovements); + } + + // update the totalTime + sumTime += totalTime; + + // concatenate the metas + combinedMetas.push(meta); + }); + + // return the combined presentation with the updated total summed time + return { movements: combinedMovements, totalTime: sumTime, meta: combinedMetas }; + } +} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 517fe097c..c1b67ba19 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,7 +8,8 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Docs } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { LinkManager } from "../util/LinkManager"; -import { RecordingApi } from "../util/RecordingApi"; +import { ReplayMovements } from '../util/ReplayMovements'; +import { TrackMovements } from "../util/TrackMovements"; import { CollectionView } from "./collections/CollectionView"; import { MainView } from "./MainView"; @@ -37,6 +38,7 @@ AssignAllExtensions(); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; new LinkManager(); - new RecordingApi; + new TrackMovements(); + new ReplayMovements(); ReactDOM.render(, document.getElementById('root')); })(); \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7db3b1482..a661cebb8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -58,7 +58,7 @@ import React = require("react"); import { FieldView, FieldViewProps } from "../../nodes/FieldView"; import { InkTranscription } from "../../InkTranscription"; import e = require("connect-flash"); -import { RecordingApi } from "../../../util/RecordingApi"; +import { ReplayMovements } from "../../../util/ReplayMovements"; export const panZoomSchema = createSchema({ _panX: "number", @@ -1005,20 +1005,8 @@ export class CollectionFreeFormView extends CollectionSubView will talk with Bob about using mobx to do this to remove this line of code. - if (Doc.UserDoc()?.presentationMode === 'watching') RecordingApi.Instance.pauseFromInteraction(); + if (Doc.UserDoc()?.presentationMode === 'watching') ReplayMovements.Instance.pauseFromInteraction(); if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index a28677525..0ff7c4292 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -8,7 +8,7 @@ import { FieldView } from "../FieldView"; import { VideoBox } from "../VideoBox"; import { RecordingView } from './RecordingView'; import { DocumentType } from "../../../documents/DocumentTypes"; -import { Presentation } from "../../../util/RecordingApi"; +import { Presentation } from "../../../util/TrackMovements"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index ec9838bdd..83ed6914e 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -8,7 +8,7 @@ import { IconContext } from "react-icons"; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; -import { Presentation, RecordingApi } from '../../../util/RecordingApi'; +import { Presentation, TrackMovements } from '../../../util/TrackMovements'; export interface MediaSegment { videoChunks: any[], @@ -64,7 +64,7 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { if (finished) { // make the total presentation that'll match the concatted video - let concatPres = trackScreen && RecordingApi.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); + let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation)); // this async function uses the server to create the concatted video and then sets the result to it's accessPaths (async () => { @@ -135,7 +135,7 @@ export function RecordingView(props: IRecordingViewProps) { videoRecorder.current.onstart = (event: any) => { setRecording(true); // start the recording api when the video recorder starts - trackScreen && RecordingApi.Instance.start(); + trackScreen && TrackMovements.Instance.start(); }; videoRecorder.current.onstop = () => { @@ -149,7 +149,7 @@ export function RecordingView(props: IRecordingViewProps) { }; // depending on if a presenation exists, add it to the video - const presentation = RecordingApi.Instance.yieldPresentation(); + const presentation = TrackMovements.Instance.yieldPresentation(); setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); } @@ -174,7 +174,7 @@ export function RecordingView(props: IRecordingViewProps) { stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop()); // finish/clear the recoringApi - RecordingApi.Instance.finish(); + TrackMovements.Instance.finish(); // this will call upon progessbar to update videos to be in the correct order setFinished(true); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 34df03954..5a221fea4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -28,9 +28,9 @@ import { AnchorMenu } from "../pdf/AnchorMenu"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; -import { Presentation, RecordingApi } from "../../util/RecordingApi"; -import { List } from "../../../fields/List"; +import { Presentation } from "../../util/TrackMovements"; import { RecordingBox } from "./RecordingBox"; +import { ReplayMovements } from "../../util/ReplayMovements"; const path = require('path'); @@ -148,7 +148,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent Date: Sun, 3 Jul 2022 14:21:41 -0400 Subject: fixed contextMenu to not trigger on a collection when the contextMenu event has already been handled. --- src/client/views/collections/CollectionView.tsx | 1 + 1 file changed, 1 insertion(+) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b432104a1..83f5198a9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -173,6 +173,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent { const cm = ContextMenu.Instance; + if (e.nativeEvent.cancelBubble) return; if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.rootDoc); -- cgit v1.2.3-70-g09d2 From 3419d46a569da7ae8899588251426b82996ca523 Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 3 Jul 2022 21:02:48 -0400 Subject: from last --- src/client/views/collections/CollectionView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 83f5198a9..2ae0c01ef 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -173,7 +173,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent { const cm = ContextMenu.Instance; - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble) return; // nested calls to React to render can cause the same event to trigger in the outer view even if the inner view has handled it. This avoid CollectionDockingView menu options from being added when the event has been handled by a sub-document. if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.rootDoc); -- cgit v1.2.3-70-g09d2 From 0906eab4135db844dc45a20d33f84e7439461c9b Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 5 Jul 2022 12:33:28 -0400 Subject: fixed sidebar annos to be editable (with i-beam cursor) when selected and to be active when parent document is active. fixed previewing a sidebar annotation to not open up sidebar --- src/client/views/DocumentDecorations.tsx | 1 + src/client/views/SidebarAnnos.tsx | 1 + src/client/views/collections/CollectionStackingView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 5 +++-- 5 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 17e135689..9dc02e3f4 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -341,6 +341,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; + if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); InkStrokeProperties.Instance._lock && diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 9e939aa19..8f73ac2c3 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -137,6 +137,7 @@ export class SidebarAnnos extends React.Component { select={emptyFunction} scaling={returnOne} childShowTitle={this.showTitle} + childDocumentsActive={this.props.isContentActive} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} childHideDecorationTitle={returnTrue} removeDocument={this.removeDocument} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4e8c14039..44abbdb1c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -229,7 +229,7 @@ export class CollectionStackingView extends CollectionSubView this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)); + isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined; getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2ea976813..360a9b242 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -469,7 +469,7 @@ export class DocumentViewInternal extends DocComponent this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, !options?.instant || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing; this.props.focus(options?.docTransform ? anchor : this.rootDoc, { ...options, afterFocus: (didFocus: boolean) => diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 8e1698eba..f83fdffc9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1607,6 +1607,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp dataDoc={this.dataDoc} // usePanelWidth={true} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} showSidebar={this.SidebarShown} PanelWidth={this.sidebarWidth} setHeight={this.setSidebarHeight} @@ -1645,8 +1646,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } render() { TraceMobx(); - const selected = this.props.isSelected() || this.Document.forceActive; - const active = this.props.isContentActive(); + const active = this.props.isContentActive() || this.props.isSelected(); + const selected = active; const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = (CurrentUserUtils.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); -- cgit v1.2.3-70-g09d2