aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/ReplayMovements.ts
blob: d99630f8285dbdbb456d38663b346426eff91d0f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import { IReactionDisposer, observable, reaction } from 'mobx';
import { Doc, IdToDoc } from '../../fields/Doc';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
import { OpenWhereMod } from '../views/nodes/DocumentView';
import { VideoBox } from '../views/nodes/VideoBox';
import { DocumentManager } from './DocumentManager';
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;
        }
        Doc.UserDoc().presentationMode = 'none';

        this.isPlaying = false;
        // TODO: set userdoc presentMode to browsing
        this.timers?.map(timer => clearTimeout(timer));
    };

    setVideoBox = async (videoBox: VideoBox) => {
        // console.info('setVideoBox', videoBox);
        if (this.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;
        }

        this.loadPresentation(presentation);

        this.videoBoxDisposeFunc = reaction(
            () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
            ({ playing, timeViewed }) => (playing ? this.playMovements(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;
    };

    // should be called from interacting with the screen
    pauseFromInteraction = () => {
        this.videoBox?.Pause();

        this.pauseMovements();
    };

    loadPresentation = (presentation: Presentation) => {
        const { movements } = presentation;
        if (movements === null) {
            throw '[recordingApi.ts] followMovements() failed: no presentation data';
        }

        movements.forEach((movement, i) => {
            if (typeof movement.doc === 'string') {
                movements[i].doc = IdToDoc(movement.doc);
                if (!movements[i].doc) {
                    console.log('ERROR: tracked doc not found');
                }
            }
        });
    };

    // returns undefined if the docView isn't open on the screen
    getCollectionFFView = (doc: Doc) => {
        const isInView = DocumentManager.Instance.getDocumentView(doc);
        if (isInView) {
            return isInView.ComponentView as CollectionFreeFormView;
        }
    };

    // will open the doc in a tab then return the CollectionFFView that holds it
    openTab = (doc: Doc) => {
        if (doc === undefined) {
            console.error(`doc undefined`);
            return undefined;
        }
        // console.log('openTab', docId, doc);
        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
        const docView = DocumentManager.Instance.getDocumentView(doc);
        // BUG - this returns undefined if the doc is already open
        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._freeform_panX = panX;
        document.Document._freeform_panY = panY;
    };

    getFirstMovements = (movements: Movement[]): Map<Doc, Movement> => {
        if (movements === null) return new Map();
        // generate a set of all unique docIds
        const docIdtoFirstMove = new Map<Doc, Movement>();
        for (const move of movements) {
            if (!docIdtoFirstMove.has(move.doc)) docIdtoFirstMove.set(move.doc, move);
        }
        return docIdtoFirstMove;
    };

    endPlayingPresentation = () => {
        this.isPlaying = false;
        Doc.UserDoc().presentationMode = 'none';
    };

    public playMovements = (presentation: Presentation, timeViewed: number = 0) => {
        // console.info('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.doc) === undefined;
            if (isClosed) this.openTab(firstMovement.doc);

            // for the open tabs, set it to the first move
            const docIdtoFirstMove = this.getFirstMovements(filteredMovements);
            for (const [doc, firstMove] of docIdtoFirstMove) {
                const colFFView = this.getCollectionFFView(doc);
                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.doc);
                if (collectionFFView) {
                    this.zoomAndPan(movement, collectionFFView);
                } else {
                    // tab wasn't open - open it and play the movement
                    const openedColFFView = this.openTab(movement.doc);
                    openedColFFView && this.zoomAndPan(movement, openedColFFView);
                }

                // if last movement, presentation is done -> cleanup :)
                if (movement === filteredMovements[filteredMovements.length - 1]) {
                    this.endPlayingPresentation();
                }
            }, timeDiff);
        });
    };
}