diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 22 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 81 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkMenuItem.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 85 |
5 files changed, 182 insertions, 29 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f7a33466e..245dd319d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -344,7 +344,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document)); let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; - let linkedFwdContextDocs = [first.length ? await (first[0].context) as Doc : undefined, undefined]; + // @TODO: shouldn't always follow target context + let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined]; @@ -454,8 +455,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu else { // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); + let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); de.data.droppedDocuments.push(destDoc); + de.data.linkDocument = linkDoc; } } } @@ -554,6 +556,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); + cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 82c2cef26..066cc40e2 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -303,6 +303,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let ctrlKey = e.ctrlKey; if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { let href = (e.target as any).href; + let location: string; + if ((e.target as any).attributes.location) { + location = (e.target as any).attributes.location.value; + } for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) { href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; } @@ -310,8 +314,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (href.indexOf(DocServer.prepend("/doc/")) === 0) { this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { - DocServer.GetRefField(this._linkClicked).then(f => { - (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab")); + DocServer.GetRefField(this._linkClicked).then(async linkDoc => { + if (linkDoc instanceof Doc) { + let proto = Doc.GetProto(linkDoc); + let targetContext = await Cast(proto.targetContext, Doc); + let jumpToDoc = await Cast(linkDoc.anchor2, Doc); + if (jumpToDoc) { + if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { + + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); + return; + } + } + if (targetContext) { + DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } + } }); e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 1df955f1f..a3e098fd8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faImage } from '@fortawesome/free-solid-svg-icons'; -import { action, observable, computed } from 'mobx'; +import { faImage, faFileAudio } from '@fortawesome/free-solid-svg-icons'; +import { action, observable, computed, runInAction } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app @@ -8,7 +8,7 @@ import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; -import { ImageField } from '../../../new_fields/URLField'; +import { ImageField, AudioField } from '../../../new_fields/URLField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -22,11 +22,16 @@ import "./ImageBox.scss"; import React = require("react"); import { RouteStore } from '../../../server/RouteStore'; import { Docs } from '../../documents/Documents'; +import { DocServer } from '../../DocServer'; +import { Font } from '@react-pdf/renderer'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); +const { Howl, Howler } = require('howler'); library.add(faImage); +library.add(faFileAudio); export const pageSchema = createSchema({ @@ -157,8 +162,15 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD }).then(function (stream) { gumStream = stream; recorder = new MediaRecorder(stream); - recorder.ondataavailable = function (e: any) { - var url = URL.createObjectURL(e.data); + recorder.ondataavailable = async function (e: any) { + const formData = new FormData(); + formData.append("file", e.data); + const res = await fetch(DocServer.prepend(RouteStore.upload), { + method: 'POST', + body: formData + }); + const files = await res.json(); + const url = DocServer.prepend(files[0]); // upload to server with known URL let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); audioDoc.embed = true; @@ -169,12 +181,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD audioAnnos.push(audioDoc); } }; + runInAction(() => self._audioState = 2); recorder.start(); setTimeout(() => { recorder.stop(); - + runInAction(() => self._audioState = 0); gumStream.getAudioTracks()[0].stop(); - }, 1000); + }, 5000); }); } @@ -264,6 +277,46 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD }); } + @observable _audioState = 0; + + @action + onPointerEnter = () => { + let self = this; + let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations); + if (audioAnnos.length && this._audioState === 0) { + audioAnnos.map(anno => anno.data instanceof AudioField && new Howl({ + src: [anno.data.url.href], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => self._audioState = 0); + } + })); + this._audioState = 1; + } + // else { + // if (this._audioState === 0) { + // this._audioState = 1; + // new Howl({ + // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"], + // autoplay: true, + // loop: false, + // volume: 0.5, + // onend: function () { + // runInAction(() => self._audioState = 0); + // } + // }); + // } + // } + } + + @action + audioDown = () => { + this.recordAudioAnnotation(); + } + + render() { // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; @@ -274,7 +327,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx let nativeWidth = FieldValue(this.Document.nativeWidth, pw); let nativeHeight = FieldValue(this.Document.nativeHeight, 0); - let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"]; + let paths: string[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"]; // this._curSuffix = ""; // if (w > 20) { Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); @@ -297,6 +350,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD return ( <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} + onPointerEnter={this.onPointerEnter} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <img id={id} @@ -308,6 +362,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD ref={this._imgRef} onError={this.onError} /> {paths.length > 1 ? this.dots(paths) : (null)} + <div onPointerDown={this.audioDown} style={{ + display: DocListCast(this.extensionDoc.audioAnnotations).length ? "inline" : "inline", + width: nativeWidth * 0.1, + height: nativeWidth * 0.1, + position: "absolute", + top: 0, + left: 0, + }}> + <FontAwesomeIcon + style={{ width: "100%", height: "100%", color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> + </div> {/* {this.lightbox(paths)} */} </div>); } diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 6a18a4e7b..a0c37a719 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -32,16 +32,28 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @undoBatch onFollowLink = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); + e.persist(); let jumpToDoc = this.props.destinationDoc; let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); if (pdfDoc) { jumpToDoc = pdfDoc; } + let proto = Doc.GetProto(this.props.linkDoc); + let targetContext = await Cast(proto.targetContext, Doc); + let sourceContext = await Cast(proto.sourceContext, Doc); + let self = this; if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - let self = this; DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page))); + } + else if (!((this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) || (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext))) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(document, undefined)); } else { - CollectionDockingView.Instance.AddRightSplit(jumpToDoc, undefined); + if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(targetContext!, undefined)); + } + else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => CollectionDockingView.Instance.AddRightSplit(sourceContext!, undefined)); + } } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 972e6875d..8b8e500c4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,19 +1,24 @@ import React = require("react"); -import { action, IReactionDisposer, observable, reaction, trace, computed, runInAction, untracked } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx"; import { observer } from "mobx-react"; +import * as rp from 'request-promise'; +import { InkTool } from "../../../new_fields/InkField"; import { makeInterface } from "../../../new_fields/Schema"; 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"; import { DocComponent } from "../DocComponent"; +import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; -import { InkTool } from "../../../new_fields/InkField"; -import { DocumentDecorations } from "../DocumentDecorations"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -52,21 +57,17 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD @action public Play = (update: boolean = true) => { this.Playing = true; update && this.player && this.player.play(); - console.log("PLAYING = " + update); update && this._youtubePlayer && this._youtubePlayer.playVideo(); !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500)); this.updateTimecode(); } @action public Seek(time: number) { - console.log("Seeking " + time); - //if (this._youtubePlayer && this._youtubePlayer.getPlayerState() === 5) return; this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true); } @action public Pause = (update: boolean = true) => { this.Playing = false; - console.log("PAUSING = " + update); update && this.player && this.player.pause(); update && this._youtubePlayer && this._youtubePlayer.pauseVideo(); this._playTimer && clearInterval(this._playTimer); @@ -93,7 +94,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let youtubeaspect = 400 / 315; var nativeWidth = FieldValue(this.Document.nativeWidth, 0); var nativeHeight = FieldValue(this.Document.nativeHeight, 0); - if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { + if (!nativeWidth || !nativeHeight) { if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect; this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect; @@ -119,13 +120,62 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } } + public static async convertDataUri(imageUri: string, returnedFilename: string) { + try { + let posting = DocServer.prepend(RouteStore.dataUriToImage); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename + }, + json: true, + }); + return returnedUri; + + } catch (e) { + console.log(e); + } + } specificContextMenu = (e: React.MouseEvent): void => { let field = Cast(this.Document[this.props.fieldKey], VideoField); if (field) { + let url = field.url.href; 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" }); - subitems.push({ description: "GOTO 3", event: action(() => this.Seek(3)), icon: "expand-arrows-alt" }); - subitems.push({ description: "PLAY", event: action(() => this.Play()), icon: "expand-arrows-alt" }); + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + subitems.push({ + description: "Take Snapshot", event: async () => { + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + if (returnedFilename) { + let url = DocServer.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" + }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems }); } } @@ -136,7 +186,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls} - onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()}> + onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> Not supported. </video>; @@ -155,13 +205,18 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD else this._youtubeContentCreated = false; let iframe = e.target; + let started = true; let onYoutubePlayerStateChange = (event: any) => runInAction(() => { - console.log("Event " + event.data); + if (started && event.data === YT.PlayerState.PLAYING) { + started = false; + this._youtubePlayer.unMute(); + this.Pause(); + return; + } if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false); if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false); }); let onYoutubePlayerReady = (event: any) => { - console.log("READY!"); this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0)); @@ -185,8 +240,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); let start = untracked(() => Math.round(NumCast(this.props.Document.curPage))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width="640" height="390" - src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)} + src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} ></iframe>; } |
