From 1a3f820a38a12b13e947f58b4b7459ed84c4f70b Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 19 Oct 2019 11:51:34 -0400 Subject: fixing up audioboxes little by little. --- src/client/views/nodes/AudioBox.scss | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'src/client/views/nodes/AudioBox.scss') diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 972966204..04d98e10d 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,6 +1,16 @@ -.audiobox-cont{ - top:0; - max-height: 32px; - position: absolute; +.audiobox-container { width: 100%; + height: 100%; + position: inherit; + display:inline-block; + .audiobox-control, .audiobox-control-interactive { + top:0; + max-height: 32px; + position: absolute; + width: 100%; + pointer-events: none; + } + .audiobox-control-interactive { + pointer-events: all; + } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 1089aa451fd4571545e06b5674bc02ed1ecf4361 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 22 Oct 2019 11:32:43 -0400 Subject: added recording to audio documents and added to toolbar --- src/client/views/MainView.tsx | 3 +- src/client/views/nodes/AudioBox.scss | 14 ++- src/client/views/nodes/AudioBox.tsx | 137 +++++++++++++++++++-- src/client/views/pdf/Annotation.tsx | 1 + .../authentication/models/current_user_utils.ts | 1 + 5 files changed, 142 insertions(+), 14 deletions(-) (limited to 'src/client/views/nodes/AudioBox.scss') 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(AudioDocument) { + _reactionDisposer: IReactionDisposer | undefined; + @observable private _audioState = 0; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } _ref = React.createRef(); 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 ; + } + + render() { let interactive = this.active() ? "-interactive" : ""; - return ( -
- + return (!this.extensionDoc ? (null) : +
+ {!this.path ? + : + this.audio + }
); } 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 { 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(), { 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 })' }, -- cgit v1.2.3-70-g09d2 From 171e5a1716e4a2f981e647ae26f8ddd0e2332693 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 22 Oct 2019 19:15:29 -0400 Subject: playback highlights from audio --- src/client/documents/Documents.ts | 2 +- src/client/views/MainView.tsx | 2 +- src/client/views/nodes/AudioBox.scss | 12 ++++++-- src/client/views/nodes/AudioBox.tsx | 58 ++++++++++++++++++++++++++---------- 4 files changed, 54 insertions(+), 20 deletions(-) (limited to 'src/client/views/nodes/AudioBox.scss') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index fd2009dd6..316efe44c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -336,7 +336,7 @@ export namespace Docs { AudioBox.ActiveRecordings.map(d => { DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title); - }) + }); return Doc.assign(viewDoc, delegateProps); } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c14739aa2..78b8ac0b7 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -449,7 +449,7 @@ export class MainView extends React.Component { public static expandFlyout = action(() => { MainView.Instance._flyoutTranslate = true; MainView.Instance.flyoutWidth = 250; - }) + }); @computed get expandButton() { return !this._flyoutTranslate ? (
) : (null); diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 5c43c3c00..9bda5b2a7 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,14 +1,21 @@ .audiobox-container, .audiobox-container-interactive { width: 100%; height: 100%; + min-height: 32px; position: inherit; - display:inline-block; + display:flex; pointer-events: all; + .audiobox-handle { + width:20px; + height:100%; + display:inline-block; + background: gray; + } .audiobox-control, .audiobox-control-interactive { top:0; max-height: 32px; - position: absolute; width: 100%; + display:inline-block; pointer-events: none; } .audiobox-control-interactive { @@ -23,6 +30,5 @@ } .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 85607c6b8..55b472726 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -38,8 +38,9 @@ export class AudioBox extends DocExtendableComponent(); + _ele: HTMLAudioElement | null = null; _recorder: any; + _lastUpdate = 0; @observable private _audioState = 0; public static ActiveRecordings: Doc[] = []; @@ -64,21 +65,44 @@ export class AudioBox extends DocExtendableComponent { + const extensionDoc = this.extensionDoc; + const htmlEle = this._ele; + const start = extensionDoc && DateCast(extensionDoc.recordingStart); + if (htmlEle && !htmlEle.paused && start) { + setTimeout(this.updateHighlights, 30); + DocListCast(this.dataDoc.links).map(l => { + let la1 = l.anchor1 as Doc; + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + } + let date = DateCast(la1.creationDate); + let offset = (date!.date.getTime() - start.date.getTime()) / 1000; + if (offset > this._lastUpdate && offset < htmlEle.currentTime) { + Doc.linkFollowHighlight(la1); + } + }); + this._lastUpdate = htmlEle.currentTime; + } + } + playFrom = (sel: Doc) => { const extensionDoc = this.extensionDoc; let start = extensionDoc && DateCast(extensionDoc.recordingStart); - let seek = sel && DateCast(sel.creationDate) - if (this._ref.current && start && seek) { + let seek = sel && DateCast(sel.creationDate); + if (this._ele && start && seek) { if (sel) { 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(); + if (start && seek && delta > 0 && delta < this._ele.duration) { + this._ele.currentTime = delta; + this._ele.play(); + this._lastUpdate = delta; + setTimeout(this.updateHighlights, 0); } else { - this._ref.current.pause(); + this._ele.pause(); } } else { - this._ref.current.pause(); + this._ele.pause(); } } } @@ -132,19 +156,22 @@ export class AudioBox extends DocExtendableComponent { if (e.button === 0 && !e.ctrlKey) { - if (this._recorder) { - this.stopRecording(); - } else { - this.recordAudioAnnotation(); - } + this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); e.stopPropagation(); } } + playClick = (e: any) => setTimeout(this.updateHighlights, 30); + + setRef = (e: HTMLAudioElement | null) => { + e && e.addEventListener("play", this.playClick as any); + this._ele = e; + } + @computed get path() { let field = Cast(this.props.Document[this.props.fieldKey], AudioField); let path = (field instanceof AudioField) ? field.url.href : ""; @@ -153,7 +180,7 @@ export class AudioBox extends DocExtendableComponent + return ; @@ -163,6 +190,7 @@ export class AudioBox extends DocExtendableComponent +
{!this.path ? : - this.audio +
+
+ + {DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + let linkTime = NumCast(l.anchor2Timecode); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1Timecode); + } + return
+ DocumentManager.Instance.FollowLink(l, la2, document => this.props.addDocTab(document, undefined, "onRight"), false)} + style={{ left: `${linkTime / this._duration * 100}%` }} />; + })} + {this.audio} +
+
}
); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 044ec746b..ee9794564 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -15,6 +15,7 @@ import { RouteStore } from "../../RouteStore"; import { InkingControl } from "../../../client/views/InkingControl"; import { DragManager } from "../../../client/util/DragManager"; import { nullAudio } from "../../../new_fields/URLField"; +import { LinkManager } from "../../../client/util/LinkManager"; export class CurrentUserUtils { private static curr_id: string; -- cgit v1.2.3-70-g09d2 From 910e4d8a22a312d2ca0b8a970ff434604f7c6f91 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 23 Oct 2019 23:35:01 -0400 Subject: several fixes to audio documents to make linking work better and to customize the audio controls UI --- src/client/documents/Documents.ts | 3 +- src/client/util/DocumentManager.ts | 4 +- src/client/views/nodes/AudioBox.scss | 62 +++++++++++++++++++--- src/client/views/nodes/AudioBox.tsx | 94 +++++++++++++++++++++------------ src/client/views/nodes/DocuLinkBox.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 16 ++++-- src/new_fields/Doc.ts | 2 + 7 files changed, 134 insertions(+), 51 deletions(-) (limited to 'src/client/views/nodes/AudioBox.scss') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index cad417d33..f26594e04 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -171,7 +171,7 @@ export namespace Docs { }], [DocumentType.AUDIO, { layout: { view: AudioBox, dataField: data }, - options: { height: 32 } + options: { height: 35, backgroundColor: "lightGray", borderRounding: "20%" } }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: data }, @@ -691,6 +691,7 @@ export namespace DocUtils { } }); } + export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) { let sv = DocumentManager.Instance.getDocumentView(source.doc); if (sv && sv.props.ContainingCollectionDoc === target.doc) return; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 0f2a47dd0..346e88f40 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -101,10 +101,10 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { let pairs = DocumentManager.Instance.DocumentViews.filter(dv => - dv.isSelected() || Doc.IsBrushed(dv.props.Document) // draw links from DocumentViews that are selected or brushed OR + (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors - let init = dv2.isSelected() || Doc.IsBrushed(dv2.props.Document); // on a view that is selected or brushed + let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed return init && rest; }) ).reduce((pairs, dv) => { diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 14b90c570..3d6f6c4f9 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,10 +1,10 @@ .audiobox-container, .audiobox-container-interactive { width: 100%; height: 100%; - min-height: 32px; position: inherit; display:flex; pointer-events: all; + cursor:default; .audiobox-handle { width:20px; height:100%; @@ -35,18 +35,66 @@ width:100%; height:100%; position: relative; - display: grid; + display: flex; .audiobox-player { margin-top:auto; margin-bottom:auto; width:100%; + height: 80%; position: relative; - display: grid; - .audiobox-marker { - position:absolute; - width:10px; + display: flex; + .audiobox-playhead { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + } + .audiobox-timeline { + position:relative; height:100%; - background:green; + width:100%; + .audiobox-current { + width: 1px; + height:100%; + background-color: red; + position: absolute;; + } + .audiobox-linker { + position:absolute; + width:15px; + min-height:10px; + height:15px; + margin-left:-2.55px; + background:gray; + border-radius: 100%; + background-color: transparent; + box-shadow: black 2px 2px 1px; + .docuLinkBox-cont { + width:15px !important; + height:15px !important; + left: calc(100% - 15px) !important; + top: calc(100% - 15px) !important; + } + } + .audiobox-linker:hover { + transform:scale(1.5); + } + .audiobox-marker-container { + position:absolute; + width:10px; + height:100%; + background:gray; + border-radius: 5px; + box-shadow: black 2px 2px 1px; + .audiobox-marker { + position:relative; + height: calc(100% - 15px); + margin-top: 15px; + } + .audio-marker:hover { + border: orange 2px solid; + } + } } } } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 57360272e..3933c6257 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -7,17 +7,18 @@ import { AudioField, nullAudio } from "../../../new_fields/URLField"; import { DocExtendableComponent } from "../DocComponent"; import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { documentSchema } from "../../../new_fields/documentSchemas"; -import { Utils } from "../../../Utils"; +import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; import { DateField } from "../../../new_fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast, WidthSym } from "../../../new_fields/Doc"; import { ContextMenuProps } from "../ContextMenuItem"; import { ContextMenu } from "../ContextMenu"; import { Id } from "../../../new_fields/FieldSymbols"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DocumentManager } from "../../util/DocumentManager"; +import { DocumentView } from "./DocumentView"; interface Window { MediaRecorder: MediaRecorder; @@ -44,16 +45,14 @@ export class AudioBox extends DocExtendableComponent AudioBox._scrubTime = timeInMillisFrom1970); public static ActiveRecordings: Doc[] = []; componentDidMount() { - runInAction(() => this._duration = NumCast(this.dataDoc.duration, 1)); runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded"); this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { @@ -75,14 +74,12 @@ export class AudioBox extends DocExtendableComponent { + timecodeChanged = () => { const extensionDoc = this.extensionDoc; const htmlEle = this._ele; const start = extensionDoc && DateCast(extensionDoc.recordingStart); - if (htmlEle && !htmlEle.paused && start) { - htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = this._duration = htmlEle.duration); - setTimeout(this.updateHighlights, 30); - this.Document.currentTimecode = htmlEle.currentTime; + if (start && htmlEle) { + htmlEle && htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); DocListCast(this.dataDoc.links).map(l => { let la1 = l.anchor1 as Doc; let linkTime = NumCast(l.anchor2Timecode); @@ -90,25 +87,29 @@ export class AudioBox extends DocExtendableComponent this._lastUpdate && linkTime < htmlEle.currentTime) { + if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); } }); - this._lastUpdate = htmlEle.currentTime; + this.Document.currentTimecode = htmlEle.currentTime; } } + pause = action(() => { + this._ele!.pause(); + this._playing = false; + }) + playFrom = (seekTimeInSeconds: number) => { if (this._ele) { if (seekTimeInSeconds < 0) { - this._ele.pause(); + this.pause(); } else if (seekTimeInSeconds <= this._ele.duration) { this._ele.currentTime = seekTimeInSeconds; this._ele.play(); - this._lastUpdate = seekTimeInSeconds; - setTimeout(this.updateHighlights, 0); + runInAction(() => this._playing = true); } else { - this._ele.pause(); + this.pause(); } } } @@ -170,6 +171,7 @@ export class AudioBox extends DocExtendableComponent { this._recorder.stop(); + this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000; this._audioState = "recorded"; let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document); ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1)); @@ -182,12 +184,15 @@ export class AudioBox extends DocExtendableComponent setTimeout(this.updateHighlights, 30); onPlay = (e: any) => this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); + onStop = (e: any) => { + this.pause(); + this._ele!.currentTime = 0; + e.stopPropagation(); + } setRef = (e: HTMLAudioElement | null) => { - e && e.addEventListener("play", this.playClick as any); - e && e.addEventListener("timeupdate", () => this.props.Document.currentTimecode = e!.currentTime); + e && e.addEventListener("timeupdate", action(() => this.Document.currentTimecode = e!.currentTime)); this._ele = e; } @@ -216,21 +221,40 @@ export class AudioBox extends DocExtendableComponent :
- - {DocListCast(this.dataDoc.links).map((l, i) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2Timecode); - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1Timecode); - } - return
- DocumentManager.Instance.FollowLink(l, la2, document => this.props.addDocTab(document, undefined, "onRight"), false)} - style={{ left: `${linkTime / this._duration * 100}%` }} />; - })} - {this.audio} + +
+
e.stopPropagation()} + onPointerDown={e => { + if (e.button === 0 && !e.ctrlKey) { + let rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + this.pause(); + e.stopPropagation(); + } + }} > + {DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + let linkTime = NumCast(l.anchor2Timecode); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1Timecode); + } + return !linkTime ? (null) :
+
+ +
+
Doc.linkFollowHighlight(la1)} + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} + onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> +
; + })} +
+ {this.audio} +
} diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx index 7119b0db0..f2ab6fcd8 100644 --- a/src/client/views/nodes/DocuLinkBox.tsx +++ b/src/client/views/nodes/DocuLinkBox.tsx @@ -33,7 +33,7 @@ export class DocuLinkBox extends DocComponent(Doc document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); + (e.button === 0 && !e.ctrlKey) && e.stopPropagation(); } onPointerMove = action((e: PointerEvent) => { let cdiv = this._ref.current!.parentElement; @@ -43,7 +43,7 @@ export class DocuLinkBox extends DocComponent(Doc let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy)); if (separation > 100) { - DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], this.props.ContainingCollectionDoc as Doc, this.props.Document); // Containging collection is the document, not a collection... hack. + DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, this.props.Document); // Containging collection is the document, not a collection... hack. document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } else if (dragdist > separation) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c82fd0f0f..ed93aa83e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,7 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import { link } from 'fs'; library.add(fa.faEdit); library.add(fa.faTrash); @@ -585,8 +586,16 @@ export class DocumentView extends DocComponent(Docu layoutKey={this.props.layoutKey || "layout"} DataDoc={this.props.DataDoc} />); } - linkEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; - linkEndpointDoc = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? Cast(linkDoc.anchor1, Doc) as Doc : Cast(linkDoc.anchor2, Doc) as Doc; + linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); + + // used to decide whether a link document should be created or not. + // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here. + // would be good to generalize this some way. + isNonTemporalLink = (linkDoc: Doc) => { + let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc; + let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode; + return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true; + } render() { if (!this.props.Document) return (null); @@ -658,8 +667,7 @@ export class DocumentView extends DocComponent(Docu onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > - {this.Document.links && DocListCast(this.Document.links).map((d, i) => - //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) : + {this.Document.links && DocListCast(this.Document.links).filter(this.isNonTemporalLink).map((d, i) =>
)} diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 08cb66d5f..6aad4a6be 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -669,6 +669,8 @@ export namespace Doc { return doc; } + export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; } + export function linkFollowUnhighlight() { Doc.UnhighlightAll(); document.removeEventListener("pointerdown", linkFollowUnhighlight); -- cgit v1.2.3-70-g09d2 From 31166219e473e105b8fd9d49627fd1df58822c55 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Thu, 24 Oct 2019 00:56:00 -0400 Subject: fixed audio box shrinking layout. fixed pdf z-index issue in stacking panels. --- src/client/views/nodes/AudioBox.scss | 31 ++++++++++++++++++++++++++++--- src/client/views/nodes/AudioBox.tsx | 16 ++++++++++------ src/client/views/nodes/PDFBox.scss | 1 - 3 files changed, 38 insertions(+), 10 deletions(-) (limited to 'src/client/views/nodes/AudioBox.scss') diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 3d6f6c4f9..ccbf0d75f 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -36,6 +36,8 @@ height:100%; position: relative; display: flex; + padding-left: 2px; + border: gray solid 3px; .audiobox-player { margin-top:auto; margin-bottom:auto; @@ -59,7 +61,7 @@ background-color: red; position: absolute;; } - .audiobox-linker { + .audiobox-linker, .audiobox-linker-mini { position:absolute; width:15px; min-height:10px; @@ -76,10 +78,24 @@ top: calc(100% - 15px) !important; } } - .audiobox-linker:hover { + .audiobox-linker-mini { + width:8px; + min-height:8px; + height:8px; + box-shadow: black 1px 1px 1px; + margin-left: -1; + margin-top: -2; + .docuLinkBox-cont { + width:8px !important; + height:8px !important; + left: calc(100% - 8px) !important; + top: calc(100% - 8px) !important; + } + } + .audiobox-linker:hover, .audiobox-linker-mini:hover { transform:scale(1.5); } - .audiobox-marker-container { + .audiobox-marker-container, .audiobox-marker-minicontainer { position:absolute; width:10px; height:100%; @@ -95,6 +111,15 @@ border: orange 2px solid; } } + .audiobox-marker-minicontainer { + width:5px; + border-radius: 1px; + .audiobox-marker { + position:relative; + height: calc(100% - 8px); + margin-top: 8px; + } + } } } } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 3933c6257..cc1c63d44 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -184,7 +184,10 @@ export class AudioBox extends DocExtendableComponent this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); + onPlay = (e: any) => { + this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); + e.stopPropagation(); + } onStop = (e: any) => { this.pause(); this._ele!.currentTime = 0; @@ -213,7 +216,8 @@ export class AudioBox extends DocExtendableComponent +
{!this.path ?