diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/AudioBox.scss | 224 | ||||
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 75 | ||||
| -rw-r--r-- | src/client/views/nodes/FilterBox.scss | 11 | ||||
| -rw-r--r-- | src/client/views/nodes/FilterBox.tsx | 44 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/nodes/PresBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.scss | 132 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 106 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 74 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/nodes_rts.ts | 38 |
12 files changed, 250 insertions, 495 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 4a3bbf8d8..fc881ca25 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -7,11 +7,6 @@ position: relative; cursor: default; - .audiobox-inner { - width:100%; - height: 100%; - } - .audiobox-buttons { display: flex; width: 100%; @@ -26,20 +21,13 @@ display: inherit; background: dimgray; left: 0px; - } - - .audiobox-dictation:hover { - color: white; - cursor: pointer; + &:hover { + color: white; + cursor: pointer; + } } } - .audiobox-handle { - width: 20px; - height: 100%; - display: inline-block; - } - .audiobox-control, .audiobox-control-interactive { top: 0; @@ -53,21 +41,16 @@ pointer-events: all; } + .audiobox-record-interactive, .audiobox-record { pointer-events: all; width: 100%; height: 100%; position: relative; - pointer-events: none; } - .audiobox-record-interactive { - pointer-events: all; - width: 100%; - height: 100%; - position: relative; - - + .audiobox-record { + pointer-events: none; } .recording { @@ -95,10 +78,9 @@ margin-bottom: auto; width: 25px; padding: 5px; - } - - .buttons:hover { - background-color: crimson; + &:hover{ + background-color: crimson; + } } } @@ -138,13 +120,10 @@ border-radius: 50%; background-color: black; color: white; - } - - .audiobox-playhead:hover { - // background-color: black; - // border-radius: 5px; - background-color: grey; - color: lightgrey; + &:hover { + background-color: grey; + color: lightgrey; + } } .audiobox-dictation { @@ -166,29 +145,6 @@ z-index: 1000; overflow: hidden; - .audiobox-container { - position: absolute; - width: 10px; - top: 2.5%; - height: 0px; - background: lightblue; - border-radius: 5px; - // box-shadow: black 2px 2px 1px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } - - .audiobox-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - } - .waveform { position: relative; width: 100%; @@ -206,161 +162,21 @@ width: 100% !important; } } - - .audiobox-linker, - .audiobox-linker-mini { - position: absolute; - width: 15px; - min-height: 10px; - height: 15px; - margin-left: -2.55px; - background: gray; - border-radius: 100%; - opacity: 0.9; - box-shadow: black 2px 2px 1px; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker-mini { - width: 8px; - min-height: 8px; - height: 8px; - box-shadow: black 1px 1px 1px; - margin-left: -1; - margin-top: -2; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker:hover, - .audiobox-linker-mini:hover { - opacity: 1; - } - - .audiobox-marker-container, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 10px; - top: 2.5%; - background: gray; - border-radius: 50%; - box-shadow: black 2px 2px 1px; - overflow: visible; - cursor: pointer; - - .audiobox-marker { - position: relative; - height: 100%; - // height: calc(100% - 15px); - width: 100%; - //margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - } - - .audiobox-marker-timeline, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 90%; - top: 2.5%; - border-radius: 5px; - - .left-resizer { - background: dimgrey; - } - .resizer { - background: dimgrey; - } - - .audiobox-marker { - position: relative; - height: calc(100% - 15px); - margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - - .resizer { - position: absolute; - top: 0; - right: 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - - .click { - position: relative; - height: 100%; - width: 100%; - z-index: 100; - } - - .left-resizer { - position: absolute; - left: 0; - top : 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - } - - .audiobox-marker-timeline:hover, - .audiobox-marker-minicontainer:hover { - opacity: 0.8; - } - - .audiobox-marker-minicontainer { - width: 5px; - border-radius: 1px; - - .audiobox-marker { - position: relative; - height: 100%; - margin-top: 8px; - } - } } - .current-time { + .audioBox-total-time, + .audioBox-current-time { position: absolute; font-size: 8; top: 100%; - left: 30px; color: white; } + .audioBox-current-time { + left: 30px; + } - .total-time { - position: absolute; - top: 100%; - font-size: 8; + .audioBox-total-time { right: 2px; - color: white; } } } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c8bec74fb..692eaae66 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -26,13 +26,11 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; declare class MediaRecorder { - // whatever MediaRecorder has - constructor(e: any); + constructor(e: any); // whatever MediaRecorder has } -export const audioSchema = createSchema({ playOnSelect: "boolean" }); -type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>; -const AudioDocument = makeInterface(documentSchema, audioSchema); +type AudioDocument = makeInterface<[typeof documentSchema]>; +const AudioDocument = makeInterface(documentSchema); @observer export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) { @@ -45,7 +43,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD _disposers: { [name: string]: IReactionDisposer } = {}; _ele: HTMLAudioElement | null = null; - _audioRef = React.createRef<HTMLDivElement>(); _stackedTimeline = React.createRef<CollectionStackedTimeline>(); _recorder: any; _recordStart = 0; @@ -83,7 +80,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); + const linkTime = this._stackedTimeline.current?.anchorStart(la2) || this._stackedTimeline.current?.anchorStart(la1) || 0; if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -92,7 +89,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } getAnchor = () => { - return this._stackedTimeline.current?.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "audioStart", "audioEnd", this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc; } componentWillUnmount() { @@ -107,26 +104,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this.audioState = this.path ? "paused" : undefined; - this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); + //this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); this._disposers.triggerAudio = reaction( () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, start => start !== undefined && setTimeout(() => { - this._audioRef.current && this.playFrom(start); + this.playFrom(start); setTimeout(() => { this.Document._currentTimecode = start; this.Document._triggerAudio = undefined; }, 10); - }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play + }), // wait for mainCont and try again to play { fireImmediately: true } ); this._disposers.audioStop = reaction( () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined, audioStop => audioStop !== undefined && setTimeout(() => { - this._audioRef.current && this.Pause(); + this.Pause(); setTimeout(() => this.Document._audioStop = undefined, 10); - }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play + }), // wait for mainCont and try again to play { fireImmediately: true } ); } @@ -329,15 +326,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD playing = () => this.audioState === "playing"; playLink = (link: Doc) => { + const stack = this._stackedTimeline.current; if (link.annotationOn === this.rootDoc) { - if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link)); - else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0); + if (this.layoutDoc.playOnSelect) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link)); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0); } else { this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { const { la1, la2 } = this.getLinkData(l); - const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); - const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); + const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2); + const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2); if (startTime !== undefined) { if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; @@ -346,40 +344,35 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } } + isActiveChild = () => this._isChildActive; + timelineWhenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(runInAction(() => this._isChildActive = isActive)); + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-AudioBox.playheadWidth, -(100 - this.heightPercent) / 200 * this.props.PanelHeight()); + setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time; + timelineHeight = () => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) + timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth; @computed get renderTimeline() { - return <CollectionStackedTimeline ref={this._stackedTimeline} - Document={this.props.Document} + return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} renderDepth={this.props.renderDepth + 1} - parentActive={this.props.parentActive} + startTag={"audioStart"} + endTag={"audioEnd"} focus={emptyFunction} - styleProvider={this.props.styleProvider} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - rootSelected={this.props.rootSelected} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} bringToFront={emptyFunction} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} CollectionView={undefined} duration={this.duration} playFrom={this.playFrom} - setTime={(time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time} + setTime={this.setAnchorTime} playing={this.playing} - select={this.props.select} - isSelected={this.props.isSelected} - whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + whenActiveChanged={this.timelineWhenActiveChanged} removeDocument={this.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(0, -(100 - this.heightPercent) / 200 * this.props.PanelHeight())} - isChildActive={() => this._isChildActive} + ScreenToLocalTransform={this.timelineScreenToLocal} + isChildActive={this.isActiveChild} Play={this.Play} Pause={this.Pause} active={this.active} playLink={this.playLink} - PanelWidth={this.props.PanelWidth} - PanelHeight={() => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100}// panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) + PanelWidth={this.timelineWidth} + PanelHeight={this.timelineHeight} />; } @@ -413,17 +406,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-dictation" /> <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} > <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> - <div className="audiobox-timeline" style={{ height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> + <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> <div className="waveform"> {this.waveform} </div> + {this.renderTimeline} </div> - {this.renderTimeline} {this.audio} - <div className="current-time"> + <div className="audioBox-current-time"> {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} </div> - <div className="total-time"> + <div className="audioBox-total-time"> {formatTime(Math.round(this.duration))} </div> </div> diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss index fb1783ad4..b1b3c0f25 100644 --- a/src/client/views/nodes/FilterBox.scss +++ b/src/client/views/nodes/FilterBox.scss @@ -27,11 +27,11 @@ } } -.filterBox-bottom { - // position: fixed; - // bottom: 0; - // width: 100%; -} +// .filterBox-bottom { + // // position: fixed; + // // bottom: 0; + // // width: 100%; + // } .filterBox-select { width: 90%; @@ -50,6 +50,7 @@ margin: 8px; display: flex; font-size: 11px; + cursor: pointer; &:hover { background-color: white; diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index a1b55f490..dfa81d3bb 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -36,12 +36,16 @@ const FilterBoxDocument = makeInterface(documentSchema); @observer export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDocument>(FilterBoxDocument) { + constructor(props: Readonly<FieldViewProps>) { + super(props); + } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } public _filterBoolean = "AND"; public _filterScope = "Current Dashboard"; public _filterSelected = false; public _filterMatch = "matched"; + private myFiltersRef = React.createRef<HTMLDivElement>(); @computed get allDocs() { const allDocs = new Set<Doc>(); @@ -231,6 +235,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc console.log(this._filterMatch); } + @computed get yPos() { + return this.myFiltersRef.current?.getBoundingClientRect(); + } + @action changeSelected = (e: any) => { @@ -244,6 +252,12 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc console.log(this._filterSelected); } + saveFilter = () => { + Doc.AddDocToList(Doc.UserDoc(), "savedFilters", this.props.Document); + console.log("saved filter"); + console.log(Doc.UserDoc().savedFilters); + } + FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { switch (property.split(":")[0]) { case StyleProp.Decorations: @@ -267,6 +281,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc } + suppressChildClick = () => ScriptField.MakeScript("")!; render() { const facetCollection = this.props.Document; // const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}> @@ -280,6 +295,17 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc // const attributes = this.activeAttributes; // const options = this._allFacets.filter(facet => !attributes.some(attribute => attribute.title === facet)).map(facet => ({ value: facet, label: facet })); + // const options = this._allFacets.map(facet => ({ value: facet, label: facet })); + // console.log(this.props.Document); + // console.log(Doc.UserDoc().currentFilter); + console.log(this.yPos); + console.log(this.myFiltersRef.current?.getBoundingClientRect()); + + const flyout = <> + <div className="nothing for now" onWheel={e => e.stopPropagation()}> + testing flyout + </div> + </>; const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> @@ -320,9 +346,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc <CollectionTreeView Document={facetCollection} DataDoc={Doc.GetProto(facetCollection)} - fieldKey={`${this.props.fieldKey}`} + fieldKey={this.props.fieldKey} CollectionView={undefined} cantBrush={true} + onChildClick={this.suppressChildClick} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} @@ -383,18 +410,27 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc <div style={{ display: "flex" }}> <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark"> + <div className="filterBox-saveBookmark" + onPointerDown={this.saveFilter} + > <FontAwesomeIcon className="filterBox-saveBookmark-icon" icon={"save"} size={"sm"} /> <div>SAVE</div> </div> </div> <div className="filterBox-saveWrapper"> - <div className="filterBox-saveBookmark"> + <div className="filterBox-saveBookmark" ref={this.myFiltersRef}> <FontAwesomeIcon className="filterBox-saveBookmark-icon" icon={"bookmark"} size={"sm"} /> - <div>MY FILTERS</div> + <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={flyout}> + <div>MY FILTERS</div> + </Flyout> </div> </div> </div> + <div + style={{ width: 200, height: 200, backgroundColor: "black", color: "white" }} + > + floot floot + </div> </div> </div>; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 649fe8f40..92d6e2612 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,7 +1,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Dictionary } from 'typescript-collections'; +import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -11,7 +12,7 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnZero, Utils, OmitKeys } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnOne, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; @@ -22,15 +23,12 @@ import { ContextMenu } from "../../views/ContextMenu"; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; +import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { StyleProp } from '../StyleProvider'; import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { StyleProp } from '../StyleProvider'; -import { AnchorMenu } from '../pdf/AnchorMenu'; -import { Dictionary } from 'typescript-collections'; -import { MarqueeAnnotator } from '../MarqueeAnnotator'; -import { Annotation } from '../pdf/Annotation'; const path = require('path'); const { Howl } = require('howler'); @@ -423,7 +421,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } @action marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; } @action finishMarquee = () => { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 8d0283a12..b6feace12 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -715,9 +715,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (audio) { audio.mediaStart = "manual"; audio.mediaStop = "manual"; - audio.presStartTime = NumCast(doc.anchorStartTime); - audio.presEndTime = NumCast(doc.anchorEndTime); - audio.presDuration = NumCast(doc.anchorEndTime) - NumCast(doc.anchorStartTime); + audio.presStartTime = NumCast(doc.audioStart, NumCast(doc.videoStart)); + audio.presEndTime = NumCast(doc.audioEnd, NumCast(doc.videoEnd)); + audio.presDuration = audio.presStartTime - audio.presEndTime; TabDocView.PinDoc(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index ac4d64f12..b9123587b 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -10,138 +10,8 @@ .inkingCanvas-paths-markers { opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } - - .audiobox-timeline { - position: absolute; - width: 100%; + .collectionStackedTimeline { background: beige; - border: gray solid 1px; - border-radius: 3px; - z-index: 1000; - overflow: hidden; - bottom: 0; - - .audiobox-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - } - - .audiobox-container { - position: absolute; - width: 10px; - top: 2.5%; - height: 0px; - background: lightblue; - border-radius: 5px; - // box-shadow: black 2px 2px 1px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } - - .audiobox-marker-timeline, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 10px; - top: 2.5%; - border-radius: 50%; - box-shadow: black 2px 2px 1px; - overflow: visible; - cursor: pointer; - - .left-resizer { - background: dimgrey; - } - .resizer { - background: dimgrey; - } - .audiobox-marker { - position: relative; - height: 100%; - // height: calc(100% - 15px); - width: 100%; - //margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - } - - .audiobox-marker-timeline, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 90%; - top: 2.5%; - 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; - } - - .resizer { - position: absolute; - top: 0; - right: 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - - .click { - position: relative; - height: 100%; - width: 100%; - z-index: 100; - } - - .left-resizer { - position: absolute; - left: 0; - top: 0; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - - // .contentFittingDocumentView-previewDoc { - // width: 100% !important; - // transform: none !important; - // } - } - - .audiobox-marker-container1:hover, - .audiobox-marker-minicontainer:hover { - opacity: 0.8; - } - - .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/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 506ba8c49..8a1cefbd9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -7,7 +7,7 @@ import { Dictionary } from "typescript-collections"; import { Doc, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; -import { createSchema, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; @@ -29,11 +29,8 @@ import { LinkDocPreview } from "./LinkDocPreview"; import "./VideoBox.scss"; const path = require('path'); -export const timeSchema = createSchema({ - _currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first -}); -type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>; -const VideoDocument = makeInterface(documentSchema, timeSchema); +type VideoDocument = makeInterface<[typeof documentSchema]>; +const VideoDocument = makeInterface(documentSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { @@ -63,7 +60,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @computed get links() { return DocListCast(this.dataDoc.links); } @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } @computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } - @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); } private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } public get player(): HTMLVideoElement | null { return this._videoRef; } @@ -73,11 +69,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance = this; } - anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))); - anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))); - getAnchor = () => { - return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey + "-timeline", "videoStart", "videoEnd", Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; } choosePath(url: string) { @@ -92,13 +85,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; } - static keyEventsWrapper = (e: KeyboardEvent) => { - VideoBox.Instance._stackedTimeline.current?.keyEvents(e); - } - @action public Play = (update: boolean = true) => { - document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); - document.addEventListener("keydown", VideoBox.keyEventsWrapper, true); this._playing = true; try { update && this.player?.play(); @@ -180,7 +167,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD //convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); + const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, ""); + const filename = path.basename(encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && this.createRealSummaryLink(returnedFilename)); } @@ -192,14 +180,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), + x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), isLinkButton: true, _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc)); - imageSummary.isLinkButton = true; this.props.addDocument?.(imageSummary); - DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot"); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot"); } @action @@ -254,7 +241,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); - document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); } @action @@ -419,7 +405,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD action((e: PointerEvent) => { this._clicking = false; if (this.active()) { - const local = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); } return false; @@ -431,11 +417,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD }); onResetDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e: PointerEvent) => { - this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); - e.stopImmediatePropagation(); - return false; - }, emptyFunction, + setupMoveUpEvents(this, e, + (e: PointerEvent) => { + this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); + e.stopImmediatePropagation(); + return false; + }, + emptyFunction, (e: PointerEvent) => this.layoutDoc._currentTimecode = 0); } @@ -454,7 +442,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc._timecodeToShow = curTime); + docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1); return this.addDocument(doc); } @@ -486,62 +474,54 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } playLink = (doc: Doc) => { - const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow)); - const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null)); + const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0; + const endTime = this._stackedTimeline.current?.anchorEnd(doc); if (startTime !== undefined) { if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } } - // returns the timeline + playing = () => this._playing; + isActiveChild = () => this._isChildActive; + timelineWhenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(runInAction(() => this._isChildActive = isActive)); + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight()); + setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time; + timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100; @computed get renderTimeline() { return <div style={{ width: "100%", transition: this.transition, height: `${100 - this.heightPercent}%`, position: "absolute" }}> - <CollectionStackedTimeline ref={this._stackedTimeline} - Document={this.props.Document} + <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} renderDepth={this.props.renderDepth + 1} - parentActive={this.props.parentActive} + startTag={"videoStart"} + endTag={"videoEnd"} + fieldKeySuffix={"-timeline"} focus={emptyFunction} - styleProvider={this.props.styleProvider} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - rootSelected={this.props.rootSelected} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} bringToFront={emptyFunction} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} CollectionView={undefined} duration={this.duration} playFrom={this.playFrom} - setTime={(time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time} - playing={() => this._playing} - select={this.props.select} - isSelected={this.props.isSelected} - whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + setTime={this.setAnchorTime} + playing={this.playing} + whenActiveChanged={this.timelineWhenActiveChanged} removeDocument={this.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())} - isChildActive={() => this._isChildActive} + ScreenToLocalTransform={this.timelineScreenToLocal} + isChildActive={this.isActiveChild} Play={this.Play} Pause={this.Pause} active={this.active} playLink={this.playLink} - PanelWidth={this.props.PanelWidth} - PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100} + PanelHeight={this.timelineHeight} /> </div>; } - contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; - @computed get annotationLayer() { return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; } marqueeDown = action((e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; }); finishMarquee = action(() => { @@ -549,6 +529,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.props.select(true); }); + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * this.heightPercent / 100; @@ -556,6 +537,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); } + marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100; + marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0]; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -590,7 +573,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.annotationLayer} {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + <MarqueeAnnotator + rootDoc={this.rootDoc} + down={this._marqueeing} + scaling={this.marqueeFitScaling} + containerOffset={this.marqueeOffset} + addDocument={this.addDocWithTimecode} + finishMarquee={this.finishMarquee} + savedAnnotations={this._savedAnnotations} + annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} + />} </div> </div >); } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 37f268823..4b7f0bf77 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -462,9 +462,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) { - this._marqueeing = [e.clientX, e.clientY]; - } + if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; } @action diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index b04f60500..81bca4c00 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -10,6 +10,21 @@ outline: none !important; } +audiotag { + left: 0; + position: absolute; + cursor: pointer; + border-radius: 10px; + width: 10px; + margin-top: -2px; + font-size: 4px; + background: lightblue; +} +audiotag:hover { + transform: scale(2); + transform-origin: bottom center; +} + .formattedTextBox-cont { touch-action: none; background: inherit; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 36d268fe9..d24ccd9ad 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -66,6 +66,7 @@ import { SubCollectionViewProps } from '../../collections/CollectionSubView'; import { StyleProp } from '../../StyleProvider'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { DocumentManager } from '../../../util/DocumentManager'; export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text @@ -99,10 +100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; private _dropDisposer?: DragManager.DragDropDisposer; - private _first: Boolean = true; private _recordingStart: number = 0; - private _currentTime: number = 0; - private _linkTime: number | null = null; private _pause: boolean = false; private _animatingScroll: number = 0; // hack to prevent scroll values from being written to document when scroll is animating @@ -341,39 +339,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView.updateState(EditorState.fromJSON(this.config, json)); } } + if (window.getSelection()?.isCollapsed) AnchorMenu.Instance.fadeOut(true); } } pause = () => this._pause = true; - formatTime = (time: number) => { - const hours = Math.floor(time / 60 / 60); - const minutes = Math.floor(time / 60) - (hours * 60); - const seconds = time % 60; - - return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); - } - // for inserting timestamps insertTime = () => { - let audioState; - if (this._first) { - DocListCast(this.dataDoc.links).map((l, i) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - this._linkTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime)); - audioState = la2.audioState; - if (Doc.AreProtosEqual(la2, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - audioState = la1.audioState; - } - }); - } - this._currentTime = Date.now(); - let time; - this._linkTime ? time = this.formatTime(Math.round(this._linkTime + this._currentTime / 1000 - this._recordingStart / 1000)) : time = null; - + let linkTime; + let linkAnchor; + DocListCast(this.dataDoc.links).forEach((l, i) => { + const anchor = (l.anchor1 as Doc).annotationOn ? l.anchor1 as Doc : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; + if (anchor && (anchor.annotationOn as Doc).audioState === "recording") { + linkTime = NumCast(anchor.audioStart); + linkAnchor = anchor; + } + }); if (this._editorView) { const state = this._editorView.state; const now = Date.now(); @@ -388,13 +370,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } - if (time && audioState === "recording") { - let value = ""; + + const path = (this._editorView.state.selection.$from as any).path; + if (linkAnchor && linkTime && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { + const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; - value = this.layoutDoc._timeStampOnEnter ? "[" + time + "] " : "\n" + "[" + time + "] "; const from = state.selection.from; - const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); - this._editorView.dispatch(this._editorView.state.tr.insertText(value)); + const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); + const replaced = this._editorView.state.tr.insert(from - 1, value); + this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); } } } @@ -568,10 +552,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } return ret; } - static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; + static _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; updateHighlights = () => { clearStyleSheetRules(FormattedTextBox._userStyleSheet); + if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, ""); + } if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" }); } @@ -643,8 +630,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); const highlighting: ContextMenuProps[] = []; - const noviceHighlighting = ["My Text", "Text from Others"]; - const expertHighlighting = ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"]; + const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others"]; + const expertHighlighting = [...noviceHighlighting, "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"]; (Doc.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { @@ -1312,6 +1299,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp _break = false; _collapsed = false; onPointerDown = (e: React.PointerEvent): void => { + if ((e.target as any).tagName === "AUDIOTAG") { + e.preventDefault(); + e.stopPropagation(); + const time = (e.target as any)?.dataset?.timecode || 0; + const audioid = (e.target as any)?.dataset?.audioid || 0; + DocServer.GetRefField(audioid).then(anchor => { + if (anchor instanceof Doc) { + const audiodoc = anchor.annotationOn as Doc; + audiodoc._triggerAudio = Number(time); + !DocumentManager.Instance.getDocumentView(audiodoc) && this.props.addDocTab(audiodoc, "add:bottom"); + } + }); + } if (this._recording && !e.ctrlKey && e.button === 0) { this.stopDictation(true); this._break = true; diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 64f7d27e5..722c0a836 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -6,6 +6,14 @@ import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from "./Para const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; +function formatAudioTime(time: number) { + time = Math.round(time); + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) - (hours * 60); + const seconds = time % 60; + + return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); +} // :: Object // [Specs](#model.NodeSpec) for the nodes defined in this schema. export const nodes: { [index: string]: NodeSpec } = { @@ -14,6 +22,34 @@ export const nodes: { [index: string]: NodeSpec } = { content: "block+" }, + audiotag: { + group: "block", + attrs: { + timeCode: { default: 0 }, + audioId: { default: "" } + }, + toDOM(node) { + return ['audiotag', + { + // style: see FormattedTextBox.scss + "data-timecode": node.attrs.timeCode, + "data-audioid": node.attrs.audioId, + }, + formatAudioTime(node.attrs.timeCode.toString()) + ]; + }, + parseDOM: [ + { + tag: "audiotag", getAttrs(dom: any) { + return { + timeCode: dom.getAttribute("data-timecode"), + audioId: dom.getAttribute("data-audioid") + }; + } + }, + ] + }, + footnote: { group: "inline", content: "inline*", @@ -315,7 +351,7 @@ export const nodes: { [index: string]: NodeSpec } = { mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet" visibility: { default: true } }, - content: 'paragraph+ | (paragraph ordered_list)', + content: '(paragraph|audiotag)+ | ((paragraph|audiotag)+ ordered_list)', parseDOM: [{ tag: "li", getAttrs(dom: any) { return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") }; |
