diff options
-rw-r--r-- | src/client/views/MainView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.scss | 14 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 137 | ||||
-rw-r--r-- | src/client/views/pdf/Annotation.tsx | 1 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 1 |
5 files changed, 142 insertions, 14 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 26e70c5c7..8bad70093 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, - faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter + faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; @@ -108,6 +108,7 @@ export class MainView extends React.Component { library.add(faEraser); library.add(faFileAudio); library.add(faPenNib); + library.add(faMicrophone); library.add(faFilm); library.add(faMusic); library.add(faTree); diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 04d98e10d..5c43c3c00 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,8 +1,9 @@ -.audiobox-container { +.audiobox-container, .audiobox-container-interactive { width: 100%; height: 100%; position: inherit; display:inline-block; + pointer-events: all; .audiobox-control, .audiobox-control-interactive { top:0; max-height: 32px; @@ -13,4 +14,15 @@ .audiobox-control-interactive { pointer-events: all; } + .audiobox-record { + pointer-events: all; + width:100%; + height:100%; + position: absolute; + pointer-events: none; + } + .audiobox-record-interactive { + pointer-events: all; + + } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 4c1c3a465..ee4e06a2e 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -2,37 +2,150 @@ import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; import { observer } from "mobx-react"; import "./AudioBox.scss"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, DateCast } from "../../../new_fields/Types"; import { AudioField } from "../../../new_fields/URLField"; import { DocExtendableComponent } from "../DocComponent"; -import { makeInterface } from "../../../new_fields/Schema"; +import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Utils } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; +import { runInAction, observable, reaction, IReactionDisposer, computed } from "mobx"; +import { DateField } from "../../../new_fields/DateField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Doc } from "../../../new_fields/Doc"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { ContextMenu } from "../ContextMenu"; -type AudioDocument = makeInterface<[typeof documentSchema]>; -const AudioDocument = makeInterface(documentSchema); +interface Window { + MediaRecorder: MediaRecorder; +} + +declare class MediaRecorder { + // whatever MediaRecorder has + constructor(e: any); +} +export const audioSchema = createSchema({ + playOnSelect: "boolean" +}); + +type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>; +const AudioDocument = makeInterface(documentSchema, audioSchema); const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3")); @observer export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) { + _reactionDisposer: IReactionDisposer | undefined; + @observable private _audioState = 0; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } _ref = React.createRef<HTMLAudioElement>(); componentDidMount() { - if (this._ref.current) this._ref.current.currentTime = 1; + runInAction(() => this._audioState = this.path ? 2 : 0); + this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), + selected => { + let sel = selected.length ? selected[0].props.Document : undefined; + const extensionDoc = this.extensionDoc; + let start = extensionDoc && DateCast(extensionDoc.recordingStart); + let seek = sel && DateCast(sel.creationDate) + if (this._ref.current && start && seek) { + if (this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document)) { + let delta = (seek.date.getTime() - start.date.getTime()) / 1000; + if (start && seek && delta > 0 && delta < this._ref.current.duration) { + this._ref.current.currentTime = delta; + this._ref.current.play(); + } else { + this._ref.current.pause(); + } + } else { + this._ref.current.pause(); + } + } + }); } - render() { + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + } + + _recorder: any; + recordAudioAnnotation = () => { + let gumStream: any; + let self = this; + const extensionDoc = this.extensionDoc; + extensionDoc && navigator.mediaDevices.getUserMedia({ + audio: true + }).then(function (stream) { + gumStream = stream; + self._recorder = new MediaRecorder(stream); + extensionDoc.recordingStart = new DateField(new Date()); + self._recorder.ondataavailable = async function (e: any) { + const formData = new FormData(); + formData.append("file", e.data); + const res = await fetch(Utils.prepend(RouteStore.upload), { + method: 'POST', + body: formData + }); + const files = await res.json(); + const url = Utils.prepend(files[0].path); + // upload to server with known URL + self.props.Document[self.props.fieldKey] = new AudioField(url); + }; + runInAction(() => self._audioState = 1); + self._recorder.start(); + setTimeout(() => { + self._recorder.stop(); + runInAction(() => self._audioState = 2); + gumStream.getAudioTracks()[0].stop(); + }, 60 * 60 * 1000); // stop after an hour? + }); + } + + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" }); + + ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" }); + } + + recordClick = (e: React.MouseEvent) => { + if (e.button === 0) { + if (this._recorder) { + this._recorder.stop(); + runInAction(() => this._audioState = 2); + } else { + this.recordAudioAnnotation(); + } + e.stopPropagation(); + } + } + + @computed get path() { let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField); let path = field.url.href; + return path === "https://actions.google.com/sounds/v1/alarms/beep_short.ogg" ? "" : path; + } + + @computed get audio() { + let interactive = this.active() ? "-interactive" : ""; + return <audio controls ref={this._ref} className={`audiobox-control${interactive}`}> + <source src={this.path} type="audio/mpeg" /> + Not supported. + </audio>; + } + + render() { let interactive = this.active() ? "-interactive" : ""; - return ( - <div className="audiobox-container"> - <audio controls ref={this._ref} className={`audiobox-control${interactive}`}> - <source src={path} type="audio/mpeg" /> - Not supported. - </audio> + return (!this.extensionDoc ? (null) : + <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}> + {!this.path ? + <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}> + {this._audioState === 1 ? "STOP" : "RECORD"} + </button> : + this.audio + } </div> ); } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index e0a3b9171..2d8f47666 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -102,6 +102,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"), false, false, undefined); + e.stopPropagation(); } } } diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 532f527cc..cf4ae4e6c 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -50,6 +50,7 @@ export class CurrentUserUtils { { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] }, { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' }, + { title: "record", icon: "microphone", ignoreClick: true, drag: 'Docs.Create.AudioDocument("https://actions.google.com/sounds/v1/alarms/beep_short.ogg", { width: 200, title: "ready to record audio" })' }, { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' }, { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, |