diff options
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/client/DocServer.ts | 6 | ||||
-rw-r--r-- | src/client/apis/youtube/YoutubeBox.tsx | 72 | ||||
-rw-r--r-- | src/client/apis/youtube/youtubeApiSample.js | 22 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 20 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
-rw-r--r-- | src/new_fields/URLField.ts | 3 | ||||
-rw-r--r-- | src/server/Message.ts | 1 | ||||
-rw-r--r-- | src/server/index.ts | 20 |
10 files changed, 135 insertions, 20 deletions
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<RefField> }> { 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<FieldViewProps> { + + 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 = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; + } else if (field instanceof WebField) { + view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />; + } else { + view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />; + } + let content = + <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + {view} + </div>; + + let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + + let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + return ( + <> + <div className={classname} > + {content} + </div> + {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + </>); + } +}
\ No newline at end of file diff --git a/src/client/apis/youtube/youtubeApiSample.js b/src/client/apis/youtube/youtubeApiSample.js index 07c3add36..7f14f2d3e 100644 --- a/src/client/apis/youtube/youtubeApiSample.js +++ b/src/client/apis/youtube/youtubeApiSample.js @@ -1,7 +1,5 @@ -let fs = require('fs'); -let readline = require('readline'); -let { google } = require('googleapis'); -let OAuth2 = google.auth.OAuth2; +import { Utils } from "tslint"; + // If modifying these scopes, delete your previously saved credentials // at ~/.credentials/youtube-nodejs-quickstart.json @@ -10,18 +8,14 @@ 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); - }); + + +function authorizedGetChannel(apiKey) { + // Authorize a client with the loaded credentials, then call the YouTube API. + authorize(JSON.parse(apiKey), getChannel); } + /** * Create an OAuth2 client with the given credentials, and then execute the * given callback function. diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ab61b915c..fd4532807 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -23,7 +23,7 @@ import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; import { OmitKeys } from "../../Utils"; -import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField"; +import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; import { Cast, NumCast } from "../../new_fields/Types"; @@ -35,6 +35,8 @@ import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; +import { createInstance } from "@react-pdf/renderer"; +import { YoutubeBox } from "../apis/youtube/YoutubeBox"; var requestImageSize = require('request-image-size'); var path = require('path'); @@ -113,6 +115,7 @@ export namespace Docs { let audioProto: Doc; let pdfProto: Doc; let iconProto: Doc; + let youtubeProto: Doc; const textProtoId = "textProto"; const histoProtoId = "histoProto"; const pdfProtoId = "pdfProto"; @@ -123,9 +126,10 @@ export namespace Docs { const videoProtoId = "videoProto"; const audioProtoId = "audioProto"; const iconProtoId = "iconProto"; + const youtubeProtoId = "youtubeProto"; export function initProtos(): Promise<void> { - return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => { + return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId, youtubeProtoId]).then(fields => { textProto = fields[textProtoId] as Doc || CreateTextPrototype(); histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype(); collProto = fields[collProtoId] as Doc || CreateCollectionPrototype(); @@ -136,6 +140,7 @@ export namespace Docs { audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype(); pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype(); iconProto = fields[iconProtoId] as Doc || CreateIconPrototype(); + youtubeProto = fields[youtubeProtoId] as Doc || CreateYoutubePrototype(); }); } @@ -183,6 +188,13 @@ export namespace Docs { { x: 0, y: 0, width: 300, height: 300 }); return webProto; } + function CreateYoutubePrototype(): Doc { + let webProto = setupPrototypeOptions(youtubeProtoId, "YOUTUBE_PROTO", YoutubeBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 300 }); + return webProto; + } + + function CreateCollectionPrototype(): Doc { let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"), { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }); @@ -238,6 +250,10 @@ export namespace Docs { // doc.SetText(KeyStore.OverlayLayout, FixedCaption()); // return doc; } + export function YoutubeDocument(url: string, options: DocumentOptions = {}) { + return CreateInstance(youtubeProto, new YoutubeField(new URL(url)), options); + } + export function VideoDocument(url: string, options: DocumentOptions = {}) { return CreateInstance(videoProto, new VideoField(new URL(url)), options); } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7d2aa3199..4868fa41c 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 { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons'; +import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell, faPlay } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; @@ -89,6 +89,7 @@ export class MainView extends React.Component { library.add(faFilm); library.add(faMusic); library.add(faTree); + library.add(faPlay); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -226,6 +227,7 @@ export class MainView extends React.Component { let weburl = "https://cs.brown.edu/courses/cs166/"; let audiourl = "http://techslides.com/demos/samples/sample.mp3"; let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; + let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" })); let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" })); @@ -238,6 +240,7 @@ export class MainView extends React.Component { let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })); + let addYoutubeSearcher = action(() => Docs.YoutubeDocument(youtubeurl, { width: 200, height: 200, title: "youtube node" })); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode], @@ -249,6 +252,7 @@ export class MainView extends React.Component { [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], + [React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher] ]; return < div id="add-nodes-menu" > diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 02396c3af..d242d8fad 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -17,6 +17,7 @@ import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { FieldView } from "./FieldView"; import { WebBox } from "./WebBox"; +import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; @@ -103,7 +104,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { render() { if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser - components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts index 4a2841fb6..6e4cfa2ed 100644 --- a/src/new_fields/URLField.ts +++ b/src/new_fields/URLField.ts @@ -41,4 +41,5 @@ export abstract class URLField extends ObjectField { @Deserializable("image") export class ImageField extends URLField { } @Deserializable("video") export class VideoField extends URLField { } @Deserializable("pdf") export class PdfField extends URLField { } -@Deserializable("web") export class WebField extends URLField { }
\ No newline at end of file +@Deserializable("web") export class WebField extends URLField { } +@Deserializable("youtube") export class YoutubeField extends URLField { }
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index e9a8b0f0c..ee9142222 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,4 +45,5 @@ export namespace MessageStore { export const GetRefFields = new Message<string[]>("Get Ref Fields"); export const UpdateField = new Message<Diff>("Update Ref Field"); export const CreateField = new Message<Reference>("Create Ref Field"); + export const YoutubeApiKey = new Message<string>("Youtube Api Key"); } diff --git a/src/server/index.ts b/src/server/index.ts index fd66c90b4..2629519a7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -39,6 +39,11 @@ import { debug } from 'util'; import _ = require('lodash'); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); +//let fs = require('fs'); +let readline = require('readline'); +let { google } = require('googleapis'); +let OAuth2 = google.auth.OAuth2; + const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); @@ -310,6 +315,7 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); + Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiKey, GetYoutubeApiKey); Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); @@ -360,6 +366,17 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v Database.Instance.getDocuments(ids, callback, "newDocuments"); } +function GetYoutubeApiKey(callback: (result?: string) => void) { + // Load client secrets from a local file. + fs.readFile('client_secret.json', function processClientSecrets(err: any, content: any) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + callback(content); + }); +} + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", @@ -442,4 +459,5 @@ function CreateField(newValue: any) { } server.listen(serverPort); -console.log(`listening on port ${serverPort}`);
\ No newline at end of file +console.log(`listening on port ${serverPort}`); + |