diff options
Diffstat (limited to 'src/client/views')
23 files changed, 664 insertions, 157 deletions
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index a608e448a..98025ac31 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -38,8 +38,12 @@ export class ContextMenu extends React.Component { this._items = []; } - findByDescription = (target: string) => { - return this._items.find(menuItem => menuItem.description === target); + findByDescription = (target: string, toLowerCase = false) => { + return this._items.find(menuItem => { + let reference = menuItem.description; + toLowerCase && (reference = reference.toLowerCase()); + reference === target; + }); } @action diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index e31b44514..373584b4e 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -5,9 +5,13 @@ import { MainView } from "./MainView"; import { DragManager } from "../util/DragManager"; import { action } from "mobx"; import { Doc } from "../../new_fields/Doc"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import DictationManager from "../util/DictationManager"; +import { ContextMenu } from "./ContextMenu"; +import { ContextMenuProps } from "./ContextMenuItem"; const modifiers = ["control", "meta", "shift", "alt"]; -type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; +type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; type KeyControlInfo = { preventDefault: boolean, stopPropagation: boolean @@ -25,9 +29,10 @@ export default class KeyManager { this.router.set(isMac ? "0001" : "0100", this.ctrl); this.router.set(isMac ? "0100" : "0010", this.alt); this.router.set(isMac ? "1001" : "1100", this.ctrl_shift); + this.router.set("1000", this.shift); } - public handle = (e: KeyboardEvent) => { + public handle = async (e: KeyboardEvent) => { let keyname = e.key.toLowerCase(); this.handleGreedy(keyname); @@ -43,7 +48,7 @@ export default class KeyManager { return; } - let control = handleConstrained(keyname, e); + let control = await handleConstrained(keyname, e); control.stopPropagation && e.stopPropagation(); control.preventDefault && e.preventDefault(); @@ -95,6 +100,24 @@ export default class KeyManager { }; }); + private shift = async (keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case " ": + let transcript = await DictationManager.Instance.listen(); + console.log(`I heard${transcript ? `: ${transcript.toLowerCase()}` : " nothing: I thought I was still listening from an earlier session."}`); + let command: ContextMenuProps | undefined; + transcript && (command = ContextMenu.Instance.findByDescription(transcript, true)) && "event" in command && command.event(); + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + } + private alt = action((keyname: string) => { let stopPropagation = true; let preventDefault = true; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 91c8fe57c..88a636784 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faCaretUp, faLongArrowAltRight, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; @@ -125,6 +125,8 @@ export class MainView extends React.Component { library.add(faFilm); library.add(faMusic); library.add(faTree); + library.add(faPlay); + library.add(faPause); library.add(faClone); library.add(faCut); library.add(faCommentAlt); @@ -389,12 +391,15 @@ export class MainView extends React.Component { let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })); let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })); + let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; + let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument], // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode], [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], + [React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher] ]; if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ab4d1aa62..588102f01 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -210,20 +210,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp docs.push(document); } let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); - var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); if (stack === undefined) { - if (this._goldenLayout.root.contentItems.length === 0) { - this._goldenLayout.root.addChild(newContentItem); - } else { - const rowOrCol = this._goldenLayout.root.contentItems[0]; - if (rowOrCol.contentItems.length) { - rowOrCol.contentItems[0].addChild(newContentItem); + let stack: any = this._goldenLayout.root; + while (!stack.isStack) { + if (stack.contentItems.length) { + stack = stack.contentItems[0]; } else { - rowOrCol.addChild(newContentItem); + stack.addChild({ type: 'stack', content: [docContentConfig] }); + stack = undefined; + break; } } + if (stack) { + stack.addChild(docContentConfig); + } } else { - stack.addChild(newContentItem.contentItems[0], undefined); + stack.addChild(docContentConfig, undefined); } this.layoutChanged(); } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 2bb36c825..0cb01dc9d 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -135,9 +135,9 @@ .collectionStackingView-addDocumentButton, .collectionStackingView-addGroupButton { - display: inline-block; - margin: 0 5px; + display: flex; overflow: hidden; + margin: auto; width: 90%; color: lightgrey; overflow: ellipses; @@ -146,6 +146,7 @@ .editableView-container-editing { color: grey; padding: 10px; + width: 100%; } .editableView-input:hover, diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index b5dbc1a22..d8bed7e88 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -239,7 +239,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC }; let headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} - style={{ width: (style.columnWidth) / ((uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1) }}> + style={{ + width: (style.columnWidth) / + ((uniqueHeadings.length + + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1) + }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} @@ -266,7 +270,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {headingView} <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} style={{ - padding: singleColumn ? `${style.yMargin}px ${5}px ${style.yMargin}px ${5}px` : `${style.yMargin}px ${0}px`, + padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, margin: "auto", width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss index 9d2c23d3e..509851ebb 100644 --- a/src/client/views/collections/CollectionVideoView.scss +++ b/src/client/views/collections/CollectionVideoView.scss @@ -6,6 +6,7 @@ top: 0; left:0; z-index: -1; + display:inline-table; } .collectionVideoView-time{ color : white; @@ -15,6 +16,14 @@ background-color: rgba(50, 50, 50, 0.2); transform-origin: left top; } +.collectionVideoView-snapshot{ + color : white; + top :25px; + right : 25px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); + transform-origin: left top; +} .collectionVideoView-play { width: 25px; height: 20px; diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index a264cc402..5185d9d0e 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -9,6 +9,7 @@ import "./CollectionVideoView.scss"; import React = require("react"); import { InkingControl } from "../InkingControl"; import { InkTool } from "../../../new_fields/InkField"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @observer @@ -21,18 +22,20 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); let curTime = NumCast(this.props.Document.curPage); - return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, + <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}> + <FontAwesomeIcon icon="camera" size="lg" /> + </div>, VideoBox._showControls ? (null) : [ - <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - {this._videoBox && this._videoBox.Playing ? "\"" : ">"} + <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}> + <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" /> </div>, - <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}> F - </div> - + </div> ]]); } @@ -56,6 +59,15 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } } + @action + onSnapshot = (e: React.PointerEvent) => { + if (this._videoBox) { + this._videoBox.Snapshot(); + e.stopPropagation(); + e.preventDefault(); + } + } + _isclick = 0; @action onResetDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b7ac8768f..212cc5477 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -85,7 +85,7 @@ export class CollectionView extends React.Component<FieldViewProps> { } else { return [ - (<CollectionViewBaseChrome CollectionView={this} type={type} collapse={this.collapse} />), + (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />), this.SubViewHelper(type, renderProps) ]; } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 2bffe3cc0..38aafd3cc 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -208,6 +208,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <input className="collectionViewBaseChrome-viewSpecsInput" placeholder="FILTER DOCUMENTS" value={this.filterValue ? this.filterValue.script.originalScript : ""} + onChange={(e) => { }} onPointerDown={this.openViewSpecs} /> <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx index 9c3c9c07c..9baa250a6 100644 --- a/src/client/views/collections/KeyRestrictionRow.tsx +++ b/src/client/views/collections/KeyRestrictionRow.tsx @@ -29,6 +29,7 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr else { this.props.script(""); } + return ( <div className="collectionViewBaseChrome-viewSpecsMenu-row"> <input className="collectionViewBaseChrome-viewSpecsMenu-rowLeft" @@ -36,7 +37,7 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr onChange={(e) => runInAction(() => this._key = e.target.value)} placeholder="KEY" /> <button className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle" - style={{ background: PastelSchemaPalette.get(this._contains ? "green" : "red") }} + style={{ background: this._contains ? "#77dd77" : "#ff6961" }} onClick={() => runInAction(() => this._contains = !this._contains)}> {this._contains ? "CONTAINS" : "DOES NOT CONTAIN"} </button> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 91d4fb524..396233551 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,6 +18,7 @@ import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { FieldView } from "./FieldView"; import { WebBox } from "./WebBox"; +import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; @@ -98,7 +99,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (this.props.renderDepth > 7) return (null); if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 58e2443c2..51662274d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,8 @@ import { ClientUtils } from '../../util/ClientUtils'; import { EditableView } from '../EditableView'; import { faHandPointer, faHandPointRight } from '@fortawesome/free-regular-svg-icons'; import { DocumentDecorations } from '../DocumentDecorations'; +import { CognitiveServices } from '../../cognitive_services/CognitiveServices'; +import DictationManager from '../../util/DictationManager'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -63,7 +65,7 @@ library.add(fa.faCrosshairs); library.add(fa.faDesktop); library.add(fa.faUnlock); library.add(fa.faLock); -library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake); +library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); // const linkSchema = createSchema({ // title: "string", @@ -536,6 +538,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true; } + listen = async () => { + let transcript = await DictationManager.Instance.listen(); + transcript && (Doc.GetProto(this.props.Document).transcript = transcript); + } + @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { e.persist(); @@ -559,6 +566,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" }); cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + cm.addItem({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); let makes: ContextMenuProps[] = []; makes.push({ description: "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 3570531b2..acf1aced3 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -20,7 +20,7 @@ export interface RectangleTemplate { export default class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { - let faces = DocListCast(Doc.GetProto(this.props.document).faces); + let faces = DocListCast(this.props.document.faces); let templates: RectangleTemplate[] = faces.map(faceDoc => { let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; let style = { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d4e0eaf98..dbe545048 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -25,9 +25,12 @@ import { Docs, DocumentType } from '../../documents/Documents'; import { DocServer } from '../../DocServer'; import { Font } from '@react-pdf/renderer'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CognitiveServices } from '../../cognitive_services/CognitiveServices'; +import { CognitiveServices, Service, Tag, Confidence } from '../../cognitive_services/CognitiveServices'; import FaceRectangles from './FaceRectangles'; import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { CompileScript } from '../../util/Scripting'; +import { thisExpression } from 'babel-types'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); @@ -230,20 +233,56 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); let modes: ContextMenuProps[] = []; - let dataDoc = Doc.GetProto(this.props.Document); - modes.push({ description: "Generate Tags", event: () => CognitiveServices.Image.generateMetadata(dataDoc), icon: "tag" }); - modes.push({ description: "Find Faces", event: () => CognitiveServices.Image.extractFaces(dataDoc), icon: "camera" }); + modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); + modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" }); } } + extractFaces = () => { + let converter = (results: any) => { + let faceDocs = new List<Doc>(); + results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); + return faceDocs; + }; + CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + } + + generateMetadata = (threshold: Confidence = Confidence.Excellent) => { + let converter = (results: any) => { + let tagDoc = new Doc; + let tagsList = new List(); + results.tags.map((tag: Tag) => { + tagsList.push(tag.name); + let sanitized = tag.name.replace(" ", "_"); + let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; + let computed = CompileScript(script, { params: { this: "Doc" } }); + computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); + }); + this.extensionDoc.generatedTags = tagsList; + tagDoc.title = "Generated Tags Doc"; + tagDoc.confidence = threshold; + return tagDoc; + }; + CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + } + @action onDotDown(index: number) { this.Document.curPage = index; } + @computed get fieldExtensionDoc() { + return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); + } + + @computed private get url() { + let data = Cast(Doc.GetProto(this.props.Document).data, ImageField); + return data ? data.url.href : undefined; + } + dots(paths: string[]) { let nativeWidth = FieldValue(this.Document.nativeWidth, 1); let dist = Math.min(nativeWidth / paths.length, 40); @@ -398,7 +437,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> </div> {/* {this.lightbox(paths)} */} - <FaceRectangles document={this.props.Document} color={"#0000FF"} backgroundColor={"#0000FF"} /> + <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 34cb47b20..1f8636826 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -8,7 +8,6 @@ import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; -import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; @@ -21,6 +20,9 @@ import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faVideo } from "@fortawesome/free-solid-svg-icons"; +import { CompileScript } from "../../util/Scripting"; +import { Doc } from "../../../new_fields/Doc"; +import { ScriptField } from "../../../new_fields/ScriptField"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -87,6 +89,56 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"); } + @action public Snapshot() { + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + if (!this._videoRef) { // can't find a way to take snapshots of videos + let b = Docs.Create.ButtonDocument({ + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString() + }); + const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, { + params: { this: Doc.name }, + capturedVariables: { self: this.props.Document }, + typecheck: false, + editable: true, + }); + if (script.compiled) { + b.onClick = new ScriptField(script); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); + } else { + console.log(script.errors.map(error => error.messageText).join("\n")); + } + } else { + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + if (returnedFilename) { + let url = Utils.prepend(returnedFilename); + let imageSummary = Docs.Create.ImageDocument(url, { + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + }); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + } + } + @action updateTimecode = () => { this.player && (this.props.Document.curPage = this.player.currentTime); @@ -150,39 +202,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let subitems: ContextMenuProps[] = []; subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); - subitems.push({ - description: "Take Snapshot", event: async () => { - var canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); - var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - if (ctx) { - ctx.rect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "blue"; - ctx.fill(); - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - - //convert to desired file format - var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' - // if you want to preview the captured image, - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - if (returnedFilename) { - let url = Utils.prepend(returnedFilename); - let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" - }); - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); - } - }); - }, - icon: "expand-arrows-alt" - }); + subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" }); } } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index e8d1da94b..513f9fed6 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -119,7 +119,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { let context = await Cast(targetDoc.targetContext, Doc); if (context) { DocumentManager.Instance.jumpToDocument(targetDoc, false, undefined, - ((doc) => this.props.parent.props.parent.props.addDocTab(context, context.proto, e.ctrlKey ? "onRight" : "inTab"))); + ((doc) => this.props.parent.props.parent.props.addDocTab(context!, context!.proto, e.ctrlKey ? "onRight" : "inTab"))); } } } diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 36f1178f1..11f3eb846 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; -import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch, faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -9,17 +9,22 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils } from "../../../Utils"; +import { Utils, returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; -import "./PresentationView.scss"; +import { ContextMenu } from "../ContextMenu"; +import { Transform } from "../../util/Transform"; +import { DocumentView } from "../nodes/DocumentView"; +import { DocumentType } from "../../documents/Documents"; import React = require("react"); + library.add(faArrowUp); library.add(fileSolid); library.add(faLocationArrow); library.add(fileRegular as any); library.add(faSearch); +library.add(faArrowRight); interface PresentationElementProps { mainDocument: Doc; @@ -46,6 +51,7 @@ export enum buttonIndex { FadeAfter = 3, HideAfter = 4, Group = 5, + OpenRight = 6 } @@ -63,12 +69,9 @@ export default class PresentationElement extends React.Component<PresentationEle private backUpDoc: Doc | undefined; - - - constructor(props: PresentationElementProps) { super(props); - this.selectedButtons = new Array(6); + this.selectedButtons = new Array(7); this.presElRef = React.createRef(); } @@ -104,6 +107,9 @@ export default class PresentationElement extends React.Component<PresentationEle } } + /** + * Function that will be called to receive stored backUp for buttons + */ receiveButtonBackUp = async () => { //get the list that stores docs that keep track of buttons @@ -132,7 +138,7 @@ export default class PresentationElement extends React.Component<PresentationEle if (!foundDoc) { let newDoc = new Doc(); - let defaultBooleanArray: boolean[] = new Array(6); + let defaultBooleanArray: boolean[] = new Array(7); newDoc.selectedButtons = new List(defaultBooleanArray); newDoc.docId = this.props.document[Id]; castedList.push(newDoc); @@ -395,6 +401,22 @@ export default class PresentationElement extends React.Component<PresentationEle } /** + * Function that opens up the option to open a element on right when navigated, + * instead of openening it as tab as default. + */ + @action + onRightTabClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.OpenRight]) { + this.selectedButtons[buttonIndex.OpenRight] = false; + // action maybe + } else { + this.selectedButtons[buttonIndex.OpenRight] = true; + } + this.autoSaveButtonChange(buttonIndex.OpenRight); + } + + /** * Creating a drop target for drag and drop when called. */ protected createListDropTarget = (ele: HTMLDivElement) => { @@ -629,7 +651,7 @@ export default class PresentationElement extends React.Component<PresentationEle */ getSelectedButtonsOfDoc = async (paramDoc: Doc) => { let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); - let foundSelectedButtons: boolean[] = new Array(6); + let foundSelectedButtons: boolean[] = new Array(7); //if this is the first time this doc mounts, push a doc for it to store for (let doc of castedList!) { @@ -649,7 +671,6 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to add dragging as an event. onPointerEnter = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -666,7 +687,6 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to remove the dragging when dropped. onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; //to get currently selected presentation doc let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -765,9 +785,86 @@ export default class PresentationElement extends React.Component<PresentationEle groupArray.push(tempStack.pop()!); } } + /** + * This function is a getter to get if a document is in previewMode. + */ + private get embedInline() { + return BoolCast(this.props.document.embedOpen); + } + + /** + * This function sets document in presentation preview mode as the given value. + */ + private set embedInline(value: boolean) { + this.props.document.embedOpen = value; + } + /** + * The function that recreates that context menu of presentation elements. + */ + onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { + e.preventDefault(); + e.stopPropagation(); + ContextMenu.Instance.addItem({ description: this.embedInline ? "Collapse Inline" : "Expand Inline", event: () => this.embedInline = !this.embedInline, icon: "expand" }); + ContextMenu.Instance.displayMenu(e.clientX, e.clientY); + } + /** + * The function that is responsible for rendering the a preview or not for this + * presentation element. + */ + renderEmbeddedInline = () => { + if (!this.embedInline) { + return (null); + } + let propDocWidth = NumCast(this.props.document.nativeWidth); + let propDocHeight = NumCast(this.props.document.nativeHeight); + let scale = () => { + let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); + return newScale; + }; + return ( + <div style={{ + position: "relative", + height: propDocHeight === 0 ? 100 : propDocHeight * scale(), + width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), + marginTop: 15 + + }}> + <DocumentView + fitToBox={StrCast(this.props.document.type).indexOf(DocumentType.COL) !== -1} + Document={this.props.document} + addDocument={returnFalse} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={() => 350} + PanelHeight={() => 90} + focus={emptyFunction} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> + <div style={{ + width: " 100%", + height: " 100%", + position: "absolute", + left: 0, + top: 0, + background: "transparent", + zIndex: 2, + + }}></div> + </div> + ); + } render() { let p = this.props; @@ -784,7 +881,7 @@ export default class PresentationElement extends React.Component<PresentationEle let dropAction = StrCast(this.props.document.dropAction) as dropActionType; let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true); return ( - <div className={className} key={p.document[Id] + p.index} + <div className={className} onContextMenu={this.onContextMenu} key={p.document[Id] + p.index} ref={this.presElRef} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerDown={onItemDown} @@ -809,7 +906,10 @@ export default class PresentationElement extends React.Component<PresentationEle this.changeGroupStatus(); this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]); }}> <FontAwesomeIcon icon={"arrow-up"} /> </button> + <button title="Open Right" className={this.selectedButtons[buttonIndex.OpenRight] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button> + <br /> + {this.renderEmbeddedInline()} </div> ); } diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index 2d63d41b5..e853c4070 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -7,8 +7,6 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; import PresentationElement, { buttonIndex } from "./PresentationElement"; -import { DragManager } from "../../util/DragManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; import "../../../new_fields/Doc"; diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss new file mode 100644 index 000000000..336f43d20 --- /dev/null +++ b/src/client/views/presentationview/PresentationModeMenu.scss @@ -0,0 +1,30 @@ +.presMenu-cont { + position: fixed; + z-index: 10000; + height: 35px; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + border-radius: 0px 6px 6px 6px; + overflow: hidden; + display: flex; + + .presMenu-button { + background-color: transparent; + width: 35px; + height: 35px; + } + + .presMenu-button:hover { + background-color: #121212; + } + + .presMenu-dragger { + height: 100%; + transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; + } + +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx new file mode 100644 index 000000000..4de8da587 --- /dev/null +++ b/src/client/views/presentationview/PresentationModeMenu.tsx @@ -0,0 +1,100 @@ +import React = require("react"); +import { observable, action, runInAction } from "mobx"; +import "./PresentationModeMenu.scss"; +import { observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + + +export interface PresModeMenuProps { + next: () => void; + back: () => void; + presStatus: boolean; + startOrResetPres: () => void; + closePresMode: () => void; +} + +/** + * This class is responsible for modeling of the Presentation Mode Menu. The menu allows + * user to navigate through presentation elements, and start/stop the presentation. + */ +@observer +export default class PresModeMenu extends React.Component<PresModeMenuProps> { + + @observable private _top: number = 20; + @observable private _right: number = 0; + @observable private _opacity: number = 1; + @observable private _transition: string = "opacity 0.5s"; + @observable private _transitionDelay: string = ""; + + + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + /** + * The function that changes the coordinates of the menu, depending on the + * movement of the mouse when it's being dragged. + */ + @action + dragging = (e: PointerEvent) => { + this._right -= e.movementX; + this._top += e.movementY; + + e.stopPropagation(); + e.preventDefault(); + } + + /** + * The function that removes the event listeners that are responsible for + * dragging of the menu. + */ + dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + /** + * The function that starts the dragging of the presentation mode menu. When + * the lines on further right are clicked on. + */ + dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointerup", this.dragEnd); + + e.stopPropagation(); + e.preventDefault(); + } + + /** + * The function that is responsible for rendering the play or pause button, depending on the + * status of the presentation. + */ + renderPlayPauseButton = () => { + if (this.props.presStatus) { + return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; + } else { + return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; + } + } + + render() { + return ( + <div className="presMenu-cont" ref={this._mainCont} + style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> + <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}> + <FontAwesomeIcon icon="times" size="lg" /> + </button> + <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> + </div > + ); + } + + + + +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 2bb0ec8c8..97cbd4a24 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -1,7 +1,7 @@ .presentationView-cont { position: absolute; background: white; - z-index: 1; + z-index: 2; box-shadow: #AAAAAA .2vw .2vw .4vw; right: 0; top: 0; @@ -19,6 +19,17 @@ -ms-user-select: none; user-select: none; transition: all .1s; + //max-height: 250px; + + + + .documentView-node { + // height: auto !important; + // width: aut !important; + position: absolute; + z-index: 1; + } + } .presentationView-item-above { @@ -55,7 +66,7 @@ padding-bottom: 3px; font-size: 25px; display: inline-block; - width: calc(100% - 160px); + width: calc(100% - 200px); } .presentation-icon { diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index e25725275..4fe9d3a1b 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -4,7 +4,7 @@ import { observable, action, runInAction, reaction, autorun } from "mobx"; import "./PresentationView.scss"; import { DocumentManager } from "../../util/DocumentManager"; import { Utils } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -12,10 +12,12 @@ import { List } from "../../../new_fields/List"; import PresentationElement, { buttonIndex } from "./PresentationElement"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons'; import { Docs } from "../../documents/Documents"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; +import PresModeMenu from "./PresentationModeMenu"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; library.add(faArrowLeft); library.add(faArrowRight); @@ -25,6 +27,7 @@ library.add(faPlus); library.add(faTimes); library.add(faMinus); library.add(faEdit); +library.add(faEye); export interface PresViewProps { @@ -32,6 +35,7 @@ export interface PresViewProps { } const expandedWidth = 400; +const presMinWidth = 300; @observer export class PresentationView extends React.Component<PresViewProps> { @@ -62,6 +66,8 @@ export class PresentationView extends React.Component<PresViewProps> { //Variable that holds reference to title input, so that new presentations get titles assigned. @observable titleInputElement: HTMLInputElement | undefined; @observable PresTitleChangeOpen: boolean = false; + @observable presMode: boolean = false; + @observable opacity = 1; @observable persistOpacity = true; @@ -84,6 +90,7 @@ export class PresentationView extends React.Component<PresViewProps> { //The first lifecycle function that gets called to set up the current presentation. async componentWillMount() { + this.props.Documents.forEach(async (doc, index: number) => { //For each presentation received from mainContainer, a mapping is created. @@ -361,11 +368,16 @@ export class PresentationView extends React.Component<PresViewProps> { //checking if curDoc has navigation open let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; if (curDocButtons[buttonIndex.Navigate]) { - DocumentManager.Instance.jumpToDocument(curDoc, false); + this.jumpToTabOrRight(curDocButtons, curDoc); } else if (curDocButtons[buttonIndex.Show]) { let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(curDoc, true); + if (curDocButtons[buttonIndex.OpenRight]) { + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(curDoc, true); + } else { + await DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; @@ -378,9 +390,15 @@ export class PresentationView extends React.Component<PresViewProps> { return; } let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + let curDocButtons = this.presElementsMappings.get(docToJump)!.selected; - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + + if (curDocButtons[buttonIndex.OpenRight]) { + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + } else { + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; //saving the scale that user was on @@ -391,6 +409,18 @@ export class PresentationView extends React.Component<PresViewProps> { } /** + * This function checks if right option is clicked on a presentation element, if not it does open it as a tab + * with help of CollectionDockingView. + */ + jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => { + if (curDocButtons[buttonIndex.OpenRight]) { + DocumentManager.Instance.jumpToDocument(curDoc, false); + } else { + DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } + } + + /** * Async function that supposedly return the doc that is located at given index. */ getDocAtIndex = async (index: number) => { @@ -434,22 +464,6 @@ export class PresentationView extends React.Component<PresViewProps> { } } - //removing it from the backUp of selected Buttons - // let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); - // if (castedList) { - // castedList.forEach(async (doc, indexOfDoc) => { - // let curDoc = await doc; - // let curDocId = StrCast(curDoc.docId); - // if (curDocId === removedDoc[Id]) { - // if (castedList) { - // castedList.splice(indexOfDoc, 1); - // return; - // } - // } - // }); - - // } - //removing it from the backUp of selected Buttons let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); if (castedList) { @@ -487,13 +501,16 @@ export class PresentationView extends React.Component<PresViewProps> { } } + /** + * An alternative remove method that removes a doc from presentation by its actual + * reference. + */ public removeDocByRef = (doc: Doc) => { let indexOfDoc = this.childrenDocs.indexOf(doc); const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); if (value) { value.splice(indexOfDoc, 1)[0]; } - //this.RemoveDoc(indexOfDoc, true); if (indexOfDoc !== - 1) { return true; } @@ -579,18 +596,40 @@ export class PresentationView extends React.Component<PresViewProps> { //The function that starts or resets presentaton functionally, depending on status flag. @action - startOrResetPres = () => { + startOrResetPres = async () => { if (this.presStatus) { this.resetPresentation(); } else { this.presStatus = true; - this.startPresentation(0); + let startIndex = await this.findStartDocument(); + this.startPresentation(startIndex); const current = NumCast(this.curPresentation.selectedDoc); - this.gotoDocument(0, current); + this.gotoDocument(startIndex, current); } this.curPresentation.presStatus = this.presStatus; } + /** + * This method is called to find the start document of presentation. So + * that when user presses on play, the correct presentation element will be + * selected. + */ + findStartDocument = async () => { + let docAtZero = await this.getDocAtIndex(0); + if (docAtZero === undefined) { + return 0; + } + let docAtZeroPresId = StrCast(docAtZero.presentId); + + if (this.groupMappings.has(docAtZeroPresId)) { + let group = this.groupMappings.get(docAtZeroPresId)!; + let lastDoc = group[group.length - 1]; + return this.childrenDocs.indexOf(lastDoc); + } else { + return 0; + } + } + //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. @action @@ -805,60 +844,159 @@ export class PresentationView extends React.Component<PresViewProps> { this.curPresentation.title = newTitle; } - addPressElem = (keyDoc: Doc, elem: PresentationElement) => { - this.presElementsMappings.set(keyDoc, elem); + /** + * On pointer down element that is catched on resizer of te + * presentation view. Sets up the event listeners to change the size with + * mouse move. + */ + _downsize = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downsize = e.clientX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + /** + * Changes the size of the presentation view, with mouse move. + * Minimum size is set to 300, so that every button is visible. + */ + @action + onPointerMove = (e: PointerEvent) => { + + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth); + } + + /** + * The method that is called on pointer up event. It checks if the button is just + * clicked so that presentation view will be closed. The way it's done is to check + * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger. + */ + @action + onPointerUp = (e: PointerEvent) => { + if (Math.abs(e.clientX - this._downsize) < 4) { + let presWidth = NumCast(this.curPresentation.width); + if (presWidth - presMinWidth !== 0) { + this.curPresentation.width = 0; + } + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); } + /** + * This function gets triggered on click of the dragger. It opens up the + * presentation view, if it was closed beforehand. + */ + togglePresView = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + let width = NumCast(this.curPresentation.width); + if (width === 0) { + this.curPresentation.width = presMinWidth; + } + } + /** + * This function is a setter that opens up the + * presentation mode, by setting it's render flag + * to true. It also closes the presentation view. + */ + @action + openPresMode = () => { + if (!this.presMode) { + this.curPresentation.width = 0; + this.presMode = true; + } + } + + /** + * This function closes the presentation mode by setting its + * render flag to false. It also opens up the presentation view. + * By setting it to it's minimum size. + */ + @action + closePresMode = () => { + if (this.presMode) { + this.presMode = false; + this.curPresentation.width = presMinWidth; + } + + } + + /** + * Function that is called to render the presentation mode, depending on its flag. + */ + renderPresMode = () => { + if (this.presMode) { + return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />; + } else { + return (null); + } + + } render() { let width = NumCast(this.curPresentation.width); return ( - <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflow: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> - <div className="presentationView-heading"> - {this.renderSelectOrPresSelection()} - <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> - <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); - runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); - }}><FontAwesomeIcon icon={"plus"} /></button> - <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> - <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); - runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); - }}><FontAwesomeIcon icon={"edit"} /></button> + <div> + <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> + <div className="presentationView-heading"> + {this.renderSelectOrPresSelection()} + <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> + <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button> + <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); + runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); + }}><FontAwesomeIcon icon={"plus"} /></button> + <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> + <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); + runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); + }}><FontAwesomeIcon icon={"edit"} /></button> + </div> + <div className="presentation-buttons"> + <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + </div> + + <PresentationViewList + mainDocument={this.curPresentation} + deleteDocument={this.RemoveDoc} + gotoDocument={this.gotoDocument} + groupMappings={this.groupMappings} + PresElementsMappings={this.presElementsMappings} + setChildrenDocs={this.setChildrenDocs} + presStatus={this.presStatus} + presButtonBackUp={this.presButtonBackUp} + presGroupBackUp={this.presGroupBackUp} + removeDocByRef={this.removeDocByRef} + clearElemMap={() => this.presElementsMappings.clear()} + /> + <input + type="checkbox" + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { + this.persistOpacity = e.target.checked; + this.opacity = this.persistOpacity ? 1 : 0.4; + })} + checked={this.persistOpacity} + style={{ position: "absolute", bottom: 5, left: 5 }} + onPointerEnter={action(() => this.labelOpacity = 1)} + onPointerLeave={action(() => this.labelOpacity = 0)} + /> + <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> </div> - <div className="presentation-buttons"> - <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> - {this.renderPlayPauseButton()} - <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <div className="mainView-libraryHandle" + style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }} + onPointerDown={this.onPointerDown} onClick={this.togglePresView}> + <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} /> </div> - <input - type="checkbox" - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - this.persistOpacity = e.target.checked; - this.opacity = this.persistOpacity ? 1 : 0.4; - })} - checked={this.persistOpacity} - style={{ position: "absolute", bottom: 5, left: 5 }} - onPointerEnter={action(() => this.labelOpacity = 1)} - onPointerLeave={action(() => this.labelOpacity = 0)} - /> - <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> - <PresentationViewList - mainDocument={this.curPresentation} - deleteDocument={this.RemoveDoc} - gotoDocument={this.gotoDocument} - groupMappings={this.groupMappings} - PresElementsMappings={this.presElementsMappings} - setChildrenDocs={this.setChildrenDocs} - presStatus={this.presStatus} - presButtonBackUp={this.presButtonBackUp} - presGroupBackUp={this.presGroupBackUp} - removeDocByRef={this.removeDocByRef} - clearElemMap={() => this.presElementsMappings.clear()} - /> + {this.renderPresMode()} + </div> ); } |
