From 0f034260b18dc06f1a1267af8707481eff8ac21a Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 25 Jun 2019 16:58:02 -0400 Subject: Youtube Api Example Js setup --- src/client/apis/youtube/youtubeApiSample.d.ts | 3 + src/client/apis/youtube/youtubeApiSample.js | 128 +++++++++++++++++++++ .../views/presentationview/PresentationElement.tsx | 3 - 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/client/apis/youtube/youtubeApiSample.d.ts create mode 100644 src/client/apis/youtube/youtubeApiSample.js (limited to 'src') diff --git a/src/client/apis/youtube/youtubeApiSample.d.ts b/src/client/apis/youtube/youtubeApiSample.d.ts new file mode 100644 index 000000000..87a669e36 --- /dev/null +++ b/src/client/apis/youtube/youtubeApiSample.d.ts @@ -0,0 +1,3 @@ + +declare const YoutubeApi: any; +export = YoutubeApi; \ No newline at end of file diff --git a/src/client/apis/youtube/youtubeApiSample.js b/src/client/apis/youtube/youtubeApiSample.js new file mode 100644 index 000000000..07c3add36 --- /dev/null +++ b/src/client/apis/youtube/youtubeApiSample.js @@ -0,0 +1,128 @@ +let fs = require('fs'); +let readline = require('readline'); +let { google } = require('googleapis'); +let 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'; + +function readFsFile() { + // Load client secrets from a local file. + fs.readFile('client_secret.json', function processClientSecrets(err, content) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + // Authorize a client with the loaded credentials, then call the YouTube API. + authorize(JSON.parse(content), getChannel); + }); +} + +/** + * 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) { + 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); + } + }); +} + +/** + * 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); + } + }); +} \ No newline at end of file diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 07cdcd43a..5818519de 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -13,9 +13,6 @@ import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; - - - library.add(faArrowUp); library.add(fileSolid); library.add(fileRegular); -- cgit v1.2.3-70-g09d2 From b285803c4e8c37302f6e02624a6127667d628305 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 25 Jun 2019 19:49:54 -0400 Subject: Youtube Api Exploration --- package.json | 2 + src/client/DocServer.ts | 6 +++ src/client/apis/youtube/YoutubeBox.tsx | 72 +++++++++++++++++++++++++ src/client/apis/youtube/youtubeApiSample.js | 22 +++----- src/client/documents/Documents.ts | 20 ++++++- src/client/views/MainView.tsx | 6 ++- src/client/views/nodes/DocumentContentsView.tsx | 3 +- src/new_fields/URLField.ts | 3 +- src/server/Message.ts | 1 + src/server/index.ts | 20 ++++++- 10 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 src/client/apis/youtube/YoutubeBox.tsx (limited to 'src') diff --git a/package.json b/package.json index 2371d530e..2b1c8f262 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "bluebird": "^3.5.3", "body-parser": "^1.18.3", "bootstrap": "^4.3.1", + "child_process": "^1.0.2", "class-transformer": "^0.2.0", "connect-flash": "^0.1.1", "connect-mongo": "^2.0.3", @@ -171,6 +172,7 @@ "react-simple-dropdown": "^3.2.3", "react-split-pane": "^0.1.85", "react-table": "^6.9.2", + "readline": "^1.3.0", "request": "^2.88.0", "request-image-size": "^2.1.0", "request-promise": "^4.2.4", diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index cbcf751ee..c9cbce78e 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -47,6 +47,12 @@ export namespace DocServer { } } + export async function getYoutubeApiKey() { + let apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiKey, undefined); + return apiKey; + } + + export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt }> { const requestedIds: string[] = []; const waitingIds: string[] = []; diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx new file mode 100644 index 000000000..ee190750f --- /dev/null +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -0,0 +1,72 @@ +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 } from 'mobx'; +import { DocumentDecorations } from "../../views/DocumentDecorations"; +import { InkingControl } from "../../views/InkingControl"; +import * as YoutubeApi from "./youtubeApiSample"; +import { Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; + + +@observer +export class YoutubeBox extends React.Component { + + private youtubeApiKey: string = ""; + + public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } + + async componentWillMount() { + let apiKey = await DocServer.getYoutubeApiKey(); + this.youtubeApiKey = apiKey; + YoutubeApi.authorizedGetChannel(this.youtubeApiKey); + } + + _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(); + } + } + render() { + let field = this.props.Document[this.props.fieldKey]; + let view; + YoutubeApi.readFsFile(); + if (field instanceof HtmlField) { + view = ; + } else if (field instanceof WebField) { + view = ; } else { return (null); } } + @action + embedVideoOnClick = (videoId: string) => { + let embeddedUrl = "https://www.youtube.com/embed/" + videoId; + this.selectedVideoUrl = embeddedUrl; + this.searchResultsFound = false; + this.videoClicked = true; + } + render() { let field = this.props.Document[this.props.fieldKey]; let content =
- this.YoutubeSearchElement = e!} /> - {this.renderSearchResults()} + this.YoutubeSearchElement = e!} /> + {this.renderSearchResultsOrVideo()}
; let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; -- cgit v1.2.3-70-g09d2 From f70b95879e87a6bb61aaae5de29747d9474623a7 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 27 Jun 2019 17:14:15 -0400 Subject: css for youtubeBox --- src/client/apis/youtube/YoutubeBox.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss index 962f814a2..e6ccfea90 100644 --- a/src/client/apis/youtube/YoutubeBox.scss +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -1,3 +1,13 @@ +ul { + list-style-type: none; +} + + li { margin: 4px; +} + +li:hover { + cursor: pointer; + opacity: 0.8; } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 9c868077db569a606ec465557d9d693bcd3abd34 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 16 Jul 2019 19:31:40 -0400 Subject: Merge Fixed --- package.json | 2 +- src/client/views/InkingControl.tsx | 4 ++-- src/client/views/MainView.tsx | 4 ++-- src/client/views/nodes/FormattedTextBox.tsx | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index 376d3db77..28c975500 100644 --- a/package.json +++ b/package.json @@ -205,4 +205,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} \ No newline at end of file +} diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index c7f7bdb66..1910e409b 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,5 +1,5 @@ import { observable, action, computed, runInAction } from "mobx"; -import { ColorResult } from 'react-color'; +import { ColorState } from 'react-color'; import React = require("react"); import { observer } from "mobx-react"; import "./InkingControl.scss"; @@ -41,7 +41,7 @@ export class InkingControl extends React.Component { } @undoBatch - switchColor = action((color: ColorResult): void => { + switchColor = action((color: ColorState): void => { this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); if (InkingControl.Instance.selectedTool === InkTool.None) { if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 5a2e0c6c3..18e98a5cc 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'; @@ -381,7 +381,7 @@ export class MainView extends React.Component { 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.YoutubeDocument(youtubeurl, { width: 200, height: 200, title: "youtube node" })); + let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 200, height: 200, title: "youtube node" })); let btns: [React.RefObject, IconName, string, () => Doc][] = [ [React.createRef(), "object-group", "Add Collection", addColNode], diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 45e7171d2..3e15f2ca9 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -308,7 +308,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; } if (href) { - `` if (href.indexOf(DocServer.prepend("/doc/")) === 0) { this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { -- cgit v1.2.3-70-g09d2 From 49edd4e6071d0ea84cd0a652d69acb826866c99b Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 17 Jul 2019 17:50:45 -0400 Subject: New VideoBox spawns when a youtube video search result is clicked --- src/client/apis/youtube/YoutubeBox.tsx | 31 ++++++++++++++++++++++++------- src/server/youtubeApi/youtubeApiSample.js | 2 +- 2 files changed, 25 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 3e7e9e06d..e7913da9e 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -11,6 +11,7 @@ import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { NumCast } from "../../../new_fields/Types"; import "./YoutubeBox.scss"; +import { Docs } from "../../documents/Documents"; @observer @@ -25,7 +26,7 @@ export class YoutubeBox extends React.Component { public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } componentWillMount() { - DocServer.getYoutubeChannels(); + //DocServer.getYoutubeChannels(); } _ignore = 0; @@ -59,34 +60,50 @@ export class YoutubeBox extends React.Component { @action processesVideoResults = (videos: any[]) => { this.searchResults = videos; + console.log("Results: ", this.searchResults); if (this.searchResults.length > 0) { this.searchResultsFound = true; - this.searchResults.forEach((video) => console.log("Image Url", video.snippet)); if (this.videoClicked) { this.videoClicked = false; } } } + filterYoutubeTitleResult = (resultTitle: string) => { + let processedTitle: string = resultTitle.ReplaceAll("&", "&"); + processedTitle = processedTitle.ReplaceAll("'", "'"); + processedTitle = processedTitle.ReplaceAll(""", "\""); + return processedTitle; + } + renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { return
    {this.searchResults.map((video) => { - return
  • this.embedVideoOnClick(video.id.videoId)} key={video.id.videoId}> {video.snippet.title}
  • ; + let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); + return
  • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
  • ; })}
; - } else if (this.videoClicked) { - return ; + // } else if (this.videoClicked) { + // return ; + // } } else { return (null); } } @action - embedVideoOnClick = (videoId: string) => { + embedVideoOnClick = (videoId: string, filteredTitle: string) => { let embeddedUrl = "https://www.youtube.com/embed/" + videoId; this.selectedVideoUrl = embeddedUrl; - this.searchResultsFound = false; + let addFunction = this.props.addDocument!; + let newVideoX = NumCast(this.props.Document.x) + NumCast(this.props.Document.width); + 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.props.addDocument(Docs.Create.VideoDocument(embeddedUrl, { title: embeddedUrl, width: 400, height: 315 })); + //this.searchResultsFound = false; this.videoClicked = true; } diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index e95f99015..f875812d5 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -153,7 +153,7 @@ function getSampleVideos(auth, args) { return; } let videos = response.data.items; - console.log('Videos found: ' + videos[0].id.videoId, " ", videos[0].snippet.title); + console.log('Videos found: ' + videos[0].id.videoId, " ", unescape(videos[0].snippet.title)); args.callBack(videos); }); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 1f1f857847cd9ffa0fdd5001c0dd72f06ba903c0 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 17 Jul 2019 20:26:00 -0400 Subject: Titles alligned --- src/client/apis/youtube/YoutubeBox.scss | 6 ++++++ src/client/apis/youtube/YoutubeBox.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss index e6ccfea90..23f264b95 100644 --- a/src/client/apis/youtube/YoutubeBox.scss +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -5,9 +5,15 @@ ul { li { margin: 4px; + display: inline-flex; } li:hover { cursor: pointer; opacity: 0.8; +} + +.videoTitle { + margin-left: 4px; + font-family: Arial, Helvetica, sans-serif; } \ No newline at end of file diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index e7913da9e..d94f3785c 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -81,7 +81,7 @@ export class YoutubeBox extends React.Component { return
    {this.searchResults.map((video) => { let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - return
  • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
  • ; + return
  • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
  • ; })}
; // } else if (this.videoClicked) { -- cgit v1.2.3-70-g09d2 From 808443664d66dc5009aaace48420659cf98c6c47 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 18 Jul 2019 13:31:17 -0400 Subject: Youtube Search Results Backed Up, Key error is present --- src/client/apis/youtube/YoutubeBox.tsx | 112 +++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index d94f3785c..fa2d3fb53 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -4,14 +4,17 @@ 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 } from 'mobx'; +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 } from "../../../new_fields/Types"; +import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; import "./YoutubeBox.scss"; import { Docs } from "../../documents/Documents"; +import { Doc } from "../../../new_fields/Doc"; +import { listSpec } from "../../../new_fields/Schema"; +import { List } from "../../../new_fields/List"; @observer @@ -22,11 +25,43 @@ export class YoutubeBox extends React.Component { @observable searchResults: any[] = []; @observable videoClicked: boolean = false; @observable selectedVideoUrl: string = ""; + // @observable cachedResults: List | undefined; + @observable lisOfBackUp: JSX.Element[] = []; + public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } - componentWillMount() { + async componentWillMount() { //DocServer.getYoutubeChannels(); + let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); + if (!castedBackUpDocs) { + this.props.Document.cachedSearch = castedBackUpDocs = new List(); + } + if (castedBackUpDocs.length !== 0) { + //let awaitedRes = await castedBackUpDocs; + + this.searchResultsFound = true; + + for (let videoBackUp of castedBackUpDocs) { + let curBackUp = await videoBackUp; + let videoId = StrCast(curBackUp.videoId); + let videoTitle = StrCast(curBackUp.videoTitle); + let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); + runInAction(() => this.lisOfBackUp.push(( +
  • this.embedVideoOnClick(videoId, videoTitle)} + key={videoId} + > + + {videoTitle} +
  • ) + )); + } + + + } + + } _ignore = 0; @@ -63,12 +98,24 @@ export class YoutubeBox extends React.Component { console.log("Results: ", this.searchResults); if (this.searchResults.length > 0) { this.searchResultsFound = true; + this.backUpSearchResults(videos); if (this.videoClicked) { this.videoClicked = false; } } } + backUpSearchResults = (videos: any[]) => { + let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); + videos.forEach((video) => { + let videoBackUp = new Doc(); + videoBackUp.videoId = video.id.videoId; + videoBackUp.videoTitle = this.filterYoutubeTitleResult(video.snippet.title); + videoBackUp.thumbnailUrl = video.snippet.thumbnails.medium.url; + castedBackUpDocs!.push(videoBackUp); + }); + } + filterYoutubeTitleResult = (resultTitle: string) => { let processedTitle: string = resultTitle.ReplaceAll("&", "&"); processedTitle = processedTitle.ReplaceAll("'", "'"); @@ -76,17 +123,56 @@ export class YoutubeBox extends React.Component { return processedTitle; } + // mapSearchResults = () => { + // if (this.searchResults.length !== 0) { + // console.log("Entered here"); + // return
      { + // this.searchResults.map((video) => { + // let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); + // return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
    • ; + // })} + //
    ; + // } else if (this.cachedResults!.length !== 0) { + // return
      { + // this.cachedResults!.map(async (videoBackUp) => { + // let curBackUp = await videoBackUp; + // let videoId = StrCast(curBackUp.videoId); + // let videoTitle = StrCast(curBackUp.videoTitle); + // let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); + // return
    • this.embedVideoOnClick(videoTitle, videoTitle)} key={videoId}> {videoTitle}
    • ; + // })} + //
    ; + // } + // } + renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { - return
      - {this.searchResults.map((video) => { - let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
    • ; - })} -
    ; - // } else if (this.videoClicked) { - // return ; - // } + if (this.searchResults.length !== 0) { + return
      + {this.searchResults.map((video) => { + let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); + return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
    • ; + })} +
    ; + } else if (this.lisOfBackUp.length !== 0) { + // let lis: JSX.Element[] = []; + // for (let videoBackUp of this.cachedResults!) { + // let curBackUp = await videoBackUp; + // let videoId = StrCast(curBackUp.videoId); + // let videoTitle = StrCast(curBackUp.videoTitle); + // let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); + // lis.push(( + //
  • this.embedVideoOnClick(videoTitle, videoTitle)} + // key={videoId} + // > + // + // {videoTitle} + //
  • ) + // ); + // } + return
      {this.lisOfBackUp}
    ; + } } else { return (null); } @@ -95,6 +181,7 @@ export class YoutubeBox extends React.Component { @action embedVideoOnClick = (videoId: string, filteredTitle: string) => { let embeddedUrl = "https://www.youtube.com/embed/" + videoId; + console.log("EmbeddedUrl: ", embeddedUrl); this.selectedVideoUrl = embeddedUrl; let addFunction = this.props.addDocument!; let newVideoX = NumCast(this.props.Document.x) + NumCast(this.props.Document.width); @@ -109,6 +196,7 @@ export class YoutubeBox extends React.Component { render() { let field = this.props.Document[this.props.fieldKey]; + //let results = this.renderSearchResultsOrVideo(); let content =
    this.YoutubeSearchElement = e!} /> -- cgit v1.2.3-70-g09d2 From 421311806a686dd0de2bbdd7d8aa7dbb5c9fe9d5 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 18 Jul 2019 14:14:43 -0400 Subject: Overriding allowed, keys problem still exist --- src/client/apis/youtube/YoutubeBox.tsx | 54 ++++------------------------------ 1 file changed, 5 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index fa2d3fb53..33d989b6a 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -25,7 +25,6 @@ export class YoutubeBox extends React.Component { @observable searchResults: any[] = []; @observable videoClicked: boolean = false; @observable selectedVideoUrl: string = ""; - // @observable cachedResults: List | undefined; @observable lisOfBackUp: JSX.Element[] = []; @@ -38,7 +37,6 @@ export class YoutubeBox extends React.Component { this.props.Document.cachedSearch = castedBackUpDocs = new List(); } if (castedBackUpDocs.length !== 0) { - //let awaitedRes = await castedBackUpDocs; this.searchResultsFound = true; @@ -50,7 +48,7 @@ export class YoutubeBox extends React.Component { runInAction(() => this.lisOfBackUp.push((
  • this.embedVideoOnClick(videoId, videoTitle)} - key={videoId} + key={Utils.GenerateGuid()} > {videoTitle} @@ -95,7 +93,6 @@ export class YoutubeBox extends React.Component { @action processesVideoResults = (videos: any[]) => { this.searchResults = videos; - console.log("Results: ", this.searchResults); if (this.searchResults.length > 0) { this.searchResultsFound = true; this.backUpSearchResults(videos); @@ -106,13 +103,14 @@ export class YoutubeBox extends React.Component { } backUpSearchResults = (videos: any[]) => { - let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); + let newCachedList = new List(); + this.props.Document.cachedSearch = newCachedList; videos.forEach((video) => { let videoBackUp = new Doc(); videoBackUp.videoId = video.id.videoId; videoBackUp.videoTitle = this.filterYoutubeTitleResult(video.snippet.title); videoBackUp.thumbnailUrl = video.snippet.thumbnails.medium.url; - castedBackUpDocs!.push(videoBackUp); + newCachedList.push(videoBackUp); }); } @@ -123,54 +121,16 @@ export class YoutubeBox extends React.Component { return processedTitle; } - // mapSearchResults = () => { - // if (this.searchResults.length !== 0) { - // console.log("Entered here"); - // return
      { - // this.searchResults.map((video) => { - // let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - // return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
    • ; - // })} - //
    ; - // } else if (this.cachedResults!.length !== 0) { - // return
      { - // this.cachedResults!.map(async (videoBackUp) => { - // let curBackUp = await videoBackUp; - // let videoId = StrCast(curBackUp.videoId); - // let videoTitle = StrCast(curBackUp.videoTitle); - // let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); - // return
    • this.embedVideoOnClick(videoTitle, videoTitle)} key={videoId}> {videoTitle}
    • ; - // })} - //
    ; - // } - // } - renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { if (this.searchResults.length !== 0) { return
      {this.searchResults.map((video) => { let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={video.id.videoId}> {filteredTitle}
    • ; + return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}> {filteredTitle}
    • ; })}
    ; } else if (this.lisOfBackUp.length !== 0) { - // let lis: JSX.Element[] = []; - // for (let videoBackUp of this.cachedResults!) { - // let curBackUp = await videoBackUp; - // let videoId = StrCast(curBackUp.videoId); - // let videoTitle = StrCast(curBackUp.videoTitle); - // let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); - // lis.push(( - //
  • this.embedVideoOnClick(videoTitle, videoTitle)} - // key={videoId} - // > - // - // {videoTitle} - //
  • ) - // ); - // } return
      {this.lisOfBackUp}
    ; } } else { @@ -188,15 +148,11 @@ export class YoutubeBox extends React.Component { 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.props.addDocument(Docs.Create.VideoDocument(embeddedUrl, { title: embeddedUrl, width: 400, height: 315 })); - //this.searchResultsFound = false; this.videoClicked = true; } render() { let field = this.props.Document[this.props.fieldKey]; - //let results = this.renderSearchResultsOrVideo(); let content =
    this.YoutubeSearchElement = e!} /> -- cgit v1.2.3-70-g09d2 From 82a9c1f854fad05db0878717aa82572ffc1290c1 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 18 Jul 2019 18:08:07 -0400 Subject: Update on coordinates --- src/client/apis/youtube/YoutubeBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 33d989b6a..da3c4b851 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -144,7 +144,7 @@ export class YoutubeBox extends React.Component { console.log("EmbeddedUrl: ", embeddedUrl); this.selectedVideoUrl = embeddedUrl; let addFunction = this.props.addDocument!; - let newVideoX = NumCast(this.props.Document.x) + NumCast(this.props.Document.width); + 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 })); -- cgit v1.2.3-70-g09d2 From 55bc9d93a8f39dfc6781677677dd3e4c55642c15 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 18 Jul 2019 18:13:10 -0400 Subject: Small css stuff --- src/client/views/MainView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 18e98a5cc..5a1b1991e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -381,7 +381,7 @@ export class MainView extends React.Component { 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: 200, height: 200, title: "youtube node" })); + let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); let btns: [React.RefObject, IconName, string, () => Doc][] = [ [React.createRef(), "object-group", "Add Collection", addColNode], -- cgit v1.2.3-70-g09d2 From 157060f7e6029c76765aa20d8fdbe325401a3880 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Fri, 19 Jul 2019 18:29:03 -0400 Subject: Youtube Search UI imitated mostly --- src/client/apis/youtube/YoutubeBox.scss | 74 +++++++++++++++++++++++++-- src/client/apis/youtube/YoutubeBox.tsx | 89 ++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss index 23f264b95..5b539b463 100644 --- a/src/client/apis/youtube/YoutubeBox.scss +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -1,5 +1,6 @@ ul { list-style-type: none; + padding-inline-start: 10px; } @@ -13,7 +14,74 @@ li:hover { opacity: 0.8; } -.videoTitle { - margin-left: 4px; - font-family: Arial, Helvetica, sans-serif; +.search_wrapper { + width: 100%; + display: inline-flex; + height: 175px; + + .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; + } + + + + } } \ No newline at end of file diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index da3c4b851..7ac8d06f6 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -121,13 +121,100 @@ export class YoutubeBox extends React.Component { return processedTitle; } + roundPublishTime = (publishTime: string) => { + let date = new Date(publishTime); + let curDate = new Date(); + let videoYearDif = curDate.getFullYear() - date.getFullYear(); + let videoMonthDif = curDate.getMonth() - date.getMonth(); + let videoDayDif = curDate.getDay() - date.getDay(); + console.log("video day dif: ", videoDayDif, " first day: ", curDate.getDay(), " second day: ", date.getDay()); + let videoHoursDif = curDate.getHours() - date.getHours(); + let videoMinutesDif = curDate.getMinutes() - date.getMinutes(); + let videoSecondsDif = curDate.getSeconds() - date.getSeconds(); + if (videoYearDif !== 0) { + return videoYearDif + " years ago"; + } else if (videoMonthDif !== 0) { + return videoMonthDif + " months ago"; + } else if (videoDayDif !== 0) { + return videoDayDif + " days ago"; + } else if (videoHoursDif !== 0) { + return videoHoursDif + " hours ago"; + } else if (videoMinutesDif) { + return videoMinutesDif + " minutes ago"; + } else if (videoSecondsDif) { + return videoSecondsDif + " seconds ago"; + } + + console.log("Date : ", date); + } + + roundPublishTime2 = (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"; + } + } + renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { if (this.searchResults.length !== 0) { return
      {this.searchResults.map((video) => { let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); - return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}> {filteredTitle}
    • ; + let channelTitle = video.snippet.channelTitle; + let videoDescription = video.snippet.description; + let pusblishDate = this.roundPublishTime2(video.snippet.publishedAt); + // let duration = video.contentDetails.duration; + //let viewCount = video.statistics.viewCount; + //this.roundPublishTime(pusblishDate); + //this.roundPublishTime2(video.snippet.publishedAt); + return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}> +
      + +
      + {filteredTitle} + {channelTitle} + {pusblishDate} + {/*
      {viewCount}
      */} +

      {videoDescription}

      +
      +
      +
    • ; })}
    ; } else if (this.lisOfBackUp.length !== 0) { -- cgit v1.2.3-70-g09d2 From 4446a3a52c4cf4b03c201ab2d6a9179647686e40 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Mon, 22 Jul 2019 18:47:14 -0400 Subject: Pulled Duration and ViewCount details, Need to csss duration --- src/client/DocServer.ts | 4 ++ src/client/apis/youtube/YoutubeBox.scss | 37 ++++++++++++++++++ src/client/apis/youtube/YoutubeBox.tsx | 64 ++++++++++++++++++++++++++++--- src/server/Message.ts | 3 +- src/server/index.ts | 2 + src/server/youtubeApi/youtubeApiSample.js | 21 ++++++++++ 6 files changed, 125 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index bc5819061..8a9abb514 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -168,6 +168,10 @@ export namespace DocServer { 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 diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss index 5b539b463..00979f945 100644 --- a/src/client/apis/youtube/YoutubeBox.scss +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -19,6 +19,27 @@ li:hover { display: inline-flex; height: 175px; + .video_duration { + margin: 0; + padding: 0; + border: 0; + background: transparent; + display: inline-block; + position: absolute; + bottom: 0; + right: 0; + 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; @@ -80,6 +101,22 @@ li:hover { 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; + } diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 7ac8d06f6..824a0251d 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -26,12 +26,15 @@ export class YoutubeBox extends React.Component { @observable videoClicked: boolean = false; @observable selectedVideoUrl: string = ""; @observable lisOfBackUp: JSX.Element[] = []; + @observable videoIds: string | undefined; + @observable videoDetails: any[] = []; public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } async componentWillMount() { //DocServer.getYoutubeChannels(); + //DocServer.getYoutubeVideoDetails("Ks-_Mh1QhMc, 1NmvhSmN2uM", (results: any[]) => console.log("Details results: ", results)); let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); if (!castedBackUpDocs) { this.props.Document.cachedSearch = castedBackUpDocs = new List(); @@ -95,6 +98,15 @@ export class YoutubeBox extends React.Component { 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; + } + }); + DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); this.backUpSearchResults(videos); if (this.videoClicked) { this.videoClicked = false; @@ -102,6 +114,12 @@ export class YoutubeBox extends React.Component { } } + @action + processVideoDetails = (videoDetails: any[]) => { + this.videoDetails = videoDetails; + console.log("Detail Res: ", this.videoDetails); + } + backUpSearchResults = (videos: any[]) => { let newCachedList = new List(); this.props.Document.cachedSearch = newCachedList; @@ -190,28 +208,64 @@ export class YoutubeBox extends React.Component { } } + 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(":"); + } + + 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"; + } + } + renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { if (this.searchResults.length !== 0) { return
      - {this.searchResults.map((video) => { + {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.roundPublishTime2(video.snippet.publishedAt); - // let duration = video.contentDetails.duration; - //let viewCount = video.statistics.viewCount; + 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); + } //this.roundPublishTime(pusblishDate); //this.roundPublishTime2(video.snippet.publishedAt); + return
    • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
      - +
      + + {duration} +
      {filteredTitle} {channelTitle} + {viewCount} {pusblishDate} - {/*
      {viewCount}
      */}

      {videoDescription}

      +
    • ; diff --git a/src/server/Message.ts b/src/server/Message.ts index 1e29aef0b..aaee143e8 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -25,12 +25,13 @@ export interface Transferable { } export enum YoutubeQueryTypes { - Channels, SearchVideo + Channels, SearchVideo, VideoDetails } export interface YoutubeQueryInput { readonly type: YoutubeQueryTypes; readonly userInput?: string; + readonly videoIds?: string; } export interface Reference { diff --git a/src/server/index.ts b/src/server/index.ts index 60e34de8c..dfbc1a468 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -537,6 +537,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any break; case YoutubeQueryType.SearchVideo: YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback); + case YoutubeQueryType.VideoDetails: + YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback); } } diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index f875812d5..cf41a33e7 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -33,6 +33,10 @@ module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { authorize(JSON.parse(apiKey), getSampleVideos, { 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 @@ -156,4 +160,21 @@ function getSampleVideos(auth, args) { console.log('Videos found: ' + videos[0].id.videoId, " ", unescape(videos[0].snippet.title)); args.callBack(videos); }); +} + +function getVideoDetails(auth, args) { + 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: ' + err); + return; + } + let videoDetails = response.data.items; + console.log('Video Details founds: ', videoDetails); + args.callBack(videoDetails); + }); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From e1b750da8faf8f00707de1b65efbd210c19fa723 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 14:29:08 -0400 Subject: Store and css --- src/client/apis/youtube/YoutubeBox.scss | 10 +++++----- src/client/apis/youtube/YoutubeBox.tsx | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss index 00979f945..1fc91a9ae 100644 --- a/src/client/apis/youtube/YoutubeBox.scss +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -20,14 +20,14 @@ li:hover { height: 175px; .video_duration { - margin: 0; - padding: 0; + // margin: 0; + // padding: 0; border: 0; background: transparent; display: inline-block; - position: absolute; - bottom: 0; - right: 0; + position: relative; + bottom: 25px; + left: 85%; margin: 4px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.80); diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 824a0251d..373eee5c4 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -12,7 +12,7 @@ import { DocServer } from "../../DocServer"; import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; import "./YoutubeBox.scss"; import { Docs } from "../../documents/Documents"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync } from "../../../new_fields/Doc"; import { listSpec } from "../../../new_fields/Schema"; import { List } from "../../../new_fields/List"; @@ -36,12 +36,29 @@ export class YoutubeBox extends React.Component { //DocServer.getYoutubeChannels(); //DocServer.getYoutubeVideoDetails("Ks-_Mh1QhMc, 1NmvhSmN2uM", (results: any[]) => console.log("Details results: ", results)); let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); + let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc); + let awaitedBackUp = await castedSearchBackUp; + + console.log("Backup results: ", awaitedBackUp); + console.log("Original Backup results: ", castedBackUpDocs); + + let json = Cast(awaitedBackUp!.json, Doc); + let jsonList = await DocListCastAsync(json); + console.log("Fucked up list: ", jsonList); + for (let video of jsonList!) { + let videoId = await Cast(video.id, Doc); + let id = StrCast(videoId!.videoId); + console.log("ID: ", id); + } + + + if (!castedBackUpDocs) { this.props.Document.cachedSearch = castedBackUpDocs = new List(); } if (castedBackUpDocs.length !== 0) { - this.searchResultsFound = true; + runInAction(() => this.searchResultsFound = true); for (let videoBackUp of castedBackUpDocs) { let curBackUp = await videoBackUp; @@ -121,6 +138,8 @@ export class YoutubeBox extends React.Component { } backUpSearchResults = (videos: any[]) => { + console.log("Res: ", videos); + this.props.Document.cachedSearchResults = Docs.Get.DocumentHierarchyFromJson(videos, "videosBackUp"); let newCachedList = new List(); this.props.Document.cachedSearch = newCachedList; videos.forEach((video) => { -- cgit v1.2.3-70-g09d2 From 0591b8f1e60d1285fd9aac3e61160824948a166b Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 17:33:31 -0400 Subject: Everything related to search stored --- src/client/apis/youtube/YoutubeBox.tsx | 123 +++++++++++++++++++----------- src/client/documents/Documents.ts | 2 +- src/server/youtubeApi/youtubeApiSample.js | 4 - 3 files changed, 81 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 373eee5c4..e630c11ae 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -16,6 +16,16 @@ 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; +} @observer export class YoutubeBox extends React.Component { @@ -28,58 +38,71 @@ export class YoutubeBox extends React.Component { @observable lisOfBackUp: JSX.Element[] = []; @observable videoIds: string | undefined; @observable videoDetails: any[] = []; + @observable curVideoTemplates: VideoTemplate[] = []; public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } async componentWillMount() { //DocServer.getYoutubeChannels(); - //DocServer.getYoutubeVideoDetails("Ks-_Mh1QhMc, 1NmvhSmN2uM", (results: any[]) => console.log("Details results: ", results)); - let castedBackUpDocs = Cast(this.props.Document.cachedSearch, listSpec(Doc)); let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc); let awaitedBackUp = await castedSearchBackUp; + let castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc); + let awaitedDetails = await castedDetailBackUp; - console.log("Backup results: ", awaitedBackUp); - console.log("Original Backup results: ", castedBackUpDocs); - - let json = Cast(awaitedBackUp!.json, Doc); - let jsonList = await DocListCastAsync(json); - console.log("Fucked up list: ", jsonList); - for (let video of jsonList!) { - let videoId = await Cast(video.id, Doc); - let id = StrCast(videoId!.videoId); - console.log("ID: ", id); - } + let jsonList = await DocListCastAsync(awaitedBackUp!.json); + let jsonDetailList = await DocListCastAsync(awaitedDetails!.json); - if (!castedBackUpDocs) { - this.props.Document.cachedSearch = castedBackUpDocs = new List(); - } - if (castedBackUpDocs.length !== 0) { - + if (jsonList!.length !== 0) { runInAction(() => this.searchResultsFound = true); + let index = 0; + 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.roundPublishTime2(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(NumCast(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)); + + // runInAction(() => this.lisOfBackUp.push(( + //
    • this.embedVideoOnClick(id, videoTitle)} key={Utils.GenerateGuid() + id}> + //
      + //
      + // + // {duration} + //
      + //
      + // {videoTitle} + // {channelTitle} + // {viewCount} + // {pusblishDate} + //

      {videoDescription}

      + + //
      + //
      + //
    • ) + // )); - for (let videoBackUp of castedBackUpDocs) { - let curBackUp = await videoBackUp; - let videoId = StrCast(curBackUp.videoId); - let videoTitle = StrCast(curBackUp.videoTitle); - let thumbnailUrl = StrCast(curBackUp.thumbnailUrl); - runInAction(() => this.lisOfBackUp.push(( -
    • this.embedVideoOnClick(videoId, videoTitle)} - key={Utils.GenerateGuid()} - > - - {videoTitle} -
    • ) - )); } - - } - - } _ignore = 0; @@ -134,11 +157,10 @@ export class YoutubeBox extends React.Component { @action processVideoDetails = (videoDetails: any[]) => { this.videoDetails = videoDetails; - console.log("Detail Res: ", this.videoDetails); + this.props.Document.cachedDetails = Docs.Get.DocumentHierarchyFromJson(videoDetails, "detailBackUp"); } backUpSearchResults = (videos: any[]) => { - console.log("Res: ", videos); this.props.Document.cachedSearchResults = Docs.Get.DocumentHierarchyFromJson(videos, "videosBackUp"); let newCachedList = new List(); this.props.Document.cachedSearch = newCachedList; @@ -164,7 +186,6 @@ export class YoutubeBox extends React.Component { let videoYearDif = curDate.getFullYear() - date.getFullYear(); let videoMonthDif = curDate.getMonth() - date.getMonth(); let videoDayDif = curDate.getDay() - date.getDay(); - console.log("video day dif: ", videoDayDif, " first day: ", curDate.getDay(), " second day: ", date.getDay()); let videoHoursDif = curDate.getHours() - date.getHours(); let videoMinutesDif = curDate.getMinutes() - date.getMinutes(); let videoSecondsDif = curDate.getSeconds() - date.getSeconds(); @@ -182,7 +203,6 @@ export class YoutubeBox extends React.Component { return videoSecondsDif + " seconds ago"; } - console.log("Date : ", date); } roundPublishTime2 = (publishTime: string) => { @@ -290,8 +310,26 @@ export class YoutubeBox extends React.Component { ; })}
    ; - } else if (this.lisOfBackUp.length !== 0) { - return
      {this.lisOfBackUp}
    ; + } else if (this.curVideoTemplates.length !== 0) { + return
      + {this.curVideoTemplates.map((video: VideoTemplate) => { + return
    • this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}> +
      +
      + + {video.duration} +
      +
      + {video.videoTitle} + {video.channelTitle} + {video.viewCount} + {video.publishDate} +

      {video.videoDescription}

      +
      +
      +
    • ; + })} +
    ; } } else { return (null); @@ -301,7 +339,6 @@ export class YoutubeBox extends React.Component { @action embedVideoOnClick = (videoId: string, filteredTitle: string) => { let embeddedUrl = "https://www.youtube.com/embed/" + videoId; - console.log("EmbeddedUrl: ", embeddedUrl); this.selectedVideoUrl = embeddedUrl; let addFunction = this.props.addDocument!; let newVideoX = NumCast(this.props.Document.x); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 191be9b7d..333e9859b 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -498,7 +498,7 @@ export namespace Docs { const convertObject = (object: any, title?: string): Doc => { let target = new Doc(), result: Opt; 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/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index cf41a33e7..4fede08aa 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -23,8 +23,6 @@ module.exports.readApiKey = (callback) => { module.exports.authorizedGetChannel = (apiKey) => { //this didnt get called - console.log("I get called ", apiKey); - console.log(TOKEN_PATH); // Authorize a client with the loaded credentials, then call the YouTube API. authorize(JSON.parse(apiKey), getChannel); } @@ -157,7 +155,6 @@ function getSampleVideos(auth, args) { return; } let videos = response.data.items; - console.log('Videos found: ' + videos[0].id.videoId, " ", unescape(videos[0].snippet.title)); args.callBack(videos); }); } @@ -174,7 +171,6 @@ function getVideoDetails(auth, args) { return; } let videoDetails = response.data.items; - console.log('Video Details founds: ', videoDetails); args.callBack(videoDetails); }); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 778286579008b57a76fbf82235348b613f5c1a5b Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 18:56:23 -0400 Subject: Fixed document --- src/client/apis/youtube/YoutubeBox.tsx | 24 ++---------------------- src/server/youtubeApi/youtubeApiSample.js | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index e630c11ae..d2f5112c2 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -81,26 +81,6 @@ export class YoutubeBox extends React.Component { 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)); - - // runInAction(() => this.lisOfBackUp.push(( - //
  • this.embedVideoOnClick(id, videoTitle)} key={Utils.GenerateGuid() + id}> - //
    - //
    - // - // {duration} - //
    - //
    - // {videoTitle} - // {channelTitle} - // {viewCount} - // {pusblishDate} - //

    {videoDescription}

    - - //
    - //
    - //
  • ) - // )); - } } } @@ -146,6 +126,7 @@ export class YoutubeBox extends React.Component { this.videoIds = this.videoIds! + ", " + video.id.videoId; } }); + console.log("Video Ids: ", this.videoIds); DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); this.backUpSearchResults(videos); if (this.videoClicked) { @@ -289,8 +270,7 @@ export class YoutubeBox extends React.Component { duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration); viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount); } - //this.roundPublishTime(pusblishDate); - //this.roundPublishTime2(video.snippet.publishedAt); + return
  • this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
    diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index 4fede08aa..f81f0dfb5 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -167,7 +167,7 @@ function getVideoDetails(auth, args) { id: args.videoIds }, function (err, response) { if (err) { - console.log('The API returned an error: ' + err); + console.log('The API returned an error from details: ' + err); return; } let videoDetails = response.data.items; -- cgit v1.2.3-70-g09d2 From c86f580191ddf70a6ac2994819a4f33731d79011 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 19:07:59 -0400 Subject: Documentation --- src/client/apis/youtube/YoutubeBox.tsx | 87 +++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index d2f5112c2..414abcc15 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -27,6 +27,9 @@ interface VideoTemplate { videoDescription: string; } +/** + * This class models the youtube search document that can be dropped on to canvas. + */ @observer export class YoutubeBox extends React.Component { @@ -43,6 +46,10 @@ export class YoutubeBox extends React.Component { 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); @@ -58,6 +65,7 @@ export class YoutubeBox extends React.Component { 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); @@ -68,7 +76,7 @@ export class YoutubeBox extends React.Component { let thumbnailMedium = await Cast(thumbnail!.medium, Doc); let thumbnailUrl = StrCast(thumbnailMedium!.url); let videoDescription = StrCast(snippet!.description); - let pusblishDate = (this.roundPublishTime2(StrCast(snippet!.publishedAt)))!; + let pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!; let channelTitle = StrCast(snippet!.channelTitle); let duration: string; let viewCount: string; @@ -103,6 +111,9 @@ export class YoutubeBox extends React.Component { } } + /** + * Function that submits the title entered by user on enter press. + */ onEnterKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === 13) { let submittedTitle = this.YoutubeSearchElement!.value; @@ -113,6 +124,11 @@ export class YoutubeBox extends React.Component { } } + /** + * 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; @@ -126,7 +142,7 @@ export class YoutubeBox extends React.Component { this.videoIds = this.videoIds! + ", " + video.id.videoId; } }); - console.log("Video Ids: ", this.videoIds); + //Asking for details that include duration and viewCount from server for videoIds DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); this.backUpSearchResults(videos); if (this.videoClicked) { @@ -135,25 +151,26 @@ export class YoutubeBox extends React.Component { } } + /** + * 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"); - let newCachedList = new List(); - this.props.Document.cachedSearch = newCachedList; - videos.forEach((video) => { - let videoBackUp = new Doc(); - videoBackUp.videoId = video.id.videoId; - videoBackUp.videoTitle = this.filterYoutubeTitleResult(video.snippet.title); - videoBackUp.thumbnailUrl = video.snippet.thumbnails.medium.url; - newCachedList.push(videoBackUp); - }); } + /** + * 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("&", "&"); processedTitle = processedTitle.ReplaceAll("'", "'"); @@ -161,32 +178,13 @@ export class YoutubeBox extends React.Component { return processedTitle; } - roundPublishTime = (publishTime: string) => { - let date = new Date(publishTime); - let curDate = new Date(); - let videoYearDif = curDate.getFullYear() - date.getFullYear(); - let videoMonthDif = curDate.getMonth() - date.getMonth(); - let videoDayDif = curDate.getDay() - date.getDay(); - let videoHoursDif = curDate.getHours() - date.getHours(); - let videoMinutesDif = curDate.getMinutes() - date.getMinutes(); - let videoSecondsDif = curDate.getSeconds() - date.getSeconds(); - if (videoYearDif !== 0) { - return videoYearDif + " years ago"; - } else if (videoMonthDif !== 0) { - return videoMonthDif + " months ago"; - } else if (videoDayDif !== 0) { - return videoDayDif + " days ago"; - } else if (videoHoursDif !== 0) { - return videoHoursDif + " hours ago"; - } else if (videoMinutesDif) { - return videoMinutesDif + " minutes ago"; - } else if (videoSecondsDif) { - return videoSecondsDif + " seconds ago"; - } - } - roundPublishTime2 = (publishTime: string) => { + /** + * 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; @@ -228,6 +226,9 @@ export class YoutubeBox extends React.Component { } } + /** + * 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(":"); @@ -243,6 +244,10 @@ export class YoutubeBox extends React.Component { 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(); @@ -255,6 +260,11 @@ export class YoutubeBox extends React.Component { } } + /** + * 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) { @@ -263,7 +273,7 @@ export class YoutubeBox extends React.Component { let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); let channelTitle = video.snippet.channelTitle; let videoDescription = video.snippet.description; - let pusblishDate = this.roundPublishTime2(video.snippet.publishedAt); + let pusblishDate = this.roundPublishTime(video.snippet.publishedAt); let duration; let viewCount; if (this.videoDetails.length !== 0) { @@ -316,6 +326,10 @@ export class YoutubeBox extends React.Component { } } + /** + * 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; @@ -329,7 +343,6 @@ export class YoutubeBox extends React.Component { } render() { - let field = this.props.Document[this.props.fieldKey]; let content =
    this.YoutubeSearchElement = e!} /> -- cgit v1.2.3-70-g09d2 From 6cdebe507b777f60a0e30a1d7a75300304fbce09 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 19:17:03 -0400 Subject: Refactor and flag --- src/client/apis/youtube/YoutubeBox.tsx | 63 ++++++++++++++++--------------- src/server/youtubeApi/youtubeApiSample.js | 4 +- 2 files changed, 35 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 414abcc15..8d6334c6e 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -58,37 +58,40 @@ export class YoutubeBox extends React.Component { let awaitedDetails = await castedDetailBackUp; - - 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(NumCast(statistics!.viewCount))!; + 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(NumCast(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)); } - 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)); } } } diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index f81f0dfb5..9853241b6 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -28,7 +28,7 @@ module.exports.authorizedGetChannel = (apiKey) => { } module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { - authorize(JSON.parse(apiKey), getSampleVideos, { userInput: userInput, callBack: callBack }); + authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack }); } module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => { @@ -141,7 +141,7 @@ function getChannel(auth) { }); } -function getSampleVideos(auth, args) { +function getVideos(auth, args) { let service = google.youtube('v3'); service.search.list({ auth: auth, -- cgit v1.2.3-70-g09d2 From f5b17bf655ab93d3214ebd1eb7697dd21265d3b5 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 19:27:25 -0400 Subject: Casting problem fixed --- src/client/apis/youtube/YoutubeBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 8d6334c6e..019d191bc 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -86,7 +86,7 @@ export class YoutubeBox extends React.Component { 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(NumCast(statistics!.viewCount))!; + 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! }; -- cgit v1.2.3-70-g09d2 From 2edcfb8a755b7fec7f937f135c794cbadbe0c94e Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 23 Jul 2019 19:40:47 -0400 Subject: Error on api extra call fixed. Don't know why would it get called twice tho --- src/server/youtubeApi/youtubeApiSample.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index 9853241b6..ec532c78e 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -160,6 +160,9 @@ function getVideos(auth, args) { } function getVideoDetails(auth, args) { + if (args.videoIds === undefined) { + return; + } let service = google.youtube('v3'); service.videos.list({ auth: auth, -- cgit v1.2.3-70-g09d2 From 7844debd9862b15d2b36bf9ce1efb3274616a313 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 24 Jul 2019 18:53:21 -0400 Subject: Context Menu and Some Preview --- .../views/collections/CollectionTreeView.tsx | 2 +- .../views/presentationview/PresentationElement.tsx | 58 ++++++++++++++++++++-- .../views/presentationview/PresentationView.scss | 4 ++ .../views/presentationview/PresentationView.tsx | 2 +- 4 files changed, 60 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 006de0c70..c0347ad7d 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -64,7 +64,7 @@ library.add(faArrowsAltH); /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ -class TreeView extends React.Component { +export class TreeView extends React.Component { private _header?: React.RefObject = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef(); diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 329630875..f354a14c2 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react"; import React = require("react"); import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; -import { NumCast, BoolCast, StrCast, Cast } from "../../../new_fields/Types"; +import { NumCast, BoolCast, StrCast, Cast, FieldValue } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; import { observable, action, computed, runInAction } from "mobx"; import "./PresentationView.scss"; -import { Utils } from "../../../Utils"; +import { Utils, emptyFunction, returnFalse } from "../../../Utils"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; @@ -16,6 +16,11 @@ import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; import { indexOf } from "typescript-collections/dist/lib/arrays"; import { map } from "bluebird"; +import { ContextMenu } from "../ContextMenu"; +import { DocumentContentsView } from "../nodes/DocumentContentsView"; +import { Transform } from "../../util/Transform"; +import { FieldView } from "../nodes/FieldView"; +import { DocumentView } from "../nodes/DocumentView"; library.add(faArrowUp); library.add(fileSolid); @@ -768,8 +773,51 @@ export default class PresentationElement extends React.Component) => { + 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); + } + renderEmbeddedInline = () => { + if (!this.embedInline) { + return (null); + } + + // return
      + // {TreeView.GetChildElements([this.props.document], "", new Doc(), undefined, "", (doc: Doc, relativeTo?: Doc, before?: boolean) => false, this.props.removeDocByRef, this.move, + // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} + //
    ; + return ( + 1} + PanelWidth={() => 400} + PanelHeight={() => 400} + focus={(doc: Doc, willZoom: boolean) => { }} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={(isActive: boolean) => { }} + bringToFront={(doc: Doc) => { }} + addDocTab={(doc: Doc, dataDoc: Doc | undefined, where: string) => { }} + zoomToScale={(scale: number) => { }} + getScale={() => 3.1415} + /> + ); + } render() { let p = this.props; @@ -786,7 +834,7 @@ export default class PresentationElement extends React.Component p.document, this.move, dropAction, this.props.mainDocument[Id], true); return ( -
    { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> @@ -811,7 +860,8 @@ export default class PresentationElement extends React.Component - +
    + {this.renderEmbeddedInline()}
    ); } diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 2bb0ec8c8..56c8beba5 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -19,6 +19,10 @@ -ms-user-select: none; user-select: none; transition: all .1s; + + .jsx-parser { + height: auto; + } } .presentationView-item-above { diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index b318f0321..95458b4ca 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -16,6 +16,7 @@ import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, fa import { Docs } from "../../documents/Documents"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; +import { ContextMenu } from "../ContextMenu"; library.add(faArrowLeft); library.add(faArrowRight); @@ -805,7 +806,6 @@ export class PresentationView extends React.Component { this.presElementsMappings.set(keyDoc, elem); } - render() { let width = NumCast(this.curPresentation.width); -- cgit v1.2.3-70-g09d2 From 1bb4cc79000b6d7c3a3fc137926e754877bf1dce Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 24 Jul 2019 20:50:13 -0400 Subject: some style --- src/client/views/presentationview/PresentationView.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 56c8beba5..392ec1fb7 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -21,7 +21,8 @@ transition: all .1s; .jsx-parser { - height: auto; + height: 300px; + width: 400px; } } -- cgit v1.2.3-70-g09d2 From 3d5884541db27dca38dac25b773a0eae7451a57f Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 24 Jul 2019 21:15:42 -0400 Subject: Ask Bob About sizing and scaling --- .../views/presentationview/PresentationElement.tsx | 44 ++++++++++++++++------ .../views/presentationview/PresentationView.scss | 1 + 2 files changed, 33 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index f354a14c2..2091974ee 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -5,7 +5,7 @@ import { NumCast, BoolCast, StrCast, Cast, FieldValue } from "../../../new_field import { Id } from "../../../new_fields/FieldSymbols"; import { observable, action, computed, runInAction } from "mobx"; import "./PresentationView.scss"; -import { Utils, emptyFunction, returnFalse } from "../../../Utils"; +import { Utils, emptyFunction, returnFalse, returnOne } from "../../../Utils"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; @@ -799,22 +799,42 @@ export default class PresentationElement extends React.Component; return ( + // 1} + // PanelWidth={() => 400} + // PanelHeight={() => 400} + // focus={(doc: Doc, willZoom: boolean) => { }} + // selectOnLoad={false} + // parentActive={returnFalse} + // whenActiveChanged={(isActive: boolean) => { }} + // bringToFront={(doc: Doc) => { }} + // addDocTab={(doc: Doc, dataDoc: Doc | undefined, where: string) => { }} + // zoomToScale={(scale: number) => { }} + // getScale={() => 3.1415} + // /> 1} - PanelWidth={() => 400} - PanelHeight={() => 400} - focus={(doc: Doc, willZoom: boolean) => { }} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} selectOnLoad={false} parentActive={returnFalse} - whenActiveChanged={(isActive: boolean) => { }} - bringToFront={(doc: Doc) => { }} - addDocTab={(doc: Doc, dataDoc: Doc | undefined, where: string) => { }} - zoomToScale={(scale: number) => { }} - getScale={() => 3.1415} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} /> ); } diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 392ec1fb7..b8b4f23c4 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -23,6 +23,7 @@ .jsx-parser { height: 300px; width: 400px; + margin-top: 20px; } } -- cgit v1.2.3-70-g09d2 From f55dd40423b85efcdfbae1dd037743de9f6f15c4 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 25 Jul 2019 12:51:59 -0400 Subject: Some Preview Shown. Css left --- src/client/views/presentationview/PresentationElement.tsx | 15 ++++++++------- src/client/views/presentationview/PresentationView.scss | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 2091974ee..859ab9862 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -21,6 +21,7 @@ import { DocumentContentsView } from "../nodes/DocumentContentsView"; import { Transform } from "../../util/Transform"; import { FieldView } from "../nodes/FieldView"; import { DocumentView } from "../nodes/DocumentView"; +import { DocumentType } from "../../documents/Documents"; library.add(faArrowUp); library.add(fileSolid); @@ -656,7 +657,7 @@ export default class PresentationElement extends React.Component { - this.props.document.libraryBrush = true; + // this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -673,7 +674,7 @@ export default class PresentationElement extends React.Component { - this.props.document.libraryBrush = false; + // this.props.document.libraryBrush = false; //to get currently selected presentation doc let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -797,7 +798,7 @@ export default class PresentationElement extends React.Component false, this.props.removeDocByRef, this.move, // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} // ; - + let scale = () => 175 / NumCast(this.props.document.nativeWidth, 175); return ( // { }} // getScale={() => 3.1415} // /> + 175} + PanelHeight={() => 175} focus={emptyFunction} selectOnLoad={false} parentActive={returnFalse} @@ -861,8 +863,7 @@ export default class PresentationElement extends React.Component { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index b8b4f23c4..c2d064941 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -19,12 +19,14 @@ -ms-user-select: none; user-select: none; transition: all .1s; + max-height: 250px; .jsx-parser { height: 300px; - width: 400px; + //width: 400px; margin-top: 20px; } + } .presentationView-item-above { -- cgit v1.2.3-70-g09d2 From 24a73583847e6ab00c8b1a00859129665e6ac212 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Thu, 25 Jul 2019 19:03:04 -0400 Subject: Css Acting weird, fixes needed --- .../views/presentationview/PresentationElement.tsx | 66 ++++++++++++++-------- .../views/presentationview/PresentationView.scss | 14 ++++- 2 files changed, 55 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 859ab9862..ff68f47ba 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -798,6 +798,8 @@ export default class PresentationElement extends React.Component false, this.props.removeDocByRef, this.move, // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} // ; + let propDocWidth = NumCast(this.props.document.nativeWidth); + let propDocHeight = NumCast(this.props.document.nativeHeight); let scale = () => 175 / NumCast(this.props.document.nativeWidth, 175); return ( // { }} // getScale={() => 3.1415} // /> - - 175} - PanelHeight={() => 175} - focus={emptyFunction} - selectOnLoad={false} - parentActive={returnFalse} - whenActiveChanged={returnFalse} - bringToFront={emptyFunction} - zoomToScale={emptyFunction} - getScale={returnOne} - ContainingCollectionView={undefined} - ContentScaling={scale} - /> +
    + 20} + PanelHeight={() => 20} + focus={emptyFunction} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> +
    +
    ); } @@ -863,7 +885,7 @@ export default class PresentationElement extends React.Component { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index c2d064941..93cf0b365 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -19,12 +19,20 @@ -ms-user-select: none; user-select: none; transition: all .1s; - max-height: 250px; + //max-height: 250px; .jsx-parser { - height: 300px; + // height: 300px; //width: 400px; - margin-top: 20px; + // margin-top: 20px; + // overflow-y: scroll; + } + + .documentView-node { + // height: auto !important; + // width: aut !important; + position: absolute; + z-index: 1; } } -- cgit v1.2.3-70-g09d2 From 3152e69dfafe1c393bed38f3aad1e55881e62a33 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 26 Jul 2019 02:39:43 -0400 Subject: initial commit --- deploy/assets/Sunflower.mp3 | Bin 0 -> 7682122 bytes package.json | 1 + src/client/cognitive_services/CognitiveServices.ts | 27 +++++++++- src/client/views/MainView.tsx | 3 ++ src/server/RouteStore.ts | 1 + src/server/index.ts | 59 ++++++++++++++++++++- 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 deploy/assets/Sunflower.mp3 (limited to 'src') diff --git a/deploy/assets/Sunflower.mp3 b/deploy/assets/Sunflower.mp3 new file mode 100644 index 000000000..ab04baac4 Binary files /dev/null and b/deploy/assets/Sunflower.mp3 differ diff --git a/package.json b/package.json index 4a15cbb2f..12f0cd302 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "jsonwebtoken": "^8.5.0", "jsx-to-string": "^1.4.0", "lodash": "^4.17.11", + "microsoft-cognitiveservices-speech-sdk": "^1.6.0", "mobile-detect": "^1.4.3", "mobx": "^5.9.0", "mobx-react": "^5.3.5", diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index d69378d0e..40bbe55a1 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -9,6 +9,10 @@ import { Utils } from "../../Utils"; import { CompileScript } from "../util/Scripting"; import { ComputedField } from "../../new_fields/ScriptField"; import { InkData } from "../../new_fields/InkField"; +import "microsoft-cognitiveservices-speech-sdk"; +import "fs"; +import { AudioInputStream } from "microsoft-cognitiveservices-speech-sdk"; +import { createReadStream, ReadStream } from "fs"; type APIManager = { converter: BodyConverter, requester: RequestExecutor, analyzer: AnalysisApplier }; type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise; @@ -22,7 +26,8 @@ export type Rectangle = { top: number, left: number, width: number, height: numb export enum Service { ComputerVision = "vision", Face = "face", - Handwriting = "handwriting" + Handwriting = "handwriting", + Transcription = "transcription" } export enum Confidence { @@ -232,4 +237,24 @@ export namespace CognitiveServices { } + export namespace Transcription { + + export const Manager: APIManager = { + + converter: (data: string) => data, + + requester: async (apiKey: string, body: string, service: Service) => { + let analysis = await fetch(`${RouteStore.audioData}/${body}`).then(async response => JSON.parse(await response.json())); + console.log(analysis); + return ""; + }, + + analyzer: async (doc: Doc, keys: string[], filename: string) => { + let results = await executeQuery(Service.Transcription, Manager, filename); + } + + }; + + } + } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 61a013963..ca75ab2c4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,6 +39,7 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { ClientUtils } from '../util/ClientUtils'; +import { CognitiveServices } from '../cognitive_services/CognitiveServices'; @observer export class MainView extends React.Component { @@ -67,6 +68,8 @@ export class MainView extends React.Component { componentWillMount() { var tag = document.createElement('script'); + CognitiveServices.Transcription.Manager.analyzer(new Doc, ["hello", "world"], "Sunflower.mp3"); + tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index e30015e39..53f176c81 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -13,6 +13,7 @@ export enum RouteStore { upload = "/upload", dataUriToImage = "/uploadURI", images = "/images", + audioData = "/audioData", // USER AND WORKSPACES getCurrUser = "/getCurrentUser", diff --git a/src/server/index.ts b/src/server/index.ts index 40c0e7981..0a02b667e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -40,6 +40,8 @@ import { Search } from './Search'; import { debug } from 'util'; import _ = require('lodash'); import { Response } from 'express-serve-static-core'; +import { AudioInputStream, AudioConfig, SpeechConfig, SpeechRecognizer, SpeechRecognitionResult } from 'microsoft-cognitiveservices-speech-sdk'; +import { Opt } from '../new_fields/Doc'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -297,7 +299,8 @@ addSecureRoute( const ServicesApiKeyMap = new Map([ ["face", process.env.FACE], ["vision", process.env.VISION], - ["handwriting", process.env.HANDWRITING] + ["handwriting", process.env.HANDWRITING], + ["transcription", process.env.TRANSCRIPTION] ]); addSecureRoute(Method.GET, (user, res, req) => { @@ -305,6 +308,60 @@ addSecureRoute(Method.GET, (user, res, req) => { res.send(ServicesApiKeyMap.get(service)); }, undefined, `${RouteStore.cognitiveServices}/:requestedservice`); +addSecureRoute( + Method.GET, + (user, res, req) => { + let asset = req.params.asset; + let pushStream = AudioInputStream.createPushStream(); + let readStream = fs.createReadStream(path.join(__dirname, '../../deploy/assets/' + asset)); + + let apiKey = process.env.TRANSCRIPTION; + if (!apiKey) { + res.send(undefined); + return; + } + + console.log("API KEY FOUND: ", apiKey); + + readStream.on('data', arrayBuffer => { + pushStream.write(arrayBuffer.buffer); + console.log(arrayBuffer.buffer); + }); + readStream.on('end', () => pushStream.close()); + readStream.on('error', (error) => { + console.log("ERROR! ", error); + res.end(error); + }); + + let audioConfig = AudioConfig.fromStreamInput(pushStream); + let speechConfig = SpeechConfig.fromSubscription(apiKey, "eastus"); + + console.log("Here are the configs!"); + console.log(audioConfig); + console.log(speechConfig); + + speechConfig.speechRecognitionLanguage = "en-US"; + + let recognizer: Opt = new SpeechRecognizer(speechConfig, audioConfig); + recognizer.recognizeOnceAsync( + (result: SpeechRecognitionResult) => { + console.log("RESULT! ", result); + res.send(result); + recognizer && recognizer.close(); + recognizer = undefined; + }, + (error: string) => { + console.log("RESULT ERROR: ", error); + res.send(error); + recognizer && recognizer.close(); + recognizer = undefined; + }, + ); + }, + undefined, + `${RouteStore.audioData}/:asset` +); + class NodeCanvasFactory { create = (width: number, height: number) => { var canvas = createCanvas(width, height); -- cgit v1.2.3-70-g09d2 From 4e73b2b844846ee39e743f69df6225fb4cbf4f2c Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Fri, 26 Jul 2019 17:24:27 -0400 Subject: Preview shown with right css --- src/client/views/presentationview/PresentationElement.tsx | 14 ++++++++++---- src/client/views/presentationview/PresentationView.tsx | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index ff68f47ba..3297acee9 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -800,7 +800,12 @@ export default class PresentationElement extends React.Component; let propDocWidth = NumCast(this.props.document.nativeWidth); let propDocHeight = NumCast(this.props.document.nativeHeight); - let scale = () => 175 / NumCast(this.props.document.nativeWidth, 175); + let previewWidth = NumCast(this.props.document.schemaPreviewWidth); + let scale = () => { + let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); + console.log("New Scale: ", newScale); + return newScale; + }; return ( // { }} // getScale={() => 3.1415} // /> +
    20} - PanelHeight={() => 20} + PanelWidth={() => 350} + PanelHeight={() => 100} focus={emptyFunction} selectOnLoad={false} parentActive={returnFalse} diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index 95458b4ca..718af1bf2 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -811,7 +811,7 @@ export class PresentationView extends React.Component { let width = NumCast(this.curPresentation.width); return ( -
    +
    {this.renderSelectOrPresSelection()} -- cgit v1.2.3-70-g09d2 From c6da9a2800037a951ea5070b4a77663401c4d00e Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Fri, 26 Jul 2019 18:52:30 -0400 Subject: resizable pres view --- .../views/presentationview/PresentationElement.tsx | 22 ----- .../views/presentationview/PresentationView.scss | 2 +- .../views/presentationview/PresentationView.tsx | 109 +++++++++++++++------ 3 files changed, 80 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 3297acee9..fdddf0e41 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -800,36 +800,14 @@ export default class PresentationElement extends React.Component; let propDocWidth = NumCast(this.props.document.nativeWidth); let propDocHeight = NumCast(this.props.document.nativeHeight); - let previewWidth = NumCast(this.props.document.schemaPreviewWidth); let scale = () => { let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); console.log("New Scale: ", newScale); return newScale; }; return ( - // 1} - // PanelWidth={() => 400} - // PanelHeight={() => 400} - // focus={(doc: Doc, willZoom: boolean) => { }} - // selectOnLoad={false} - // parentActive={returnFalse} - // whenActiveChanged={(isActive: boolean) => { }} - // bringToFront={(doc: Doc) => { }} - // addDocTab={(doc: Doc, dataDoc: Doc | undefined, where: string) => { }} - // zoomToScale={(scale: number) => { }} - // getScale={() => 3.1415} - // /> -
    { //Variable that holds reference to title input, so that new presentations get titles assigned. @observable titleInputElement: HTMLInputElement | undefined; @observable PresTitleChangeOpen: boolean = false; + @observable PresViewWidth: number | undefined; //initilize class variables constructor(props: PresViewProps) { @@ -81,6 +82,7 @@ export class PresentationView extends React.Component { //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. @@ -806,43 +808,90 @@ export class PresentationView extends React.Component { this.presElementsMappings.set(keyDoc, elem); } + //_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(); + } + @action + onPointerMove = (e: PointerEvent) => { + + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, 255); + } + @action + onPointerUp = (e: PointerEvent) => { + //this.isPointerDown = false; + // if (Math.abs(e.clientX - this._downsize) < 4) { + // if (this.flyoutWidth < 5) this.flyoutWidth = 250; + // else this.flyoutWidth = 0; + // } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + togglePresView = (e) => { + e.stopPropagation(); + e.preventDefault(); + let width = NumCast(this.curPresentation.width); + if (width === 0) { + this.curPresentation.width = 255; + } else { + this.curPresentation.width = 0; + } + } + render() { let width = NumCast(this.curPresentation.width); + this.PresViewWidth = width; + return ( -
    -
    - {this.renderSelectOrPresSelection()} - - - - +
    +
    +
    + {this.renderSelectOrPresSelection()} + + + + +
    +
    + + {this.renderPlayPauseButton()} + +
    + + this.presElementsMappings.clear()} + />
    -
    - - {this.renderPlayPauseButton()} - +
    +
    - this.presElementsMappings.clear()} - />
    ); } -- cgit v1.2.3-70-g09d2 From 811cb72cdb04b316f19d02c609ad515d6ec0a1b1 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Fri, 26 Jul 2019 18:54:37 -0400 Subject: CleanUp --- src/client/views/presentationview/PresentationView.scss | 7 +------ src/client/views/presentationview/PresentationView.tsx | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 5769a1a0a..ee413f379 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -21,12 +21,7 @@ transition: all .1s; //max-height: 250px; - .jsx-parser { - // height: 300px; - //width: 400px; - // margin-top: 20px; - // overflow-y: scroll; - } + .documentView-node { // height: auto !important; diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index 273fa5e5f..de6c8ff82 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -834,7 +834,7 @@ export class PresentationView extends React.Component { document.removeEventListener("pointerup", this.onPointerUp); } - togglePresView = (e) => { + togglePresView = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); let width = NumCast(this.curPresentation.width); -- cgit v1.2.3-70-g09d2 From 215b73fbcfe0d6f205668e1bb7c755228e858ac9 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Mon, 29 Jul 2019 19:19:49 -0400 Subject: PresMode, CLoseable Pres, and some bug fixes --- src/client/views/MainView.tsx | 2 +- .../views/presentationview/PresentationElement.tsx | 24 ++++- .../views/presentationview/PresentationList.tsx | 2 +- .../presentationview/PresentationModeMenu.scss | 30 ++++++ .../presentationview/PresentationModeMenu.tsx | 92 ++++++++++++++++ .../views/presentationview/PresentationView.scss | 2 +- .../views/presentationview/PresentationView.tsx | 116 +++++++++++++++++---- 7 files changed, 238 insertions(+), 30 deletions(-) create mode 100644 src/client/views/presentationview/PresentationModeMenu.scss create mode 100644 src/client/views/presentationview/PresentationModeMenu.tsx (limited to 'src') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 53be0278e..0dbb241e4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -267,7 +267,7 @@ export class MainView extends React.Component { zoomToScale={emptyFunction} getScale={returnOne} />} - {castRes ? : null} + {castRes ? : null}
    } ; diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index fdddf0e41..05a70f79a 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -8,7 +8,7 @@ import "./PresentationView.scss"; import { Utils, emptyFunction, returnFalse, returnOne } from "../../../Utils"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSearch, faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; @@ -28,6 +28,7 @@ library.add(fileSolid); library.add(faLocationArrow); library.add(fileRegular as any); library.add(faSearch); +library.add(faArrowRight); interface PresentationElementProps { mainDocument: Doc; @@ -54,6 +55,7 @@ export enum buttonIndex { FadeAfter = 3, HideAfter = 4, Group = 5, + OpenRight = 6 } @@ -76,7 +78,7 @@ export default class PresentationElement extends React.Component { + 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. */ @@ -637,7 +651,7 @@ export default class PresentationElement extends React.Component { 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!) { @@ -887,6 +901,8 @@ export default class PresentationElement extends React.Component + +
    {this.renderEmbeddedInline()}
    diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index 2d63d41b5..2a31137af 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -95,7 +95,7 @@ export default class PresentationViewList extends React.Component this.props.PresElementsMappings.set(doc, e); } }} - key={doc[Id]} + key={doc[Id] + index} mainDocument={this.props.mainDocument} document={doc} index={index} 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..b3edeb1e2 --- /dev/null +++ b/src/client/views/presentationview/PresentationModeMenu.tsx @@ -0,0 +1,92 @@ +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; +} + +@observer +export default class PresModeMenu extends React.Component { + + @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 = ""; + //@observable private Pinned: boolean = false; + + + private _mainCont: React.RefObject = React.createRef(); + + @action + pointerEntered = (e: React.PointerEvent) => { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 1; + } + + @action + dragging = (e: PointerEvent) => { + this._right -= e.movementX; + this._top += e.movementY; + + e.stopPropagation(); + e.preventDefault(); + } + + dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointerup", this.dragEnd); + let clientRect = this._mainCont.current!.getBoundingClientRect(); + + // runInAction(() => this._left = (clientRect.width - e.nativeEvent.offsetX) + clientRect.left); + // runInAction(() => this._top = e.nativeEvent.offsetY); + + e.stopPropagation(); + e.preventDefault(); + } + + renderPlayPauseButton = () => { + if (this.props.presStatus) { + return ; + } else { + return ; + } + } + + render() { + return ( +
    + + {this.renderPlayPauseButton()} + + +
    +
    + ); + } + + + + +} \ No newline at end of file diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index ee413f379..97cbd4a24 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -66,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 de6c8ff82..70d86624c 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,11 +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 { ContextMenu } from "../ContextMenu"; +import PresModeMenu from "./PresentationModeMenu"; library.add(faArrowLeft); library.add(faArrowRight); @@ -26,10 +27,12 @@ library.add(faPlus); library.add(faTimes); library.add(faMinus); library.add(faEdit); +library.add(faEye); export interface PresViewProps { Documents: List; + addDocTab: (doc: Doc) => void; } const expandedWidth = 400; @@ -63,7 +66,8 @@ export class PresentationView extends React.Component { //Variable that holds reference to title input, so that new presentations get titles assigned. @observable titleInputElement: HTMLInputElement | undefined; @observable PresTitleChangeOpen: boolean = false; - @observable PresViewWidth: number | undefined; + @observable presMode: boolean = false; + //initilize class variables constructor(props: PresViewProps) { @@ -360,11 +364,21 @@ export class PresentationView extends React.Component { //checking if curDoc has navigation open let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; if (curDocButtons[buttonIndex.Navigate]) { - DocumentManager.Instance.jumpToDocument(curDoc, false); + // if (curDocButtons[buttonIndex.OpenRight]) { + // DocumentManager.Instance.jumpToDocument(curDoc, false); + // } else { + // DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); + // } + 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, this.props.addDocTab); + } + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; @@ -377,9 +391,15 @@ export class PresentationView extends React.Component { 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, this.props.addDocTab); + } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; //saving the scale that user was on @@ -389,6 +409,15 @@ export class PresentationView extends React.Component { } + jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => { + if (curDocButtons[buttonIndex.OpenRight]) { + DocumentManager.Instance.jumpToDocument(curDoc, false); + } else { + console.log("Open in tab!!"); + DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); + } + } + /** * Async function that supposedly return the doc that is located at given index. */ @@ -578,18 +607,35 @@ export class PresentationView extends React.Component { //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; } + 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 @@ -808,9 +854,9 @@ export class PresentationView extends React.Component { this.presElementsMappings.set(keyDoc, elem); } - //_downsize = 0; + _downsize = 0; onPointerDown = (e: React.PointerEvent) => { - //this._downsize = e.clientX; + this._downsize = e.clientX; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -821,15 +867,16 @@ export class PresentationView extends React.Component { @action onPointerMove = (e: PointerEvent) => { - this.curPresentation.width = Math.max(window.innerWidth - e.clientX, 255); + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, 300); } @action onPointerUp = (e: PointerEvent) => { - //this.isPointerDown = false; - // if (Math.abs(e.clientX - this._downsize) < 4) { - // if (this.flyoutWidth < 5) this.flyoutWidth = 250; - // else this.flyoutWidth = 0; - // } + if (Math.abs(e.clientX - this._downsize) < 4) { + let presWidth = NumCast(this.curPresentation.width); + if (presWidth - 300 !== 0) { + this.curPresentation.width = 0; + } + } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } @@ -839,17 +886,38 @@ export class PresentationView extends React.Component { e.preventDefault(); let width = NumCast(this.curPresentation.width); if (width === 0) { - this.curPresentation.width = 255; - } else { + this.curPresentation.width = 300; + } + } + @action + openPresMode = () => { + if (!this.presMode) { this.curPresentation.width = 0; + this.presMode = true; + } + } + + @action + closePresMode = () => { + if (this.presMode) { + this.presMode = false; + this.curPresentation.width = 300; + } + + } + + renderPresMode = () => { + if (this.presMode) { + return ; + } else { + return (null); } + } render() { let width = NumCast(this.curPresentation.width); - this.PresViewWidth = width; - return (
    @@ -857,6 +925,7 @@ export class PresentationView extends React.Component {
    {this.renderSelectOrPresSelection()} +
    + {this.renderPresMode()}
    ); } -- cgit v1.2.3-70-g09d2 From 7b2bbf518779db3d5ef389f6a9f0d41aa2862999 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Mon, 29 Jul 2019 19:54:00 -0400 Subject: trying to get tab --- src/client/views/presentationview/PresentationView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index cd38fc2ec..c583ae4ce 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -18,6 +18,7 @@ import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; import { ContextMenu } from "../ContextMenu"; import PresModeMenu from "./PresentationModeMenu"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; library.add(faArrowLeft); library.add(faArrowRight); @@ -418,7 +419,8 @@ export class PresentationView extends React.Component { DocumentManager.Instance.jumpToDocument(curDoc, false); } else { console.log("Open in tab!!"); - DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); + // DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); + CollectionDockingView.Instance.AddTab(undefined, curDoc, undefined); } } -- cgit v1.2.3-70-g09d2 From c8074da503a5f4d252417c652709f7a393d7d158 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 29 Jul 2019 20:07:27 -0400 Subject: Partially fixed add tab --- src/client/views/collections/CollectionDockingView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ab4d1aa62..3a402b6b2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -210,14 +210,14 @@ export class CollectionDockingView extends React.Component Date: Mon, 29 Jul 2019 20:22:13 -0400 Subject: Important fix for backUp receive --- src/client/views/presentationview/PresentationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index 2a31137af..2d63d41b5 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -95,7 +95,7 @@ export default class PresentationViewList extends React.Component this.props.PresElementsMappings.set(doc, e); } }} - key={doc[Id] + index} + key={doc[Id]} mainDocument={this.props.mainDocument} document={doc} index={index} -- cgit v1.2.3-70-g09d2 From c4a8b656eff910fb6c2cd1693308f0b948547cb0 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Mon, 29 Jul 2019 20:43:18 -0400 Subject: Opening as a tab if not present as default instead of open right --- src/client/views/MainView.tsx | 2 +- src/client/views/presentationview/PresentationView.tsx | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0f9f2687b..91c8fe57c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -277,7 +277,7 @@ export class MainView extends React.Component { zoomToScale={emptyFunction} getScale={returnOne} />} - {castRes ? : null} + {castRes ? : null}
    } ; diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index c583ae4ce..ea85a8c6a 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -33,7 +33,6 @@ library.add(faEye); export interface PresViewProps { Documents: List; - addDocTab: (doc: Doc) => void; } const expandedWidth = 400; @@ -369,11 +368,6 @@ export class PresentationView extends React.Component { //checking if curDoc has navigation open let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; if (curDocButtons[buttonIndex.Navigate]) { - // if (curDocButtons[buttonIndex.OpenRight]) { - // DocumentManager.Instance.jumpToDocument(curDoc, false); - // } else { - // DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); - // } this.jumpToTabOrRight(curDocButtons, curDoc); } else if (curDocButtons[buttonIndex.Show]) { let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); @@ -381,7 +375,7 @@ export class PresentationView extends React.Component { //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, this.props.addDocTab); + await DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); @@ -403,7 +397,7 @@ export class PresentationView extends React.Component { //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, this.props.addDocTab); + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; @@ -418,9 +412,7 @@ export class PresentationView extends React.Component { if (curDocButtons[buttonIndex.OpenRight]) { DocumentManager.Instance.jumpToDocument(curDoc, false); } else { - console.log("Open in tab!!"); - // DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, this.props.addDocTab); - CollectionDockingView.Instance.AddTab(undefined, curDoc, undefined); + DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); } } -- cgit v1.2.3-70-g09d2 From 6954d47c252de5eadb88725963773fe0b9b0bffe Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 29 Jul 2019 20:48:02 -0400 Subject: Changed how AddTab works to work better --- .../views/collections/CollectionDockingView.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 3a402b6b2..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 Date: Mon, 29 Jul 2019 23:48:35 -0400 Subject: styling changes to stacking --- src/client/views/collections/CollectionStackingView.scss | 5 +++-- .../views/collections/CollectionStackingViewFieldColumn.tsx | 8 ++++++-- src/client/views/pdf/Annotation.tsx | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) (limited to 'src') 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 + 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. */}
    { 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"))); } } } -- cgit v1.2.3-70-g09d2 From 2468ab85d642b954e47a560e2d0739dba9674f88 Mon Sep 17 00:00:00 2001 From: yipstanley Date: Tue, 30 Jul 2019 00:02:21 -0400 Subject: put back contains/not contains colors --- src/client/views/collections/KeyRestrictionRow.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') 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 runInAction(() => this._key = e.target.value)} placeholder="KEY" /> -- cgit v1.2.3-70-g09d2 From 5248a770123e312e1684d7147ecb7118dd6ef1e7 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 30 Jul 2019 01:24:37 -0400 Subject: tag generation and faces on extensionDoc --- src/client/cognitive_services/CognitiveServices.ts | 40 ++++-------------- src/client/views/nodes/FaceRectangles.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 49 +++++++++++++++++++--- 3 files changed, 54 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index bbc438a9b..076907f09 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -42,7 +42,7 @@ export enum Confidence { */ export namespace CognitiveServices { - const executeQuery = async (service: Service, manager: APIManager, data: D): Promise> => { + const ExecuteQuery = async (service: Service, manager: APIManager, data: D): Promise> => { return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => { let apiKey = await response.text(); if (!apiKey) { @@ -103,15 +103,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(service, Manager, imageData.url.href); + let results = await ExecuteQuery(service, Manager, url); if (!results) { toStore = "Cognitive Services could not process the given image URL."; } else { @@ -122,6 +122,7 @@ export namespace CognitiveServices { } } target[storageKey] = toStore; + batch.end(); } @@ -129,31 +130,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(); - 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 +185,8 @@ export namespace CognitiveServices { analyzer: async (target: Doc, keys: string[], inkData: InkData) => { let batch = UndoManager.StartBatch("Ink Analysis"); - let results = await executeQuery(Service.Handwriting, Manager, inkData); + + let results = await ExecuteQuery(Service.Handwriting, Manager, inkData); if (results) { results.recognitionUnits && (results = results.recognitionUnits); target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis"); @@ -217,6 +194,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/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 { 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 29a76b0c8..5554475ec 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'); @@ -226,20 +229,56 @@ export class ImageBox extends DocComponent(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(); + 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); @@ -394,7 +433,7 @@ export class ImageBox extends DocComponent(ImageD style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
    {/* {this.lightbox(paths)} */} - +
    ); } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 4f0ea3428e1e98d42b356c00f276fe74a6c4a672 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 30 Jul 2019 09:37:38 -0400 Subject: removed TreeView export --- src/client/views/collections/CollectionTreeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 6cc065342..b1e6eada0 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -66,7 +66,7 @@ library.add(faPlus, faMinus); /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ -export class TreeView extends React.Component { +class TreeView extends React.Component { private _header?: React.RefObject = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef(); -- cgit v1.2.3-70-g09d2 From 5b455e2aaf119c7db1fe9ef22d71a3accf55a8e2 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 30 Jul 2019 10:27:42 -0400 Subject: tweaks. --- src/client/apis/youtube/YoutubeBox.tsx | 22 +++++++++------------- src/client/views/collections/CollectionView.tsx | 2 +- .../views/collections/CollectionViewChromes.tsx | 1 + 3 files changed, 11 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 019d191bc..7f9a3ad70 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -1,20 +1,16 @@ -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 { action, observable, runInAction } from 'mobx'; 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 { Doc, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; 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"; +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; 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 { } else { return [ - (), + (), 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 { }} onPointerDown={this.openViewSpecs} />
    Date: Tue, 30 Jul 2019 11:00:57 -0400 Subject: clean up --- src/client/cognitive_services/CognitiveServices.ts | 8 +++----- src/client/views/MainView.tsx | 1 - src/server/index.ts | 4 +--- 3 files changed, 4 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 720892b61..b9c718cfa 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -5,8 +5,6 @@ import { Docs } from "../documents/Documents"; import { RouteStore } from "../../server/RouteStore"; import { Utils } from "../../Utils"; import { InkData } from "../../new_fields/InkField"; -import "microsoft-cognitiveservices-speech-sdk"; -import "fs"; import { UndoManager } from "../util/UndoManager"; type APIManager = { converter: BodyConverter, requester: RequestExecutor, analyzer: AnalysisApplier }; @@ -27,7 +25,7 @@ export type Rectangle = { top: number, left: number, width: number, height: numb export enum Service { ComputerVision = "vision", Face = "face", - Handwriting = "handwriting", + Handwriting = "handwriting" } export enum Confidence { @@ -221,7 +219,7 @@ export namespace CognitiveServices { export namespace Transcription { - export const analyzer = (doc: Doc, keys: string[]) => { + export const analyzer = (target: Doc, keys: string[]) => { let { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; let recognizer = new webkitSpeechRecognition(); recognizer.interimResults = true; @@ -229,7 +227,7 @@ export namespace CognitiveServices { recognizer.onresult = (e: any) => { let result = e.results[0][0]; - doc[keys[0]] = result.transcript; + target[keys[0]] = result.transcript; }; recognizer.start(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ccf8f571e..91c8fe57c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -38,7 +38,6 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { ClientUtils } from '../util/ClientUtils'; -import { CognitiveServices } from '../cognitive_services/CognitiveServices'; import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; @observer diff --git a/src/server/index.ts b/src/server/index.ts index f4bbd4423..adf218be6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -40,8 +40,6 @@ import { Search } from './Search'; import { debug } from 'util'; import _ = require('lodash'); import { Response } from 'express-serve-static-core'; -import { AudioInputStream, AudioConfig, SpeechConfig, SpeechRecognizer, SpeechRecognitionResult } from 'microsoft-cognitiveservices-speech-sdk'; -import { Opt } from '../new_fields/Doc'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -299,7 +297,7 @@ addSecureRoute( const ServicesApiKeyMap = new Map([ ["face", process.env.FACE], ["vision", process.env.VISION], - ["handwriting", process.env.HANDWRITING], + ["handwriting", process.env.HANDWRITING] ]); addSecureRoute(Method.GET, (user, res, req) => { -- cgit v1.2.3-70-g09d2 From 3468440a4297a105cf4138892c3d163ca9a0c83c Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 30 Jul 2019 11:30:48 -0400 Subject: CleanUp and Documentation --- .../views/presentationview/PresentationElement.tsx | 37 ++++++----- .../views/presentationview/PresentationList.tsx | 2 - .../presentationview/PresentationModeMenu.tsx | 34 ++++++---- .../views/presentationview/PresentationView.tsx | 76 ++++++++++++++-------- 4 files changed, 92 insertions(+), 57 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index ccc3a72a9..11f3eb846 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -12,12 +12,8 @@ import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils, returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; -import { indexOf } from "typescript-collections/dist/lib/arrays"; -import { map } from "bluebird"; import { ContextMenu } from "../ContextMenu"; -import { DocumentContentsView } from "../nodes/DocumentContentsView"; import { Transform } from "../../util/Transform"; -import { FieldView } from "../nodes/FieldView"; import { DocumentView } from "../nodes/DocumentView"; import { DocumentType } from "../../documents/Documents"; import React = require("react"); @@ -73,9 +69,6 @@ export default class PresentationElement extends React.Component { //get the list that stores docs that keep track of buttons @@ -404,6 +400,10 @@ export default class PresentationElement extends React.Component { e.stopPropagation(); @@ -671,7 +671,6 @@ export default class PresentationElement extends React.Component { - // this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -688,7 +687,6 @@ export default class PresentationElement extends React.Component { - // this.props.document.libraryBrush = false; //to get currently selected presentation doc let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -787,15 +785,23 @@ export default class PresentationElement extends React.Component) => { e.preventDefault(); e.stopPropagation(); @@ -803,20 +809,19 @@ export default class PresentationElement extends React.Component { if (!this.embedInline) { return (null); } - // return
      - // {TreeView.GetChildElements([this.props.document], "", new Doc(), undefined, "", (doc: Doc, relativeTo?: Doc, before?: boolean) => false, this.props.removeDocByRef, this.move, - // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} - //
    ; 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); - console.log("New Scale: ", newScale); return newScale; }; return ( @@ -836,7 +841,7 @@ export default class PresentationElement extends React.Component 350} - PanelHeight={() => 100} + PanelHeight={() => 90} focus={emptyFunction} selectOnLoad={false} parentActive={returnFalse} 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.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx index b3edeb1e2..4de8da587 100644 --- a/src/client/views/presentationview/PresentationModeMenu.tsx +++ b/src/client/views/presentationview/PresentationModeMenu.tsx @@ -13,6 +13,10 @@ export interface PresModeMenuProps { 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 { @@ -21,18 +25,14 @@ export default class PresModeMenu extends React.Component { @observable private _opacity: number = 1; @observable private _transition: string = "opacity 0.5s"; @observable private _transitionDelay: string = ""; - //@observable private Pinned: boolean = false; private _mainCont: React.RefObject = React.createRef(); - @action - pointerEntered = (e: React.PointerEvent) => { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 1; - } - + /** + * 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; @@ -42,6 +42,10 @@ export default class PresModeMenu extends React.Component { 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); @@ -49,20 +53,24 @@ export default class PresModeMenu extends React.Component { 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); - let clientRect = this._mainCont.current!.getBoundingClientRect(); - - // runInAction(() => this._left = (clientRect.width - e.nativeEvent.offsetX) + clientRect.left); - // runInAction(() => this._top = e.nativeEvent.offsetY); 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 ; @@ -73,7 +81,7 @@ export default class PresModeMenu extends React.Component { render() { return ( -
    {this.renderPlayPauseButton()} diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index ea85a8c6a..4fe9d3a1b 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -16,7 +16,6 @@ import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, fa import { Docs } from "../../documents/Documents"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; -import { ContextMenu } from "../ContextMenu"; import PresModeMenu from "./PresentationModeMenu"; import { CollectionDockingView } from "../collections/CollectionDockingView"; @@ -36,6 +35,7 @@ export interface PresViewProps { } const expandedWidth = 400; +const presMinWidth = 300; @observer export class PresentationView extends React.Component { @@ -408,6 +408,10 @@ export class PresentationView extends React.Component { } + /** + * 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); @@ -460,22 +464,6 @@ export class PresentationView extends React.Component { } } - //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) { @@ -513,13 +501,16 @@ export class PresentationView extends React.Component { } } + /** + * 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; } @@ -618,6 +609,11 @@ export class PresentationView extends React.Component { 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) { @@ -848,10 +844,11 @@ export class PresentationView extends React.Component { 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; @@ -862,16 +859,26 @@ export class PresentationView extends React.Component { 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, 300); + 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 - 300 !== 0) { + if (presWidth - presMinWidth !== 0) { this.curPresentation.width = 0; } } @@ -879,14 +886,23 @@ export class PresentationView extends React.Component { 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 = 300; + 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) { @@ -895,15 +911,23 @@ export class PresentationView extends React.Component { } } + /** + * 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 = 300; + this.curPresentation.width = presMinWidth; } } + /** + * Function that is called to render the presentation mode, depending on its flag. + */ renderPresMode = () => { if (this.presMode) { return ; -- cgit v1.2.3-70-g09d2 From b8ace05da169802bc18d138b83576dbab98053e9 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 30 Jul 2019 12:50:18 -0400 Subject: added youtube as button snapshots. changed video default to 10. --- src/client/views/nodes/VideoBox.tsx | 49 +++++++++++++++++++++---------- src/server/youtubeApi/youtubeApiSample.js | 2 +- 2 files changed, 35 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 34cb47b20..d2657227a 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); @@ -165,21 +167,38 @@ export class VideoBox extends DocComponent(VideoD 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); + 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(`this.props.Document.curPage = ${NumCast(this.props.Document.curPage)}`, { + params: { this: Doc.name }, + typecheck: false, + editable: true, + }); + if (!script.compiled) { + console.log(script.errors.map(error => error.messageText).join("\n")); + return; } - }); + b.onClick = new ScriptField(script); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); + } 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); + } + }); + } }, icon: "expand-arrows-alt" }); diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js index ec532c78e..50b3c7b38 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -148,7 +148,7 @@ function getVideos(auth, args) { part: 'id, snippet', type: 'video', q: args.userInput, - maxResults: 3 + maxResults: 10 }, function (err, response) { if (err) { console.log('The API returned an error: ' + err); -- cgit v1.2.3-70-g09d2 From 1aac1e8820c62a5f06d7e7630394e0bd58b19a94 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 30 Jul 2019 12:56:25 -0400 Subject: refactored and implemented dictation manager and shift keyboard shortcut for voice commands --- package.json | 2 +- src/client/cognitive_services/CognitiveServices.ts | 24 ------------- src/client/util/DictationManager.ts | 39 ++++++++++++++++++++++ src/client/views/ContextMenu.tsx | 8 +++-- src/client/views/GlobalKeyHandler.ts | 29 ++++++++++++++-- src/client/views/nodes/DocumentView.tsx | 8 ++++- 6 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 src/client/util/DictationManager.ts (limited to 'src') diff --git a/package.json b/package.json index 3f100a3ef..37052fde3 100644 --- a/package.json +++ b/package.json @@ -203,4 +203,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} \ No newline at end of file +} diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index b9c718cfa..c118d91d3 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -13,12 +13,6 @@ type AnalysisApplier = (target: Doc, relevantKeys: string[], ...args: any) => an type BodyConverter = (data: D) => string; type Converter = (results: any) => Field; -namespace CORE { - export interface IWindow extends Window { - webkitSpeechRecognition: any; - } -} - export type Tag = { name: string, confidence: number }; export type Rectangle = { top: number, left: number, width: number, height: number }; @@ -217,22 +211,4 @@ export namespace CognitiveServices { } - export namespace Transcription { - - export const analyzer = (target: Doc, keys: string[]) => { - let { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; - let recognizer = new webkitSpeechRecognition(); - recognizer.interimResults = true; - recognizer.continuous = true; - - recognizer.onresult = (e: any) => { - let result = e.results[0][0]; - target[keys[0]] = result.transcript; - }; - - recognizer.start(); - }; - - } - } \ No newline at end of file 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((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; 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/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fd53cd451..dc56c1c8f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -42,6 +42,7 @@ 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); @@ -536,6 +537,11 @@ export class DocumentView extends DocComponent(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 => { e.persist(); @@ -559,7 +565,7 @@ export class DocumentView extends DocComponent(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: () => CognitiveServices.Transcription.analyzer(Doc.GetProto(this.props.Document), ["transcript"]), icon: "microphone" }); + 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" }); -- cgit v1.2.3-70-g09d2 From 937af0cd2b2c01e7971c1edf120567ee15a4d5b9 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 30 Jul 2019 13:43:02 -0400 Subject: fixed snapshots for youtube videos. --- src/client/views/MainView.tsx | 3 +- .../views/collections/CollectionVideoView.scss | 9 ++ .../views/collections/CollectionVideoView.tsx | 24 +++-- src/client/views/nodes/VideoBox.tsx | 101 +++++++++++---------- 4 files changed, 80 insertions(+), 57 deletions(-) (limited to 'src') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ababbe949..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, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, 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 { 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'; @@ -126,6 +126,7 @@ export class MainView extends React.Component { library.add(faMusic); library.add(faTree); library.add(faPlay); + library.add(faPause); library.add(faClone); library.add(faCut); library.add(faCommentAlt); 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 { private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); let curTime = NumCast(this.props.Document.curPage); - return ([
    + return ([
    {"" + Math.round(curTime)} {" " + Math.round((curTime - Math.trunc(curTime)) * 100)}
    , +
    + +
    , VideoBox._showControls ? (null) : [ -
    - {this._videoBox && this._videoBox.Playing ? "\"" : ">"} +
    +
    , -
    +
    F -
    - +
    ]]); } @@ -56,6 +59,15 @@ export class CollectionVideoView extends React.Component { } } + @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/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index d2657227a..1f8636826 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -89,6 +89,56 @@ export class VideoBox extends DocComponent(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); @@ -152,56 +202,7 @@ export class VideoBox extends DocComponent(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); - } - - 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(`this.props.Document.curPage = ${NumCast(this.props.Document.curPage)}`, { - params: { this: Doc.name }, - typecheck: false, - editable: true, - }); - if (!script.compiled) { - console.log(script.errors.map(error => error.messageText).join("\n")); - return; - } - b.onClick = new ScriptField(script); - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); - } 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); - } - }); - } - }, - 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" }); } } -- cgit v1.2.3-70-g09d2