aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBob Zeleznik <zzzman@gmail.com>2019-10-23 23:35:01 -0400
committerBob Zeleznik <zzzman@gmail.com>2019-10-23 23:35:01 -0400
commit910e4d8a22a312d2ca0b8a970ff434604f7c6f91 (patch)
treea74313b0464300c79520b7679cfb3066e449c4c7 /src
parentc861f31a93ad246b9200b328a63df15da795f050 (diff)
several fixes to audio documents to make linking work better and to customize the audio controls UI
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/views/nodes/AudioBox.scss62
-rw-r--r--src/client/views/nodes/AudioBox.tsx94
-rw-r--r--src/client/views/nodes/DocuLinkBox.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx16
-rw-r--r--src/new_fields/Doc.ts2
7 files changed, 134 insertions, 51 deletions
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<FieldViewProps, AudioDocume
_ele: HTMLAudioElement | null = null;
_recorder: any;
_recordStart = 0;
- @observable _duration = 1;
- _lastUpdate = 0;
- @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
@observable private static _scrubTime = 0;
+ @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
+ @observable private _playing = false;
public static SetScrubTime = action((timeInMillisFrom1970: number) => 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<FieldViewProps, AudioDocume
});
}
- updateHighlights = () => {
+ 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<FieldViewProps, AudioDocume
la1 = l.anchor2 as Doc;
linkTime = NumCast(l.anchor1Timecode);
}
- if (linkTime > 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<FieldViewProps, AudioDocume
stopRecording = action(() => {
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<FieldViewProps, AudioDocume
}
}
- playClick = (e: any) => 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<FieldViewProps, AudioDocume
</button> :
<div className="audiobox-controls">
<div className="audiobox-player" onClick={this.onPlay}>
- <FontAwesomeIcon icon="play" size="sm" ></FontAwesomeIcon>
- {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 <div key={i} className="audiobox-marker" onPointerDown={() =>
- DocumentManager.Instance.FollowLink(l, la2, document => this.props.addDocTab(document, undefined, "onRight"), false)}
- style={{ left: `${linkTime / this._duration * 100}%` }} />;
- })}
- {this.audio}
+ <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 25 ? "1x" : "2x"} />
+ <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 25 ? "1x" : "2x"} /></div>
+ <div className="audiobox-timeline" onClick={e => 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) : <div className="audiobox-marker-container" style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+ <div className="audioBox-linker" key={"linker" + i}>
+ <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+ parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+ backgroundColor={returnTransparent} />
+ </div>
+ <div key={i} className="audiobox-marker" onPointerEnter={() => 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(); } }} />
+ </div>;
+ })}
+ <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+ {this.audio}
+ </div>
</div>
</div>
}
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<FieldViewProps, DocLinkSchema>(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<FieldViewProps, DocLinkSchema>(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<DocumentViewProps, Document>(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<DocumentViewProps, Document>(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) =>
<div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
<DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
</div>)}
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);