diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2019-10-23 23:35:01 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2019-10-23 23:35:01 -0400 |
commit | 910e4d8a22a312d2ca0b8a970ff434604f7c6f91 (patch) | |
tree | a74313b0464300c79520b7679cfb3066e449c4c7 /src | |
parent | c861f31a93ad246b9200b328a63df15da795f050 (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.ts | 3 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 4 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.scss | 62 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 94 | ||||
-rw-r--r-- | src/client/views/nodes/DocuLinkBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 16 | ||||
-rw-r--r-- | src/new_fields/Doc.ts | 2 |
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); |