aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/nodes/AudioBox.scss14
-rw-r--r--src/client/views/nodes/AudioBox.tsx137
-rw-r--r--src/client/views/pdf/Annotation.tsx1
-rw-r--r--src/server/authentication/models/current_user_utils.ts1
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 })' },