aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts134
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/util/History.ts6
-rw-r--r--src/client/util/RecordingApi.ts269
-rw-r--r--src/client/util/Scripting.ts2
5 files changed, 364 insertions, 49 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 51bfdbbd2..b4e18a8bb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -7,8 +7,8 @@ import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
-import { nullAudio } from "../../fields/URLField";
+import { BoolCast, Cast, DateCast, DocCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { ImageField, nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
@@ -22,11 +22,10 @@ import { TreeView } from "../views/collections/TreeView";
import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
-import { LabelBox } from "../views/nodes/LabelBox";
import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { OverlayView } from "../views/OverlayView";
import { DocumentManager } from "./DocumentManager";
-import { DragManager, dropActionType } from "./DragManager";
+import { DragManager } from "./DragManager";
import { makeTemplate, MakeTemplate } from "./DropConverter";
import { HistoryUtil } from "./History";
import { LinkManager } from "./LinkManager";
@@ -272,8 +271,8 @@ export class CurrentUserUtils {
doc.emptyScreenshot = Docs.Create.ScreenshotDocument({ ...standardOps(), title: "empty screenshot", _width: 400, _height: 200 });
}
if (doc.emptyWall === undefined) {
- doc.emptyWall = Docs.Create.ScreenshotDocument({ ...standardOps(), title: "screen snapshot", _width: 400, _height: 200, });
- (doc.emptyWall as Doc).videoWall = true;
+ doc.emptyWall = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ (doc.emptyWall as Doc).recording = true;
}
if (doc.emptyAudio === undefined) {
doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { ...standardOps(), title: "audio recording", x: 200, y: 200, _width: 200, _height: 100, });
@@ -355,6 +354,7 @@ export class CurrentUserUtils {
}
static menuBtnDescriptions(doc: Doc) {
+ const badgeValue = ScriptField.MakeFunction("((len) => len ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length)")
return [
{ title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' },
{ title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' },
@@ -362,7 +362,7 @@ export class CurrentUserUtils {
{ title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" },
{ title: "Imports", target: Cast(doc.myImportDocs, Doc, null), icon: "upload", click: 'selectMainMenu(self)' },
{ title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' },
- { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc },
+ { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', badgeValue},
{ title: "Trails", target: Cast(doc.myTrails, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' },
{ title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" },
];
@@ -370,8 +370,9 @@ export class CurrentUserUtils {
static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
- await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
- const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments, hidden }) =>
+ await this.setupLinkDocs(doc, linkDatabaseId);
+ await this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
+ const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, badgeValue, hidden }) =>
Docs.Create.FontIconDocument({
icon,
btnType: ButtonType.MenuButton,
@@ -382,12 +383,13 @@ export class CurrentUserUtils {
dontUndo: true,
title,
target,
+ dontRegisterView: true,
hidden: hidden ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined,
_dropAction: "alias",
_removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
_width: 60,
_height: 60,
- watchedDocuments,
+ badgeValue,
onClick: ScriptField.MakeScript(click, { scriptContext: "any" })
})
);
@@ -400,6 +402,7 @@ export class CurrentUserUtils {
_chromeHidden: true,
backgroundColor: Colors.DARK_GRAY,
boxShadow: "rgba(0,0,0,0)",
+ dontRegisterView: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
ignoreClick: true,
_gridGap: 0,
@@ -720,7 +723,7 @@ export class CurrentUserUtils {
];
doc.dockedBtns = CurrentUserUtils.linearButtonList({
title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true,
- linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
+ childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
}, btnDescs.map(desc => doc[`dockedBtn-${desc.title}`] as Doc ?? (doc[`dockedBtn-${desc.title}`] = dockBtn({ title: desc.title, ...desc.opts() }))));
}
}
@@ -834,12 +837,13 @@ export class CurrentUserUtils {
});
if (doc.contextMenuBtns === undefined) {
doc.contextMenuBtns = CurrentUserUtils.linearButtonList(
- { title: "menu buttons", flexGap: 0, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 },
+ { title: "menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 },
CurrentUserUtils.contextMenuTools(doc).map(params =>
!params.subMenu ?
btnFunc(params) :
CurrentUserUtils.linearButtonList({
title: params.title,
+ childDontRegisterViews: true,
linearViewSubMenu: true, flexGap: 0, ignoreClick: true,
linearViewExpandable: true, icon: params.title, _height: 30,
linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined,
@@ -855,6 +859,7 @@ export class CurrentUserUtils {
btnFunc(params) :
CurrentUserUtils.linearButtonList({
title: params.title,
+ childDontRegisterViews: true,
linearViewSubMenu: true, flexGap: 0, ignoreClick: true,
linearViewExpandable: true, icon: params.title, _height: 30,
linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined,
@@ -897,10 +902,7 @@ export class CurrentUserUtils {
}
// Sharing sidebar is where shared documents are contained
- static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
- if (doc.myPublishedDocs === undefined) {
- doc.myPublishedDocs = new List<Doc>();
- }
+ static async setupLinkDocs(doc: Doc, linkDatabaseId: string) {
if (doc.myLinkDatabase === undefined) {
let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
if (!linkDocs) {
@@ -912,29 +914,41 @@ export class CurrentUserUtils {
}
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
- // TODO:glr NOTE: treeViewHideTitle & _showTitle may be confusing, treeViewHideTitle is for the editable title (just for tree view), _showTitle is to show the Document title for any document
- if (doc.mySharedDocs === undefined) {
- let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer");
- if (!sharedDocs) {
- sharedDocs = Docs.Create.TreeDocument([], {
- title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
- _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- _chromeHidden: true, boxShadow: "0 0",
- explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
- }, sharingDocumentId + "outer", sharingDocumentId);
- (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment;
- }
- if (sharedDocs instanceof Doc) {
- Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
- const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
- const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
- sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]);
- sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]);
- sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]);
- sharedDocs.childContextMenuIcons = new List<string>(["user-plus",]);
-
- }
- doc.mySharedDocs = new PrefetchProxy(sharedDocs as Doc);
+ }
+ // A user's sharing document is where all documents that are shared to that user are placed.
+ // When the user views one of these documents, it will be added to the sharing documents 'viewed' list field
+ // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents
+ static async setupSharedDocs(doc: Doc, sharingDocumentId: string) {
+ const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+ const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+ const dblClkScript = ScriptField.MakeScript("{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}", {scriptContext:"any", documentView:Doc.name})
+
+ const sharedDocOpts:DocumentOptions = {
+ title: "My Shared Docs",
+ userColor: "rgb(202, 202, 202)",
+ childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
+ childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
+ childContextMenuLabels: new List<string>(["Add to Dashboards",]),
+ childContextMenuIcons: new List<string>(["user-plus",]),
+ treeViewChildDoubleClick: dblClkScript,
+ };
+ const sharedRequiredDocOpts:DocumentOptions = {
+ "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
+ childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar
+ _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
+ };
+
+ const sharedDocs = Docs.newAccount ? undefined : DocCast(doc.mySharedDocs) ?? DocCast(await DocServer.GetRefField(sharingDocumentId + "outer"));
+ if (!(sharedDocs instanceof Doc)) {
+ doc.mySharedDocs = new PrefetchProxy(
+ Docs.Create.TreeDocument([], {...sharedDocOpts, ...sharedRequiredDocOpts}, sharingDocumentId + "outer", sharingDocumentId));
+ } else {
+ Object.entries(sharedRequiredDocOpts).forEach(pair => {
+ const targetDoc = pair[0].startsWith("_") ? sharedDocs as Doc : Doc.GetProto(sharedDocs as Doc);
+ targetDoc[pair[0]] = pair[1];
+ });
}
}
@@ -945,7 +959,7 @@ export class CurrentUserUtils {
doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], {
title: "My Imports", _forceActive: true, buttonMenu: true, buttonMenuDoc: newImportButton, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0,
childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true,
- explainer: "This is where documents that are Imported into Dash will go."
+ dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go."
}));
}
}
@@ -954,7 +968,7 @@ export class CurrentUserUtils {
static setupSearchSidebar(doc: Doc) {
if (doc.mySearchPanel === undefined) {
doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
- backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true,
+ dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true,
childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true
})) as any as Doc;
}
@@ -1042,6 +1056,7 @@ export class CurrentUserUtils {
doc.savedFilters = new List<Doc>();
doc.filterDocCount = 0;
doc.freezeChildren = "remove|add";
+ doc.myPublishedDocs = doc.myPublishedDocs ?? new List<Doc>();
doc.myHeaderBarDoc = doc.myHeaderBarDoc ?? Docs.Create.MulticolumnDocument([], { title: "header bar", system: true });
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
@@ -1094,6 +1109,7 @@ export class CurrentUserUtils {
Docs.newAccount = !(field instanceof Doc);
await Docs.Prototypes.initialize();
const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
+ Docs.newAccount &&(userDoc.activePage = "home");
const updated = this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
(await DocListCastAsync(Cast(Doc.UserDoc().myLinkDatabase, Doc, null)?.data))?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager
const a1 = await Cast(link?.anchor1, Doc, null);
@@ -1111,6 +1127,9 @@ export class CurrentUserUtils {
public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
+ if (!DocListCast(CurrentUserUtils.MyDashboards.data).includes(doc)) {
+ Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc);
+ }
if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard
!("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]));
@@ -1184,18 +1203,42 @@ export class CurrentUserUtils {
input.click();
}
+ public static CaptureDashboardThumbnail() {
+ const docView = CollectionDockingView.Instance.props.DocumentView?.();
+ const content = docView?.ContentDiv;
+ if (docView && content) {
+ const _width = Number(getComputedStyle(content).width.replace("px",""));
+ const _height = Number(getComputedStyle(content).height.replace("px",""));
+ return CollectionFreeFormView.UpdateIcon(
+ docView.layoutDoc[Id] + "-icon" + (new Date()).getTime(),
+ content,
+ _width, _height,
+ _width, _height, 0, 1, true, docView.layoutDoc[Id] + "-icon",
+ (iconFile, _nativeWidth, _nativeHeight) => {
+ const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: docView.rootDoc.title+"-icon", _width, _height, _nativeWidth, _nativeHeight});
+ const proto = Cast(img.proto, Doc, null)!;
+ proto["data-nativeWidth"] = _width;
+ proto["data-nativeHeight"] = _height;
+ Doc.GetProto(CurrentUserUtils.ActiveDashboard).thumb = img;
+ });
+ }
+
+ }
+
public static async snapshotDashboard(userDoc: Doc) {
const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard);
Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy);
CurrentUserUtils.openDashboard(userDoc, copy);
}
+ public static closeActiveDashboard = () => {
+ Doc.UserDoc().activeDashboard = undefined;
+ }
+
public static createNewDashboard = async (userDoc: Doc, id?: string) => {
const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true);
const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc;
const dashboardCount = DocListCast(dashboards.data).length + 1;
- const emptyPane = Cast(userDoc.emptyPane, Doc, null);
- emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
const freeformOptions: DocumentOptions = {
x: 0,
y: 400,
@@ -1203,7 +1246,7 @@ export class CurrentUserUtils {
_height: 1000,
_fitWidth: true,
_backgroundGridShow: true,
- title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
+ title: `Untitled Tab 1`,
};
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
@@ -1212,11 +1255,12 @@ export class CurrentUserUtils {
// switching the tabs from the datadoc to the regular doc
const dashboardTabs = DocListCast(dashboardDoc[DataSym].data);
dashboardDoc.data = new List<Doc>(dashboardTabs);
+ dashboardDoc["pane-count"] = 1;
userDoc.activePresentation = presentation;
Doc.AddDocToList(dashboards, "data", dashboardDoc);
- CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
+ // CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
}
public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 9f8c49081..dbfba8992 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -455,7 +455,7 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction;
}
- if (((e.target as any)?.className === "lm_tabs" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
+ if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
if (!startWindowDragTimer) {
startWindowDragTimer = setTimeout(async () => {
startWindowDragTimer = undefined;
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index e6f75a7db..632348306 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -58,8 +58,10 @@ export namespace HistoryUtil {
export function getState(): ParsedUrl {
const state = copyState(history.state);
- state.initializers = state.initializers || {};
- return state;
+ if (state) {
+ state.initializers = state.initializers || {};
+ }
+ return state ?? {initializers:{}};
}
// export function addHandler(handler: (state: ParsedUrl | null) => void) {
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
new file mode 100644
index 000000000..021feee9a
--- /dev/null
+++ b/src/client/util/RecordingApi.ts
@@ -0,0 +1,269 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../fields/Types";
+import { Doc } from "../../fields/Doc";
+import { VideoBox } from "../views/nodes/VideoBox";
+import { scaleDiverging } from "d3-scale";
+import { Transform } from "./Transform";
+
+type Movement = {
+ time: number,
+ panX: number,
+ panY: number,
+ scale: number,
+}
+
+type Presentation = {
+ movements: Array<Movement> | null
+ meta: Object,
+}
+
+export class RecordingApi {
+
+ private static NULL_PRESENTATION: Presentation = {
+ movements: null,
+ meta: {},
+ }
+
+ // 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.movements === 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.movements = []
+ // (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'
+ // set the previus recording view to the play view
+ this.playFFView = this.recordingFFView
+
+ 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 adjust absoluteStart to add the time difference
+ const timestamp = new Date().getTime()
+ this.absoluteStart = timestamp - this.absoluteStart
+ }
+
+ public resume = () => {
+ this.isRecording = true
+ // set absoluteStart to the difference in time
+ this.absoluteStart = new Date().getTime() - this.absoluteStart
+ }
+
+ private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
+ // ensure we are recording
+ if (!this.isRecording) {
+ return new Error('[recordingApi.ts] trackMovements()')
+ }
+ // check to see if the presetation is init
+ if (this.isInitPresenation) {
+ return new Error('[recordingApi.ts] trackMovements(): no presentation')
+ }
+
+ // get the time
+ const time = new Date().getTime() - this.absoluteStart
+ // make new movement object
+ const movement: Movement = { time, panX, panY, scale }
+
+ // add that movement to the current presentation data's movement array
+ this.currentPresentation.movements && 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), scale: NumCast(view.Document.viewScale, -1) }),
+ (res) => (res.x !== -1 && res.y !== -1 && this.isRecording) && this.trackMovements(res.x, res.y, res.scale)
+ )
+
+ // 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
+ // TODO: store the FFview with the movements
+ private playFFView: CollectionFreeFormView | null;
+ private timers: NodeJS.Timeout[] | null;
+
+ public setPlayFFView = (view: CollectionFreeFormView): void => {
+ this.playFFView = view
+ }
+
+ // 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.playFFView === null) {
+ return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+ }
+
+ if (!this._isPlaying) {
+ //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+ return
+ }
+ this._isPlaying = false
+ // TODO: set userdoc presentMode to browsing
+ this.timers?.map(timer => clearTimeout(timer))
+
+ // this.videoBox = null;
+ }
+
+ private videoBox: VideoBox | null = null;
+
+ // by calling pause on the VideoBox, the pauseMovements will be called
+ public pauseVideoAndMovements = (): boolean => {
+ this.videoBox?.Pause()
+
+ this.pauseMovements()
+ return this.videoBox == null
+ }
+
+ public _isPlaying = false;
+
+ public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
+ if (presentation.movements === null || this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ }
+ if(this._isPlaying) return;
+
+ this._isPlaying = true;
+ Doc.UserDoc().presentationMode = 'watching';
+
+ // TODO: consider this bug at the end of the clip on seek
+ this.videoBox = videoBox || null;
+
+ // only get the movements that are remaining in the video time left
+ const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+ // helper to replay a movement
+ const document = this.playFFView
+ let preScale = -1;
+ const zoomAndPan = (movement: Movement) => {
+ const { panX, panY, scale } = movement;
+ (scale !== -1 && 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)
+ })
+ }
+
+ // Unfinished code for tracing multiple free form views
+ // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = 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)
+ // }
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 3b0a47b54..223b0268c 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -223,7 +223,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
}
const paramString = paramList.join(", ");
- const body = addReturn ? `return ${script};` : `return ${script};`;
+ const body = addReturn ? `return ${script};` : script;
const reqTypes = requiredType ? `: ${requiredType}` : '';
const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
host.writeFile("file.ts", funcScript);