import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; import { IReactionDisposer, observable, reaction } from "mobx"; import { NumCast } from "../../fields/Types"; import { Doc } from "../../fields/Doc"; type Movement = { time: number, panX: number, panY: number, } type Presentation = { movements: Array meta: Object, startTime: number | null, } export class RecordingApi { private static NULL_PRESENTATION: Presentation = { movements: [], meta: {}, startTime: null, } // instance variables private currentPresentation: Presentation; private isRecording: boolean; private absoluteStart: number; // 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.isRecording = false; this.absoluteStart = -1; // used for tracking movements in the view frame this.disposeFunc = null; this.recordingFFView = null; // for now, set playFFView this.playFFView = null; this.timers = null; } // little helper :) private get isInitPresenation(): boolean { return this.currentPresentation.startTime === null } public start = (meta?: Object): Error | undefined => { // check if already init a presentation if (!this.isInitPresenation) { console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.') return new Error('[recordingApi.ts] start()') } // 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.startTime = startDate.getTime() // (4) set isRecording true to allow trackMovements this.isRecording = true } public clear = (): Error | Presentation => { // TODO: maybe archive the data? if (this.isRecording) { console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first') return new Error('[recordingApi.ts] clear()') } // update the presentation mode Doc.UserDoc().presentationMode = 'none' const presCopy = { ...this.currentPresentation } // clear presenation data this.currentPresentation = RecordingApi.NULL_PRESENTATION // clear isRecording this.isRecording = false // clear absoluteStart this.absoluteStart = -1 // clear the disposeFunc this.removeRecordingFFView() return presCopy; } public pause = (): Error | undefined => { if (this.isInitPresenation) { console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first') return new Error('[recordingApi.ts] pause(): no presentation') } // don't allow track movments this.isRecording = false // set relativeStart to the pausedTimestamp const timestamp = new Date().getTime() this.absoluteStart = timestamp } public resume = () => { const timestamp = new Date().getTime() const startTimestamp = this.currentPresentation.startTime if (!startTimestamp) { console.error('[recordingApi.ts] resume() failed: no presentation data. try calling start() first') return new Error('[recordingApi.ts] pause()') } // update absoluteStart to bridge the paused time const absoluteTimePaused = timestamp - this.absoluteStart this.absoluteStart = absoluteTimePaused } // public finish = (): Error | Presentation => { // if (this.isInitPresenation) { // console.error('[recordingApi.ts] finish() failed: no presentation data. try calling start() first') // return new Error('[recordingApi.ts] finish()') // } // // return a copy of the the presentation data // return { ...this.currentPresentation } // } private trackMovements = (panX: number, panY: number): Error | undefined => { // ensure we are recording if (!this.isRecording) { console.error('[recordingApi.ts] trackMovements() failed: recording is paused()') return new Error('[recordingApi.ts] trackMovements()') } // get the time const timestamp = new Date().getTime() // make new movement struct const movement: Movement = { time: timestamp, panX, panY } // add that movement to the current presentation data's movement array this.currentPresentation.movements.push(movement) } // instance variable for the FFView private disposeFunc: IReactionDisposer | null; private recordingFFView: CollectionFreeFormView | null; // set the FFView that will be used in a reaction to track the movements public setRecordingFFView = (view: CollectionFreeFormView): void => { // set the view to the current view if (view === this.recordingFFView || view === null) return; //this.recordingFFView = view; // set the reaction to track the movements this.disposeFunc = reaction( () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1) }), (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y) ) // for now, set the most recent recordingFFView to the playFFView this.recordingFFView = view; } // call on dispose function to stop tracking movements public removeRecordingFFView = (): void => { this.disposeFunc?.(); this.disposeFunc = null; } // TODO: extract this into different class with pause and resume recording private playFFView: CollectionFreeFormView | null; private timers: NodeJS.Timeout[] | null; // public loadPresentation = (presentation: Presentation): undefined | Error => { // if (presentation.startTime === null || this.playFFView === null) { // return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') // } // const document = this.playFFView.Document // const { movements } = presentation // this.timers = movements.map(movement => { // const { panX, panY, time } = movement // const t = new Timer(() => { // document._panX = panX; // document._panY = panY; // // TODO: consider cleaning this array to null or some state // }, time - presentation.startTime) // t.pause() // return t // }) // console.log(this.timers) // } public setPlayFFView = (view: CollectionFreeFormView): void => { this.playFFView = view } // public pauseMovements = (): undefined | Error => { // console.log('[recordingApi.ts] pauseMovements()') // if (this.playFFView === null) { // return new Error('[recordingApi.ts] pauseMovements() failed: no view') // } // // TODO: set userdoc presentMode to browsing // this.timers?.forEach(timer => timer.pause()) // } public pauseMovements = (): undefined | Error => { if (this.playFFView === null) { return new Error('[recordingApi.ts] pauseMovements() failed: no view') } if (!this._isPlaying) { return new Error('[recordingApi.ts] pauseMovements() failed: not playing') } // TODO: set userdoc presentMode to browsing //console.log('cleared timers', this.timers) console.log(this.timers?.map(timer => clearTimeout(timer))) console.log('[recordingApi.ts] pauseMovements()') this._isPlaying = false } // public resumeMovements = (): undefined | Error => { // if (this.playFFView === null) { // return new Error('[recordingApi.ts] resumeMovements() failed: no view') // } // console.log('resume') // this.timers?.forEach(timer => timer.resume()) // } private _isPlaying = false; public playMovements = (presentation: Presentation, timeViewed: number = 0): undefined | Error => { if (presentation.startTime === null || this.playFFView === null) { return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view') } if (this._isPlaying) { return new Error('[recordingApi.ts] playMovements() failed: already playing') } this._isPlaying = true; console.log(timeViewed) const document = this.playFFView.Document const { movements } = presentation this.timers = movements.reduce((arr: NodeJS.Timeout[], movement) => { const { panX, panY, time } = movement const absoluteTime = time - presentation.startTime - timeViewed*1000 if (absoluteTime < 0) return arr; // set the pan to what was stored arr.push(setTimeout(() => { document._panX = panX; document._panY = panY; }, absoluteTime)) return arr; }, []) console.log(this.timers.length) } // Unfinished code for tracing multiple free form views // export let pres: Map = new Map() // export function AddRecordingFFView(ffView: CollectionFreeFormView): void { // pres.set(ffView, // reaction(() => ({ x: ffView.panX, y: ffView.panY }), // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y))) // ) // } // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void { // const disposer = pres.get(ffView); // disposer?.(); // pres.delete(ffView) // } } /** Represents the `setTimeout` with an ability to perform pause/resume actions * citation: https://stackoverflow.com/questions/3969475/javascript-pause-settimeout */ class Timer { private timerId: NodeJS.Timeout | null; private callback: (...args: any[]) => void private start: number private remaining: number; constructor(callback: (...args: any[]) => void, delay: number) { this.callback = callback; this.remaining = delay; this.start = Date.now(); this.timerId = setTimeout(this.callback, this.remaining); } public pause = () => { console.log('[timer.ts] pause()') this.timerId !== null && clearTimeout(this.timerId); this.timerId = null; this.remaining -= (Date.now() - this.start); }; public resume = () => { if (this.timerId) { return; } this.start = Date.now(); this.timerId = setTimeout(this.callback, this.remaining); }; public clear = () => { console.log('[timer.ts] clear()') this.timerId !== null && clearTimeout(this.timerId); // this.timerId = null; // this.remaining -= (Date.now() - this.start); }; } // class Timer { // private _start: Date; // private _remaining: number; // private _durationTimeoutId?: NodeJS.Timeout; // private _callback: (...args: any[]) => void; // private _done = false; // get done () { // return this._done; // } // public constructor(callback: (...args: any[]) => void, ms = 0) { // this._callback = () => { // callback(); // this._done = true; // }; // this._remaining = ms; // this.resume(); // } // /** pauses the timer */ // public pause(): Timer { // if (this._durationTimeoutId && !this._done) { // this._clearTimeoutRef(); // this._remaining -= new Date().getTime() - this._start.getTime(); // } // return this; // } // /** resumes the timer */ // public resume(): Timer { // if (!this._durationTimeoutId && !this._done) { // this._start = new Date; // this._durationTimeoutId = setTimeout(this._callback, this._remaining); // } // return this; // } // /** // * clears the timeout and marks it as done. // * // * After called, the timeout will not resume // */ // public clearTimeout() { // this._clearTimeoutRef(); // this._done = true; // } // private _clearTimeoutRef() { // if (this._durationTimeoutId) { // clearTimeout(this._durationTimeoutId); // this._durationTimeoutId = undefined; // } // } // }