aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTyler Schicke <tyler_schicke@brown.edu>2019-07-30 16:03:23 -0400
committerTyler Schicke <tyler_schicke@brown.edu>2019-07-30 16:03:23 -0400
commit28db2fab55015f26286c577347a90e5acb24ca0f (patch)
treebdb7e1286267ea805bbc10496a3a66ba6e3552dd /src
parent26086ee95a9a16486d637aa43c96638b6154379f (diff)
parentcc271d2802cab295221dc4ea3d8c77a6328a8cfb (diff)
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts16
-rw-r--r--src/client/apis/youtube/YoutubeBox.scss124
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx362
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts46
-rw-r--r--src/client/documents/Documents.ts16
-rw-r--r--src/client/util/DictationManager.ts39
-rw-r--r--src/client/views/ContextMenu.tsx8
-rw-r--r--src/client/views/GlobalKeyHandler.ts29
-rw-r--r--src/client/views/MainView.tsx7
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx20
-rw-r--r--src/client/views/collections/CollectionStackingView.scss5
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx8
-rw-r--r--src/client/views/collections/CollectionVideoView.scss9
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx24
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx1
-rw-r--r--src/client/views/collections/KeyRestrictionRow.tsx3
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx10
-rw-r--r--src/client/views/nodes/FaceRectangles.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx49
-rw-r--r--src/client/views/nodes/VideoBox.tsx88
-rw-r--r--src/client/views/pdf/Annotation.tsx2
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx124
-rw-r--r--src/client/views/presentationview/PresentationList.tsx2
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.scss30
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.tsx100
-rw-r--r--src/client/views/presentationview/PresentationView.scss15
-rw-r--r--src/client/views/presentationview/PresentationView.tsx280
-rw-r--r--src/new_fields/URLField.ts3
-rw-r--r--src/server/Message.ts11
-rw-r--r--src/server/index.ts20
-rw-r--r--src/server/youtubeApi/youtubeApiSample.d.ts2
-rw-r--r--src/server/youtubeApi/youtubeApiSample.js179
34 files changed, 1439 insertions, 200 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index cb460799f..258acd9cd 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,5 +1,5 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore, Diff } from "./../server/Message";
+import { MessageStore, Diff, YoutubeQueryTypes } from "./../server/Message";
import { Opt } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
@@ -156,6 +156,20 @@ export namespace DocServer {
return _GetRefField(id);
}
+ export async function getYoutubeChannels() {
+ let apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
+ return apiKey;
+ }
+
+ export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
+ Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
+ }
+
+ export function getYoutubeVideoDetails(videoIds: string, callBack: (videoDetails: any[]) => void) {
+ Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.VideoDetails, videoIds: videoIds }, callBack);
+ }
+
+
/**
* Given a list of Doc GUIDs, this utility function will asynchronously attempt to each id's associated
* field, first looking in the RefField cache and then communicating with
diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss
new file mode 100644
index 000000000..1fc91a9ae
--- /dev/null
+++ b/src/client/apis/youtube/YoutubeBox.scss
@@ -0,0 +1,124 @@
+ul {
+ list-style-type: none;
+ padding-inline-start: 10px;
+}
+
+
+li {
+ margin: 4px;
+ display: inline-flex;
+}
+
+li:hover {
+ cursor: pointer;
+ opacity: 0.8;
+}
+
+.search_wrapper {
+ width: 100%;
+ display: inline-flex;
+ height: 175px;
+
+ .video_duration {
+ // margin: 0;
+ // padding: 0;
+ border: 0;
+ background: transparent;
+ display: inline-block;
+ position: relative;
+ bottom: 25px;
+ left: 85%;
+ margin: 4px;
+ color: #FFFFFF;
+ background-color: rgba(0, 0, 0, 0.80);
+ padding: 2px 4px;
+ border-radius: 2px;
+ letter-spacing: .5px;
+ font-size: 1.2rem;
+ font-weight: 500;
+ line-height: 1.2rem;
+
+ }
+
+ .textual_info {
+ font-family: Arial, Helvetica, sans-serif;
+
+ .videoTitle {
+ margin-left: 4px;
+ // display: inline-block;
+ color: #0D0D0D;
+ -webkit-line-clamp: 2;
+ display: block;
+ max-height: 4.8rem;
+ overflow: hidden;
+ font-size: 1.8rem;
+ font-weight: 400;
+ line-height: 2.4rem;
+ -webkit-box-orient: vertical;
+ text-overflow: ellipsis;
+ white-space: normal;
+ display: -webkit-box;
+ }
+
+ .channelName {
+ color:#606060;
+ margin-left: 4px;
+ font-size: 1.3rem;
+ font-weight: 400;
+ line-height: 1.8rem;
+ text-transform: none;
+ margin-top: 0px;
+ display: inline-block;
+ }
+
+ .video_description {
+ margin-left: 4px;
+ // font-size: 12px;
+ color: #606060;
+ padding-top: 8px;
+ margin-bottom: 8px;
+ display: block;
+ line-height: 1.8rem;
+ max-height: 4.2rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+ .publish_time {
+ //display: inline-block;
+ margin-left: 8px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: #606060;
+ max-width: 100%;
+ line-height: 1.8rem;
+ max-height: 3.6rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+ .viewCount {
+
+ margin-left: 8px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: #606060;
+ max-width: 100%;
+ line-height: 1.8rem;
+ max-height: 3.6rem;
+ overflow: hidden;
+ font-size: 1.3rem;
+ font-weight: 400;
+ text-transform: none;
+ }
+
+
+
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
new file mode 100644
index 000000000..7f9a3ad70
--- /dev/null
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -0,0 +1,362 @@
+import { action, observable, runInAction } from 'mobx';
+import { observer } from "mobx-react";
+import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs } from "../../documents/Documents";
+import { DocumentDecorations } from "../../views/DocumentDecorations";
+import { InkingControl } from "../../views/InkingControl";
+import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
+import "../../views/nodes/WebBox.scss";
+import "./YoutubeBox.scss";
+import React = require("react");
+
+interface VideoTemplate {
+ thumbnailUrl: string;
+ videoTitle: string;
+ videoId: string;
+ duration: string;
+ channelTitle: string;
+ viewCount: string;
+ publishDate: string;
+ videoDescription: string;
+}
+
+/**
+ * This class models the youtube search document that can be dropped on to canvas.
+ */
+@observer
+export class YoutubeBox extends React.Component<FieldViewProps> {
+
+ @observable YoutubeSearchElement: HTMLInputElement | undefined;
+ @observable searchResultsFound: boolean = false;
+ @observable searchResults: any[] = [];
+ @observable videoClicked: boolean = false;
+ @observable selectedVideoUrl: string = "";
+ @observable lisOfBackUp: JSX.Element[] = [];
+ @observable videoIds: string | undefined;
+ @observable videoDetails: any[] = [];
+ @observable curVideoTemplates: VideoTemplate[] = [];
+
+
+ public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+
+ /**
+ * When component mounts, last search's results are laoded in based on the back up stored
+ * in the document of the props.
+ */
+ async componentWillMount() {
+ //DocServer.getYoutubeChannels();
+ let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
+ let awaitedBackUp = await castedSearchBackUp;
+ let castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
+ let awaitedDetails = await castedDetailBackUp;
+
+
+ if (awaitedBackUp) {
+
+
+ let jsonList = await DocListCastAsync(awaitedBackUp!.json);
+ let jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
+
+ if (jsonList!.length !== 0) {
+ runInAction(() => this.searchResultsFound = true);
+ let index = 0;
+ //getting the necessary information from backUps and building templates that will be used to map in render
+ for (let video of jsonList!) {
+
+ let videoId = await Cast(video.id, Doc);
+ let id = StrCast(videoId!.videoId);
+ let snippet = await Cast(video.snippet, Doc);
+ let videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
+ let thumbnail = await Cast(snippet!.thumbnails, Doc);
+ let thumbnailMedium = await Cast(thumbnail!.medium, Doc);
+ let thumbnailUrl = StrCast(thumbnailMedium!.url);
+ let videoDescription = StrCast(snippet!.description);
+ let pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!;
+ let channelTitle = StrCast(snippet!.channelTitle);
+ let duration: string;
+ let viewCount: string;
+ if (jsonDetailList!.length !== 0) {
+ let contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
+ let statistics = await Cast(jsonDetailList![index].statistics, Doc);
+ duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration));
+ viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!;
+ }
+ index = index + 1;
+ let newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration!, viewCount: viewCount! };
+ runInAction(() => this.curVideoTemplates.push(newTemplate));
+ }
+ }
+ }
+ }
+
+ _ignore = 0;
+ onPreWheel = (e: React.WheelEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPrePointer = (e: React.PointerEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPostPointer = (e: React.PointerEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+ onPostWheel = (e: React.WheelEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Function that submits the title entered by user on enter press.
+ */
+ onEnterKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let submittedTitle = this.YoutubeSearchElement!.value;
+ this.YoutubeSearchElement!.value = "";
+ this.YoutubeSearchElement!.blur();
+ DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults);
+
+ }
+ }
+
+ /**
+ * The callback that is passed in to server, which functions as a way to
+ * get videos that is returned by search. It also makes a call to server
+ * to get details for the videos found.
+ */
+ @action
+ processesVideoResults = (videos: any[]) => {
+ this.searchResults = videos;
+ if (this.searchResults.length > 0) {
+ this.searchResultsFound = true;
+ this.videoIds = "";
+ videos.forEach((video) => {
+ if (this.videoIds === "") {
+ this.videoIds = video.id.videoId;
+ } else {
+ this.videoIds = this.videoIds! + ", " + video.id.videoId;
+ }
+ });
+ //Asking for details that include duration and viewCount from server for videoIds
+ DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails);
+ this.backUpSearchResults(videos);
+ if (this.videoClicked) {
+ this.videoClicked = false;
+ }
+ }
+ }
+
+ /**
+ * The callback that is given to server to process and receive returned details about the videos.
+ */
+ @action
+ processVideoDetails = (videoDetails: any[]) => {
+ this.videoDetails = videoDetails;
+ this.props.Document.cachedDetails = Docs.Get.DocumentHierarchyFromJson(videoDetails, "detailBackUp");
+ }
+
+ /**
+ * The function that stores the search results in the props document.
+ */
+ backUpSearchResults = (videos: any[]) => {
+ this.props.Document.cachedSearchResults = Docs.Get.DocumentHierarchyFromJson(videos, "videosBackUp");
+ }
+
+ /**
+ * The function that filters out escaped characters returned by the api
+ * in the title of the videos.
+ */
+ filterYoutubeTitleResult = (resultTitle: string) => {
+ let processedTitle: string = resultTitle.ReplaceAll("&amp;", "&");
+ processedTitle = processedTitle.ReplaceAll("&#39;", "'");
+ processedTitle = processedTitle.ReplaceAll("&quot;", "\"");
+ return processedTitle;
+ }
+
+
+
+ /**
+ * The function that converts ISO date, which is passed in, to normal date and finds the
+ * difference between today's date and that date, in terms of "ago" to imitate youtube.
+ */
+ roundPublishTime = (publishTime: string) => {
+ let date = new Date(publishTime).getTime();
+ let curDate = new Date().getTime();
+ let timeDif = curDate - date;
+ let totalSeconds = timeDif / 1000;
+ let totalMin = totalSeconds / 60;
+ let totalHours = totalMin / 60;
+ let totalDays = totalHours / 24;
+ let totalMonths = totalDays / 30.417;
+ let totalYears = totalMonths / 12;
+
+
+ let truncYears = Math.trunc(totalYears);
+ let truncMonths = Math.trunc(totalMonths);
+ let truncDays = Math.trunc(totalDays);
+ let truncHours = Math.trunc(totalHours);
+ let truncMin = Math.trunc(totalMin);
+ let truncSec = Math.trunc(totalSeconds);
+
+ let pluralCase = "";
+
+ if (truncYears !== 0) {
+ truncYears > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncYears + " year" + pluralCase + " ago";
+ } else if (truncMonths !== 0) {
+ truncMonths > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncMonths + " month" + pluralCase + " ago";
+ } else if (truncDays !== 0) {
+ truncDays > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncDays + " day" + pluralCase + " ago";
+ } else if (truncHours !== 0) {
+ truncHours > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncHours + " hour" + pluralCase + " ago";
+ } else if (truncMin !== 0) {
+ truncMin > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncMin + " minute" + pluralCase + " ago";
+ } else if (truncSec !== 0) {
+ truncSec > 1 ? pluralCase = "s" : pluralCase = "";
+ return truncSec + " second" + pluralCase + " ago";
+ }
+ }
+
+ /**
+ * The function that converts the passed in ISO time to normal duration time.
+ */
+ convertIsoTimeToDuration = (isoDur: string) => {
+
+ let convertedTime = isoDur.replace(/D|H|M/g, ":").replace(/P|T|S/g, "").split(":");
+
+ if (1 === convertedTime.length) {
+ 2 !== convertedTime[0].length && (convertedTime[0] = "0" + convertedTime[0]), convertedTime[0] = "0:" + convertedTime[0];
+ } else {
+ for (var r = 1, l = convertedTime.length - 1; l >= r; r++) {
+ 2 !== convertedTime[r].length && (convertedTime[r] = "0" + convertedTime[r]);
+ }
+ }
+
+ return convertedTime.join(":");
+ }
+
+ /**
+ * The function that rounds the viewCount to the nearest
+ * thousand, million or billion, given a viewCount number.
+ */
+ abbreviateViewCount = (viewCount: number) => {
+ if (viewCount < 1000) {
+ return viewCount.toString();
+ } else if (viewCount >= 1000 && viewCount < 1000000) {
+ return (Math.trunc(viewCount / 1000)) + "K";
+ } else if (viewCount >= 1000000 && viewCount < 1000000000) {
+ return (Math.trunc(viewCount / 1000000)) + "M";
+ } else if (viewCount >= 1000000000) {
+ return (Math.trunc(viewCount / 1000000000)) + "B";
+ }
+ }
+
+ /**
+ * The function that is called to decide on what'll be rendered by the component.
+ * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates
+ * generated by the backUps. If none present, renders nothing.
+ */
+ renderSearchResultsOrVideo = () => {
+ if (this.searchResultsFound) {
+ if (this.searchResults.length !== 0) {
+ return <ul>
+ {this.searchResults.map((video, index) => {
+ let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
+ let channelTitle = video.snippet.channelTitle;
+ let videoDescription = video.snippet.description;
+ let pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
+ let duration;
+ let viewCount;
+ if (this.videoDetails.length !== 0) {
+ duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration);
+ viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount);
+ }
+
+
+ return <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
+ <div className="search_wrapper">
+ <div style={{ backgroundColor: "yellow" }}>
+ <img src={video.snippet.thumbnails.medium.url} />
+ <span className="video_duration">{duration}</span>
+ </div>
+ <div className="textual_info">
+ <span className="videoTitle">{filteredTitle}</span>
+ <span className="channelName">{channelTitle}</span>
+ <span className="viewCount">{viewCount}</span>
+ <span className="publish_time">{pusblishDate}</span>
+ <p className="video_description">{videoDescription}</p>
+
+ </div>
+ </div>
+ </li>;
+ })}
+ </ul>;
+ } else if (this.curVideoTemplates.length !== 0) {
+ return <ul>
+ {this.curVideoTemplates.map((video: VideoTemplate) => {
+ return <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}>
+ <div className="search_wrapper">
+ <div style={{ backgroundColor: "yellow" }}>
+ <img src={video.thumbnailUrl} />
+ <span className="video_duration">{video.duration}</span>
+ </div>
+ <div className="textual_info">
+ <span className="videoTitle">{video.videoTitle}</span>
+ <span className="channelName">{video.channelTitle}</span>
+ <span className="viewCount">{video.viewCount}</span>
+ <span className="publish_time">{video.publishDate}</span>
+ <p className="video_description">{video.videoDescription}</p>
+ </div>
+ </div>
+ </li>;
+ })}
+ </ul>;
+ }
+ } else {
+ return (null);
+ }
+ }
+
+ /**
+ * Given a videoId and title, creates a new youtube embedded url, and uses that
+ * to create a new video document.
+ */
+ @action
+ embedVideoOnClick = (videoId: string, filteredTitle: string) => {
+ let embeddedUrl = "https://www.youtube.com/embed/" + videoId;
+ this.selectedVideoUrl = embeddedUrl;
+ let addFunction = this.props.addDocument!;
+ let newVideoX = NumCast(this.props.Document.x);
+ let newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
+
+ addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY }));
+ this.videoClicked = true;
+ }
+
+ render() {
+ let content =
+ <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: "100%", border: "1px solid black", padding: 5, textAlign: "center" }} ref={(e) => this.YoutubeSearchElement = e!} />
+ {this.renderSearchResultsOrVideo()}
+ </div>;
+
+ let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+
+ let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+ return (
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ </>);
+ }
+} \ No newline at end of file
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index bbc438a9b..c118d91d3 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -1,15 +1,11 @@
import * as request from "request-promise";
import { Doc, Field, Opt } from "../../new_fields/Doc";
import { Cast } from "../../new_fields/Types";
-import { ImageField } from "../../new_fields/URLField";
-import { List } from "../../new_fields/List";
import { Docs } from "../documents/Documents";
import { RouteStore } from "../../server/RouteStore";
import { Utils } from "../../Utils";
-import { CompileScript } from "../util/Scripting";
-import { ComputedField } from "../../new_fields/ScriptField";
import { InkData } from "../../new_fields/InkField";
-import { undoBatch, UndoManager } from "../util/UndoManager";
+import { UndoManager } from "../util/UndoManager";
type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor, analyzer: AnalysisApplier };
type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>;
@@ -42,7 +38,7 @@ export enum Confidence {
*/
export namespace CognitiveServices {
- const executeQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => {
+ const ExecuteQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => {
return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
let apiKey = await response.text();
if (!apiKey) {
@@ -103,15 +99,15 @@ export namespace CognitiveServices {
return request.post(options);
},
- analyzer: async (target: Doc, keys: string[], service: Service, converter: Converter) => {
+ analyzer: async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => {
let batch = UndoManager.StartBatch("Image Analysis");
- let imageData = Cast(target.data, ImageField);
+
let storageKey = keys[0];
- if (!imageData || await Cast(target[storageKey], Doc)) {
+ if (!url || await Cast(target[storageKey], Doc)) {
return;
}
let toStore: any;
- let results = await executeQuery<string, any>(service, Manager, imageData.url.href);
+ let results = await ExecuteQuery<string, any>(service, Manager, url);
if (!results) {
toStore = "Cognitive Services could not process the given image URL.";
} else {
@@ -122,6 +118,7 @@ export namespace CognitiveServices {
}
}
target[storageKey] = toStore;
+
batch.end();
}
@@ -129,31 +126,6 @@ export namespace CognitiveServices {
export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle };
- export const generateMetadata = async (target: Doc, threshold: Confidence = Confidence.Excellent) => {
- let converter = (results: any) => {
- let tagDoc = new Doc;
- results.tags.map((tag: Tag) => {
- 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));
- });
- tagDoc.title = "Generated Tags";
- tagDoc.confidence = threshold;
- return tagDoc;
- };
- Manager.analyzer(target, ["generatedTags"], Service.ComputerVision, converter);
- };
-
- export const extractFaces = async (target: Doc) => {
- let converter = (results: any) => {
- let faceDocs = new List<Doc>();
- results.map((face: Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));
- return faceDocs;
- };
- Manager.analyzer(target, ["faces"], Service.Face, converter);
- };
-
}
export namespace Inking {
@@ -209,7 +181,8 @@ export namespace CognitiveServices {
analyzer: async (target: Doc, keys: string[], inkData: InkData) => {
let batch = UndoManager.StartBatch("Ink Analysis");
- let results = await executeQuery<InkData, any>(Service.Handwriting, Manager, inkData);
+
+ let results = await ExecuteQuery<InkData, any>(Service.Handwriting, Manager, inkData);
if (results) {
results.recognitionUnits && (results = results.recognitionUnits);
target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis");
@@ -217,6 +190,7 @@ export namespace CognitiveServices {
let individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
target[keys[1]] = individualWords.join(" ");
}
+
batch.end();
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d1b3071ed..2dcf655e3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -22,7 +22,7 @@ import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
import { OmitKeys, JSONUtils } from "../../Utils";
-import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
+import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
import { Cast, NumCast, StrCast, ToConstructor, InterfaceValue, FieldValue } from "../../new_fields/Types";
@@ -33,6 +33,7 @@ import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
+import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { LinkManager } from "../util/LinkManager";
import { DocumentManager } from "../util/DocumentManager";
@@ -60,7 +61,8 @@ export enum DocumentType {
LINKDOC = "linkdoc",
BUTTON = "button",
TEMPLATE = "template",
- EXTENSION = "extension"
+ EXTENSION = "extension",
+ YOUTUBE = "youtube",
}
export interface DocumentOptions {
@@ -165,6 +167,10 @@ export namespace Docs {
[DocumentType.LINKDOC, {
data: new List<Doc>(),
layout: { view: EmptyBox },
+ options: {}
+ }],
+ [DocumentType.YOUTUBE, {
+ layout: { view: YoutubeBox }
}],
[DocumentType.BUTTON, {
layout: { view: ButtonBox },
@@ -340,6 +346,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
}
+ export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
+ }
+
export function AudioDocument(url: string, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
}
@@ -505,7 +515,7 @@ export namespace Docs {
const convertObject = (object: any, title?: string): Doc => {
let target = new Doc(), result: Opt<Field>;
Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result));
- title && (target.title = title);
+ title && !target.title && (target.title = title);
return target;
};
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
new file mode 100644
index 000000000..b58bdb6c7
--- /dev/null
+++ b/src/client/util/DictationManager.ts
@@ -0,0 +1,39 @@
+namespace CORE {
+ export interface IWindow extends Window {
+ webkitSpeechRecognition: any;
+ }
+}
+
+const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+
+export default class DictationManager {
+ public static Instance = new DictationManager();
+ private isListening = false;
+ private recognizer: any;
+
+ constructor() {
+ this.recognizer = new webkitSpeechRecognition();
+ this.recognizer.interimResults = false;
+ this.recognizer.continuous = true;
+ }
+
+ finish = (handler: any, data: any) => {
+ handler(data);
+ this.isListening = false;
+ this.recognizer.stop();
+ }
+
+ listen = () => {
+ if (this.isListening) {
+ return undefined;
+ }
+ this.isListening = true;
+ this.recognizer.start();
+ return new Promise<string>((resolve, reject) => {
+ this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript);
+ this.recognizer.onerror = (e: any) => this.finish(reject, e);
+ });
+
+ }
+
+} \ No newline at end of file
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>
);
}
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index d935a61af..b9ad96450 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -42,4 +42,5 @@ export abstract class URLField extends ObjectField {
@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
-@scriptingGlobal @Deserializable("web") export class WebField extends URLField { } \ No newline at end of file
+@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
+@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 19e0a48aa..aaee143e8 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -24,6 +24,16 @@ export interface Transferable {
readonly data?: any;
}
+export enum YoutubeQueryTypes {
+ Channels, SearchVideo, VideoDetails
+}
+
+export interface YoutubeQueryInput {
+ readonly type: YoutubeQueryTypes;
+ readonly userInput?: string;
+ readonly videoIds?: string;
+}
+
export interface Reference {
readonly id: string;
}
@@ -45,6 +55,7 @@ export namespace MessageStore {
export const GetRefFields = new Message<string[]>("Get Ref Fields");
export const UpdateField = new Message<Diff>("Update Ref Field");
export const CreateField = new Message<Reference>("Create Ref Field");
+ export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 2fa5132d0..281fc1eb2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -25,7 +25,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Types, Diff, Message } from "./Message";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message";
import { RouteStore } from './RouteStore';
import v4 = require('uuid/v4');
const app = express();
@@ -42,6 +42,7 @@ import { debug } from 'util';
import _ = require('lodash');
import * as Archiver from 'archiver';
import * as AdmZip from 'adm-zip';
+import * as YoutubeApi from './youtubeApi/youtubeApiSample.js';
import { Response } from 'express-serve-static-core';
import { DocComponent } from '../client/views/DocComponent';
const MongoStore = require('connect-mongo')(session);
@@ -49,6 +50,8 @@ const mongoose = require('mongoose');
const probe = require("probe-image-size");
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
+let youtubeApiKey: string;
+YoutubeApi.readApiKey((apiKey: string) => youtubeApiKey = apiKey);
const release = process.env.RELEASE === "true";
if (process.env.RELEASE === "true") {
@@ -670,6 +673,7 @@ server.on("connection", function (socket: Socket) {
}
Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
@@ -724,6 +728,17 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v
Database.Instance.getDocuments(ids, callback, "newDocuments");
}
+function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
+ switch (query.type) {
+ case YoutubeQueryType.Channels:
+ YoutubeApi.authorizedGetChannel(youtubeApiKey);
+ break;
+ case YoutubeQueryType.SearchVideo:
+ YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback);
+ case YoutubeQueryType.VideoDetails:
+ YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback);
+ }
+}
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
@@ -823,4 +838,5 @@ function CreateField(newValue: any) {
}
server.listen(serverPort);
-console.log(`listening on port ${serverPort}`); \ No newline at end of file
+console.log(`listening on port ${serverPort}`);
+
diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/youtubeApi/youtubeApiSample.d.ts
new file mode 100644
index 000000000..427f54608
--- /dev/null
+++ b/src/server/youtubeApi/youtubeApiSample.d.ts
@@ -0,0 +1,2 @@
+declare const YoutubeApi: any;
+export = YoutubeApi; \ No newline at end of file
diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js
new file mode 100644
index 000000000..50b3c7b38
--- /dev/null
+++ b/src/server/youtubeApi/youtubeApiSample.js
@@ -0,0 +1,179 @@
+const fs = require('fs');
+const readline = require('readline');
+const { google } = require('googleapis');
+const OAuth2 = google.auth.OAuth2;
+
+
+// If modifying these scopes, delete your previously saved credentials
+// at ~/.credentials/youtube-nodejs-quickstart.json
+let SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
+let TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
+ process.env.USERPROFILE) + '/.credentials/';
+let TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
+
+module.exports.readApiKey = (callback) => {
+ fs.readFile('client_secret.json', function processClientSecrets(err, content) {
+ if (err) {
+ console.log('Error loading client secret file: ' + err);
+ return;
+ }
+ callback(content);
+ });
+}
+
+module.exports.authorizedGetChannel = (apiKey) => {
+ //this didnt get called
+ // Authorize a client with the loaded credentials, then call the YouTube API.
+ authorize(JSON.parse(apiKey), getChannel);
+}
+
+module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => {
+ authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack });
+}
+
+module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => {
+ authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack });
+}
+
+
+/**
+ * Create an OAuth2 client with the given credentials, and then execute the
+ * given callback function.
+ *
+ * @param {Object} credentials The authorization client credentials.
+ * @param {function} callback The callback to call with the authorized client.
+ */
+function authorize(credentials, callback, args = {}) {
+ let clientSecret = credentials.installed.client_secret;
+ let clientId = credentials.installed.client_id;
+ let redirectUrl = credentials.installed.redirect_uris[0];
+ let oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
+
+ // Check if we have previously stored a token.
+ fs.readFile(TOKEN_PATH, function (err, token) {
+ if (err) {
+ getNewToken(oauth2Client, callback);
+ } else {
+ oauth2Client.credentials = JSON.parse(token);
+ callback(oauth2Client, args);
+ }
+ });
+}
+
+/**
+ * Get and store new token after prompting for user authorization, and then
+ * execute the given callback with the authorized OAuth2 client.
+ *
+ * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
+ * @param {getEventsCallback} callback The callback to call with the authorized
+ * client.
+ */
+function getNewToken(oauth2Client, callback) {
+ var authUrl = oauth2Client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES
+ });
+ console.log('Authorize this app by visiting this url: ', authUrl);
+ var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+ rl.question('Enter the code from that page here: ', function (code) {
+ rl.close();
+ oauth2Client.getToken(code, function (err, token) {
+ if (err) {
+ console.log('Error while trying to retrieve access token', err);
+ return;
+ }
+ oauth2Client.credentials = token;
+ storeToken(token);
+ callback(oauth2Client);
+ });
+ });
+}
+
+/**
+ * Store token to disk be used in later program executions.
+ *
+ * @param {Object} token The token to store to disk.
+ */
+function storeToken(token) {
+ try {
+ fs.mkdirSync(TOKEN_DIR);
+ } catch (err) {
+ if (err.code != 'EEXIST') {
+ throw err;
+ }
+ }
+ fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
+ if (err) throw err;
+ console.log('Token stored to ' + TOKEN_PATH);
+ });
+ console.log('Token stored to ' + TOKEN_PATH);
+}
+
+/**
+ * Lists the names and IDs of up to 10 files.
+ *
+ * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
+ */
+function getChannel(auth) {
+ var service = google.youtube('v3');
+ service.channels.list({
+ auth: auth,
+ part: 'snippet,contentDetails,statistics',
+ forUsername: 'GoogleDevelopers'
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error: ' + err);
+ return;
+ }
+ var channels = response.data.items;
+ if (channels.length == 0) {
+ console.log('No channel found.');
+ } else {
+ console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
+ 'it has %s views.',
+ channels[0].id,
+ channels[0].snippet.title,
+ channels[0].statistics.viewCount);
+ }
+ });
+}
+
+function getVideos(auth, args) {
+ let service = google.youtube('v3');
+ service.search.list({
+ auth: auth,
+ part: 'id, snippet',
+ type: 'video',
+ q: args.userInput,
+ maxResults: 10
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error: ' + err);
+ return;
+ }
+ let videos = response.data.items;
+ args.callBack(videos);
+ });
+}
+
+function getVideoDetails(auth, args) {
+ if (args.videoIds === undefined) {
+ return;
+ }
+ let service = google.youtube('v3');
+ service.videos.list({
+ auth: auth,
+ part: 'contentDetails, statistics',
+ id: args.videoIds
+ }, function (err, response) {
+ if (err) {
+ console.log('The API returned an error from details: ' + err);
+ return;
+ }
+ let videoDetails = response.data.items;
+ args.callBack(videoDetails);
+ });
+} \ No newline at end of file