aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/DocServer.ts16
-rw-r--r--src/client/apis/youtube/YoutubeBox.scss124
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx366
-rw-r--r--src/client/documents/Documents.ts16
-rw-r--r--src/client/views/MainView.tsx6
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
6 files changed, 525 insertions, 6 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 077c8e5ba..4cea68f69 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';
@@ -158,6 +158,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..019d191bc
--- /dev/null
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -0,0 +1,366 @@
+import "../../views/nodes/WebBox.scss";
+import React = require("react");
+import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
+import { HtmlField } from "../../../new_fields/HtmlField";
+import { WebField } from "../../../new_fields/URLField";
+import { observer } from "mobx-react";
+import { computed, reaction, IReactionDisposer, observable, action, runInAction } from 'mobx';
+import { DocumentDecorations } from "../../views/DocumentDecorations";
+import { InkingControl } from "../../views/InkingControl";
+import { Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import "./YoutubeBox.scss";
+import { Docs } from "../../documents/Documents";
+import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { List } from "../../../new_fields/List";
+
+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/documents/Documents.ts b/src/client/documents/Documents.ts
index a5b1d6573..8a2bc9b85 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";
@@ -56,6 +57,7 @@ export enum DocumentType {
IMPORT = "import",
LINK = "link",
LINKDOC = "linkdoc",
+ YOUTUBE = "youtube",
TEMPLATE = "template",
EXTENSION = "extension"
}
@@ -162,7 +164,11 @@ export namespace Docs {
data: new List<Doc>(),
layout: { view: EmptyBox },
options: {}
- }]
+ }],
+ [DocumentType.YOUTUBE, {
+ layout: { view: YoutubeBox }
+ }],
+
]);
// All document prototypes are initialized with at least these values
@@ -332,6 +338,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);
}
@@ -489,7 +499,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/views/MainView.tsx b/src/client/views/MainView.tsx
index 53be0278e..6feb93086 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, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat } 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';
@@ -123,6 +123,7 @@ export class MainView extends React.Component {
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
+ library.add(faPlay);
library.add(faClone);
library.add(faCut);
library.add(faCommentAlt);
@@ -379,11 +380,14 @@ export class MainView extends React.Component {
let addTreeNode = action(() => CurrentUserUtils.UserDocument);
let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
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>(), "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/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ef65c12cf..36d7b38f4 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -17,6 +17,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";
@@ -97,7 +98,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, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}