diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/AudioBox.scss | 9 | ||||
| -rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 374 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/nodes/FilterBox.tsx | 18 | ||||
| -rw-r--r-- | src/client/views/nodes/LabelBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/PresBox.tsx | 23 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.scss | 146 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 325 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/RichTextMenu.tsx | 2 |
11 files changed, 508 insertions, 417 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 93cdbbb09..4a3bbf8d8 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -159,9 +159,8 @@ } .audiobox-timeline { - position: relative; + position: absolute; width: 100%; - background: white; border: gray solid 1px; border-radius: 3px; z-index: 1000; @@ -342,7 +341,7 @@ .audiobox-marker { position: relative; - height: calc(100% - 8px); + height: 100%; margin-top: 8px; } } @@ -351,14 +350,14 @@ .current-time { position: absolute; font-size: 8; - top: calc(100% - 8px); + top: 100%; left: 30px; color: white; } .total-time { position: absolute; - top: calc(100% - 8px); + top: 100%; font-size: 8; right: 2px; color: white; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c3c876b75..c8bec74fb 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -3,33 +3,28 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import axios from "axios"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { computedFn } from "mobx-utils"; import Waveform from "react-audio-waveform"; import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Scripting } from "../../util/Scripting"; -import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import "./AudioBox.scss"; -import { DocumentView } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; - declare class MediaRecorder { // whatever MediaRecorder has constructor(e: any); @@ -47,13 +42,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD static playheadWidth = 30; // width of playhead static heightPercent = 80; // height of timeline in percent of height of audioBox. static Instance: AudioBox; - static RangeScript: ScriptField; - static LabelScript: ScriptField; - static RangePlayScript: ScriptField; - static LabelPlayScript: ScriptField; _disposers: { [name: string]: IReactionDisposer } = {}; _ele: HTMLAudioElement | null = null; + _audioRef = React.createRef<HTMLDivElement>(); + _stackedTimeline = React.createRef<CollectionStackedTimeline>(); _recorder: any; _recordStart = 0; _pauseStart = 0; @@ -61,16 +54,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD _pausedTime = 0; _stream: MediaStream | undefined; _start: number = 0; - _hold: boolean = false; - _left: boolean = false; - _dragging = false; _play: any = null; - _audioRef = React.createRef<HTMLDivElement>(); - _timeline: Opt<HTMLDivElement>; - _markerStart: number = 0; - _currMarker: any; - @observable static SelectingRegion: AudioBox | undefined = undefined; @observable static _scrubTime = 0; @observable _markerEnd: number = 0; @observable _position: number = 0; @@ -80,26 +65,25 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD set audioState(value) { this.dataDoc.audioState = value; } public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; }); @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - @computed get audioDuration() { return NumCast(this.dataDoc.duration); } - @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } + @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } + @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get links() { return DocListCast(this.dataDoc.links); } @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time + @computed get heightPercent() { return AudioBox.heightPercent; } constructor(props: Readonly<FieldViewProps>) { super(props); AudioBox.Instance = this; - // onClick play scripts - AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + if (this.duration === undefined) { + runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration); + } } getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2.audioStart, NumCast(la1.audioStart)); + const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -107,16 +91,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD return { la1, la2, linkTime }; } + 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; + } + componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); } - getAnchor = () => { - return this.createMarker(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)); - } - @action componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. @@ -125,13 +109,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); - this._disposers.audioStart = reaction( - () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._audioStart, "number", null) : undefined, - audioStart => audioStart !== undefined && setTimeout(() => { - this._audioRef.current && this.playFrom(audioStart); + 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); setTimeout(() => { - this.Document._currentTimecode = audioStart; - this.Document._audioStart = undefined; + this.Document._currentTimecode = start; + this.Document._triggerAudio = undefined; }, 10); }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } @@ -140,32 +124,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD 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._audioRef.current && this.Pause(); setTimeout(() => this.Document._audioStop = undefined, 10); }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); } - playLink = (doc: Doc) => { - this.links.filter(l => l.anchor1 === doc || l.anchor2 === doc).forEach(l => { - const { la1, la2 } = this.getLinkData(l); - const startTime = NumCast(la1.audioStart, NumCast(la2.audioStart, null)); - const endTime = NumCast(la1.audioEnd, NumCast(la2.audioEnd, null)); - if (startTime !== undefined) { - this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); - } - }); - doc.annotationOn === this.rootDoc && this.playFrom(NumCast(doc.audioStart), Cast(doc.audioEnd, "number", null)); - } - // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; if (this.audioState !== "recording" && htmlEle) { - htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); - this.links.map(l => { - const { la1, linkTime } = this.getLinkData(l); + htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); + this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => { if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); } @@ -175,7 +146,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } // pause play back - pause = action(() => { + Pause = action(() => { this._ele!.pause(); this.audioState = "paused"; }); @@ -187,23 +158,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // play back the audio from time @action - playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); - this.playFrom(seekTimeInSeconds, endTime); - } - - // play back the audio from time - @action - clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); - else { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); - this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); - } - } - // play back the audio from time - @action - playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => { + playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { clearTimeout(this._play); if (Number.isNaN(this._ele?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); @@ -212,17 +167,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (seekTimeInSeconds > -1) { setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); } else { - this.pause(); + this.Pause(); } } else if (seekTimeInSeconds <= this._ele.duration) { this._ele.currentTime = seekTimeInSeconds; this._ele.play(); runInAction(() => this.audioState = "playing"); - if (endTime !== this.audioDuration) { - this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration + if (endTime !== this.duration) { + this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } } else { - this.pause(); + this.Pause(); } } } @@ -261,9 +216,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; + funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -271,7 +226,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; - this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; this.audioState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); @@ -287,9 +242,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } // for play button - onPlay = (e: any) => { + Play = (e?: any) => { this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1); - e.stopPropagation(); + e?.stopPropagation?.(); } // creates a text document for dictation @@ -306,15 +261,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // ref for updating time setRef = (e: HTMLAudioElement | null) => { e?.addEventListener("timeupdate", this.timecodeChanged); - e?.addEventListener("ended", this.pause); + e?.addEventListener("ended", this.Pause); this._ele = e; } - // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { - this._timeline = timeline; - } - // returns the path of the audio file @computed get path() { const field = Cast(this.props.Document[this.props.fieldKey], AudioField); @@ -324,8 +274,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // returns the html audio element @computed get audio() { - const interactive = this.active() ? "-interactive" : ""; - return <audio ref={this.setRef} className={`audiobox-control${interactive}`}> + return <audio ref={this.setRef} className={`audiobox-control${this.active() ? "-interactive" : ""}`}> <source src={this.path} type="audio/mpeg" /> Not supported. </audio>; @@ -349,118 +298,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD e.stopPropagation(); } - // starting the drag event for marker resizing - @action - onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); - if (rect && e.target !== this._audioRef.current && this.active()) { - const wasPaused = this.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; - wasPaused && this.pause(); - - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; - this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); - AudioBox.SelectingRegion = this; - setupMoveUpEvents(this, e, - action(e => { - this._markerEnd = toTimeline(e.clientX - rect.x); - if (this._markerEnd < this._markerStart) { - const tmp = this._markerStart; - this._markerStart = this._markerEnd; - this._markerEnd = tmp; - } - return false; - }), - action((e, movement) => { - AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); - AudioBox.SelectingRegion = undefined; - }), - e => { - this.props.select(false); - e.shiftKey && this.createMarker(this._ele!.currentTime); - } - , this.props.isSelected(true) || this._isChildActive); - } - } - - @action - createMarker(audioStart?: number, audioEnd?: number) { - if (audioStart === undefined) return this.rootDoc; - const marker = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, - useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false, - isLabel: audioEnd === undefined, - _autoHeight: true, annotationOn: this.props.Document - }); - if (this.dataDoc[this.annotationKey]) { - this.dataDoc[this.annotationKey].push(marker); - } else { - this.dataDoc[this.annotationKey] = new List<Doc>([marker]); - } - return marker; - } - - // starting the drag event for marker resizing - onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { - this._currMarker = m; - this._left = left; - this._timeline?.setPointerCapture(e.pointerId); - const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.audioDuration; - setupMoveUpEvents(this, e, - (e) => { - const rect = (e.target as any).getBoundingClientRect(); - this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); - return false; - }, - (e) => { - const rect = (e.target as any).getBoundingClientRect(); - this._ele!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width); - this._timeline?.releasePointerCapture(e.pointerId); - }, - emptyFunction); - } - - // updates the marker with the new time - @action - changeMarker = (m: any, time: any) => { - this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.audioStart = time : marker.audioEnd = time); - } - - // checks if the two markers are the same with start and end time - isSame = (m1: any, m2: any) => { - return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; - } - - // makes sure no markers overlaps each other by setting the correct position and width - getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => { - const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth; - const x1 = m.audioStart; - const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.audioDuration : m.audioEnd; - let max = 0; - const overlappedLevels = new Set(placed.map(p => { - const y1 = p.audioStart; - const y2 = p.audioEnd; - if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || - (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { - max = Math.max(max, p.level); - return p.level; - } - })); - let level = max + 1; - for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - - placed.push({ audioStart: x1, audioEnd: x2, level }); - return level; - } - - @computed get selectionContainer() { - return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ - left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`, - width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%" - }} />; - } - // returns the audio waveform @computed get waveform() { const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []); @@ -469,8 +306,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD color={"darkblue"} height={this._waveHeight} barWidth={0.1} - pos={this.audioDuration} - duration={this.audioDuration} + pos={this.duration} + duration={this.duration} peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined} progressColor={"blue"} />; } @@ -490,51 +327,64 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD ); } - rangeClickScript = () => AudioBox.RangeScript; - labelClickScript = () => AudioBox.LabelScript; - rangePlayScript = () => AudioBox.RangePlayScript; - labelPlayScript = () => AudioBox.LabelPlayScript; - renderInner = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const marker = observable({ view: undefined as any }); - return { - marker, view: <DocumentView key="view" {...this.props} ref={action((r: DocumentView | null) => marker.view = r)} - Document={mark} - PanelWidth={() => width} - PanelHeight={() => height} - focus={() => this.playLink(mark)} - rootSelected={returnFalse} - LayoutTemplate={undefined} - ContainingCollectionDoc={this.props.Document} - removeDocument={this.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)} - parentActive={(out) => this.props.isSelected(out) || this._isChildActive} - whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} - onClick={script} - onDoubleClick={this.layoutDoc.playOnClick ? undefined : doublescript} - ignoreAutoHeight={false} - bringToFront={emptyFunction} - scriptContext={this} /> - }; - }); - renderMarker = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const inner = this.renderInner(mark, script, doublescript, x, y, width, height); - return <> - {inner.view} - {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) : - <> - <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} /> - <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} /> - </>} - </>; - }); + playing = () => this.audioState === "playing"; + playLink = (link: Doc) => { + 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); + } + 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)); + if (startTime !== undefined) { + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; + } + }); + } + } + + @computed get renderTimeline() { + return <CollectionStackedTimeline ref={this._stackedTimeline} + Document={this.props.Document} + fieldKey={this.annotationKey} + renderDepth={this.props.renderDepth + 1} + parentActive={this.props.parentActive} + 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} + playing={this.playing} + select={this.props.select} + isSelected={this.props.isSelected} + whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + removeDocument={this.removeDocument} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(0, -(100 - this.heightPercent) / 200 * this.props.PanelHeight())} + isChildActive={() => this._isChildActive} + 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) + />; + } render() { const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : ""; - const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth; - const timelineContentHeight = (this.props.PanelHeight() * AudioBox.heightPercent / 100) * AudioBox.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) - const overlaps: { audioStart: number, audioEnd: number, level: number }[] = []; - const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); - const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 1; return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} @@ -562,43 +412,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} > <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.onPlay}> <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: `${AudioBox.heightPercent}%` }} ref={this.timelineRef} - onClick={e => { e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> + <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="waveform"> {this.waveform} </div> - {drawMarkers.map((d, i) => { - const m = d.marker; - const left = NumCast(m.audioStart) / this.audioDuration * timelineContentWidth; - const top = d.level / maxLevel * timelineContentHeight; - const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.audioDuration : NumCast(m.audioEnd) - NumCast(m.audioStart); - return this.layoutDoc.hideMarkers ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={i} - style={{ left, top, width: `${timespan / this.audioDuration * 100}%`, height: `${1 / maxLevel * 100}%` }} - onClick={e => { this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > - {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, - left + AudioBox.playheadWidth, - (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top, - timelineContentWidth * timespan / this.audioDuration, - timelineContentHeight / maxLevel)} - </div>; - })} - {this.selectionContainer} - <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> - {this.audio} </div> + {this.renderTimeline} + {this.audio} <div className="current-time"> {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} </div> <div className="total-time"> - {formatTime(Math.round(this.audioDuration))} + {formatTime(Math.round(this.duration))} </div> </div> </div> } </div>; } -} -Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c5c47b463..6dc7a822c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -385,9 +385,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, documentView: this.props.DocumentView, + clientX: e.clientX, + clientY: e.clientY, shiftKey: e.shiftKey }, console.log); - undoBatch(func)(); + UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); } else if (!Doc.IsSystem(this.props.Document)) { if (this.props.Document.type !== DocumentType.LABEL) { UndoManager.RunInBatch(() => { @@ -401,19 +403,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const shiftKey = e.shiftKey; + const clientX = e.clientX; + const clientY = e.clientY; const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, documentView: this.props.DocumentView, + clientX: clientX, + clientY: clientY, shiftKey }, console.log); const clickFunc = () => { if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) && !this.onClickHandler.script.originalScript.includes("selectMainMenu")) { - UndoManager.RunInBatch(func, "on click"); + UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on click"); } else func(); }; if (this.onDoubleClickHandler) { @@ -544,7 +550,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (this.props.LayoutTemplateString) return; + if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; if (this.props.Document === CurrentUserUtils.ActiveDashboard) { alert((e.target as any)?.closest?.("*.lm_content") ? "You can't perform this move most likely because you don't have permission to modify the destination." : @@ -954,7 +960,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; ContentScale = () => this.nativeScaling; - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + screenToLocalTransform = () => { + return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + } componentDidMount() { !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 2db610e43..63455e3d1 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -103,7 +103,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc * Responds to clicking the check box in the flyout menu */ facetClick = (facetHeader: string) => { - const targetDoc = CollectionDockingView.Instance.props.Document; + const targetDoc = SelectionManager.Views()[0].Document; // CollectionDockingView.Instance.props.Document; const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader); if (found !== -1) { (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); @@ -208,15 +208,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc render() { const facetCollection = this.props.Document; - const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input className="filterBox-flyout-facet-check" type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div>; + // const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}> + // {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> + // <input className="filterBox-flyout-facet-check" type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> + // <span className="checkmark" /> + // {facet} + // </label>)} + // </div>; - const attributes = this.activeAttributes; + // 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 })); diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 3448a4abd..87d5b07a2 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -67,7 +67,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... - const label = StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title)); + const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); return ( <div className="labelBox-outerDiv" onClick={action(() => this.clicked = !this.clicked)} diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index aef776563..62e497e18 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -232,14 +232,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); if (targetDoc.type === DocumentType.AUDIO) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._audioStart = NumCast(activeItem.presStartTime); + targetDoc._triggerAudio = NumCast(activeItem.presStartTime); this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; } else if (targetDoc.type === DocumentType.VID) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._videoStop = true; + targetDoc._triggerVideoStop = true; setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); - setTimeout(() => targetDoc._videoStart = true, 20); - this._mediaTimer = [setTimeout(() => targetDoc._videoStop = true, (duration * 1000) + 20), targetDoc]; + setTimeout(() => targetDoc._triggerVideo = true, 20); + this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; } } @@ -249,7 +249,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> targetDoc._audioStop = true; } else if (targetDoc.type === DocumentType.VID) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._videoStop = true; + targetDoc._triggerVideoStop = true; } } @@ -714,9 +714,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (audio) { audio.mediaStart = "manual"; audio.mediaStop = "manual"; - audio.presStartTime = NumCast(doc.audioStart); - audio.presEndTime = NumCast(doc.audioEnd); - audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart); + audio.presStartTime = NumCast(doc.anchorStartTime); + audio.presEndTime = NumCast(doc.anchorEndTime); + audio.presDuration = NumCast(doc.anchorEndTime) - NumCast(doc.anchorStartTime); TabDocView.PinDoc(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; @@ -1477,6 +1477,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get mediaOptionsDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10); const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + ". " + this.childDocs[mediaStopDocInd - 1].title : ""; if (activeItem && targetDoc) { @@ -1521,7 +1522,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> <div className="multiThumb-slider"> - <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presEndTime)} + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presEndTime)} style={{ gridColumn: 1, gridRow: 1 }} className={`toolbar-slider ${"end"}`} id="toolbar-slider" @@ -1545,7 +1546,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> e.stopPropagation(); activeItem.presEndTime = Number(e.target.value); }} /> - <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presStartTime)} + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presStartTime)} style={{ gridColumn: 1, gridRow: 1 }} className={`toolbar-slider ${"start"}`} id="toolbar-slider" @@ -1573,7 +1574,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> <div className="slider-text">0 s</div> <div className="slider-text"></div> - <div className="slider-text">{activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} s</div> + <div className="slider-text">{duration / 10} s</div> </div> </div> <div className="ribbon-final-box"> diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 07e8e0951..19f605278 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -10,6 +10,139 @@ .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%; + 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; + } + } + } } .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen, @@ -49,6 +182,19 @@ transform-origin: left top; pointer-events:all; } + +.timeline-button { + position: absolute; + display: flex; + align-items: center; + z-index: 1010; + bottom: 35px; + right: 235px; + color: white; + background: dimgrey; + width: 20px; + height: 20px; +} .videoBox-play { width: 25px; height: 20px; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 6354c677e..bfac7dc1c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,31 +3,30 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Doc } from "../../../fields/Doc"; +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 { Cast, StrCast, NumCast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils"; +import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; +import { Networking } from "../../Network"; +import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; -import "./VideoBox.scss"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { Networking } from "../../Network"; -import { SnappingManager } from "../../util/SnappingManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { LinkDocPreview } from "./LinkDocPreview"; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; -import { Transform } from "../../util/Transform"; -import { StyleProp } from "../StyleProvider"; -import { Dictionary } from "typescript-collections"; -import { MarqueeAnnotator } from "../MarqueeAnnotator"; -import { AnchorMenu } from "../pdf/AnchorMenu"; +import { LinkDocPreview } from "./LinkDocPreview"; +import "./VideoBox.scss"; const path = require('path'); export const timeSchema = createSchema({ @@ -38,26 +37,50 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } static _youtubeIframeCounter: number = 0; + static Instance: VideoBox; + static heightPercent = 60; // height of timeline in percent of height of videoBox. private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; - private _isResetClick = 0; + private _stackedTimeline = React.createRef<CollectionStackedTimeline>(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + private _playRegionTimer: any = null; + private _playRegionDuration = 0; + @observable static _showControls: boolean; @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable _screenCapture = false; + @observable _visible: boolean = false; @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable _playing = false; - @observable static _showControls: boolean; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } + @computed get links() { return DocListCast(this.dataDoc.links); } + @computed get heightPercent() { return this.layoutDoc._timelineShow ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 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])); } - public get player(): HTMLVideoElement | null { - return this._videoRef; + public get player(): HTMLVideoElement | null { return this._videoRef; } + + constructor(props: Readonly<FieldViewProps>) { + super(props); + 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; + } + + choosePath(url: string) { + return url.indexOf(window.location.origin) === -1 ? Utils.CorsProxy(url) : url; } videoLoad = () => { @@ -68,7 +91,13 @@ 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(); @@ -114,13 +143,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } } - choosePath(url: string) { - if (url.indexOf(window.location.origin) === -1) { - return Utils.CorsProxy(url); - } - return url; - } - @action public Snapshot() { const width = (this.layoutDoc._width || 0); const height = (this.layoutDoc._height || 0); @@ -190,6 +212,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } componentDidMount() { + this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + this._disposers.selection = reaction(() => this.props.isSelected(), selected => { if (!selected) { @@ -198,30 +222,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } }, { fireImmediately: true }); - this._disposers.videoStart = reaction( - () => this.Document._videoStart, - (videoStart) => { - if (videoStart !== undefined) { - if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - const delay = this.player ? 0 : 250; // wait for mainCont and try again to play - setTimeout(() => this.player && this.Play(), delay); - setTimeout(() => { this.Document._videoStart = undefined; }, 10 + delay); - } - } - }, + this._disposers.triggerVideo = reaction( + () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined, + time => time !== undefined && setTimeout(() => { + this.player && this.Play(); + setTimeout(() => this.Document._triggerVideo = undefined, 10); + }, this.player ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); - this._disposers.videoStop = reaction( - () => this.Document._videoStop, - (videoStop) => { - if (videoStop !== undefined) { - if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - const delay = this.player ? 0 : 250; // wait for mainCont and try again to play - setTimeout(() => this.player && this.Pause(), delay); - setTimeout(() => { this.Document._videoStop = undefined; }, 10 + delay); - } - } - }, + this._disposers.triggerStop = reaction( + () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? NumCast(this.Document._triggerVideoStop, null) : undefined, + stop => stop !== undefined && setTimeout(() => { + this.player && this.Pause(); + setTimeout(() => this.Document._triggerVideoStop = undefined, 10); + }, this.player ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); if (this.youtubeVideoId) { @@ -238,7 +252,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); - Object.values(this._disposers).forEach(disposer => disposer?.()); + Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); + document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); } @action @@ -270,7 +285,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD console.log("VideoBox :" + e); } } - @observable _screenCapture = false; + specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { @@ -285,6 +300,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } @@ -294,17 +311,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : - <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} - onCanPlay={this.videoLoad} - controls={VideoBox._showControls} - onPlay={() => this.Play()} - onSeeked={this.updateTimecode} - onPause={() => this.Pause()} - onClick={e => e.preventDefault()}> - <source src={field.url.href} type="video/mp4" /> - Not supported. - </video>; + <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}> + <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}> + <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }} + onCanPlay={this.videoLoad} + controls={VideoBox._showControls} + onPlay={() => this.Play()} + onSeeked={this.updateTimecode} + onPause={() => this.Pause()} + onClick={e => e.preventDefault()}> + <source src={field.url.href} type="video/mp4" /> + Not supported. + </video> + </div> + </div>; } @computed get youtubeVideoId() { @@ -356,18 +377,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private get uIButtons() { const curTime = (this.layoutDoc._currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > - <span>{"" + Math.round(curTime)}</span> + <span>{"" + formatTime(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, + <div className="timeline-button" key="timeline-button" onPointerDown={action(e => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow)} + style={{ + transform: `scale(${this.scaling()})`, + right: this.scaling() * 10 - 10, + bottom: this.scaling() * 10 - 10 + }}> + <FontAwesomeIcon icon={this.layoutDoc._timelineShow ? "eye-slash" : "eye"} style={{ width: "100%" }} /> + </div>, VideoBox._showControls ? (null) : [ + // <div className="control-background"> <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} > <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> </div>, <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} > F + {/* </div> */} </div> ]]); } @@ -387,24 +418,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } onResetDown = (e: React.PointerEvent) => { - this.Pause(); - e.stopPropagation(); - this._isResetClick = 0; - document.addEventListener("pointermove", this.onResetMove, true); - document.addEventListener("pointerup", this.onResetUp, true); - } - - onResetMove = (e: PointerEvent) => { - this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY); - this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); - e.stopImmediatePropagation(); - } - - @action - onResetUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onResetMove, true); - document.removeEventListener("pointerup", this.onResetUp, true); - this._isResetClick < 10 && (this.layoutDoc._currentTimecode = 0); + 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); } @computed get youtubeContent() { @@ -419,59 +438,147 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action.bound - addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { + addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc.displayTimecode = curTime); + docs.forEach(doc => doc._timecodeToShow = curTime); return this.addDocument(doc); } - screenToLocalTransform = () => this.props.ScreenToLocalTransform(); + // play back the video from time + @action + playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { + clearTimeout(this._playRegionTimer); + this._playRegionDuration = endTime - seekTimeInSeconds; + if (Number.isNaN(this.player?.duration)) { + setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + } else if (this.player) { + if (seekTimeInSeconds < 0) { + if (seekTimeInSeconds > -1) { + setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); + } else { + this.Pause(); + } + } else if (seekTimeInSeconds <= this.player.duration) { + this.player.currentTime = seekTimeInSeconds; + this.player.play(); + runInAction(() => this._playing = true); + if (endTime !== this.duration) { + this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration + } + } else { + this.Pause(); + } + } + } + + playLink = (doc: Doc) => { + const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow)); + const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null)); + if (startTime !== undefined) { + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this.Seek(startTime); + } + } + + // returns the timeline + @computed get renderTimeline() { + return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}> + <CollectionStackedTimeline ref={this._stackedTimeline} + Document={this.props.Document} + fieldKey={this.annotationKey} + renderDepth={this.props.renderDepth + 1} + parentActive={this.props.parentActive} + 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))} + removeDocument={this.removeDocument} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())} + isChildActive={() => this._isChildActive} + Play={this.Play} + Pause={this.Pause} + active={this.active} + playLink={this.playLink} + PanelWidth={this.props.PanelWidth} + PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100} + /> + </div>; + } + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ 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]; - }) + }); finishMarquee = action(() => { this._marqueeing = undefined; this.props.select(true); - }) + }); + + 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; + screenToLocalTransform = () => { + const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); + return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); + } render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; + const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius }} > - <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - forceScaling={true} - fieldKey={this.annotationKey} - isAnnotationOverlay={true} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - ScreenToLocalTransform={this.screenToLocalTransform} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWithTimestamp} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - {this.contentFunc} - </CollectionFreeFormView> + <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > + <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + fieldKey={this.annotationKey} + isAnnotationOverlay={true} + forceScaling={true} + select={emptyFunction} + active={this.annotationsActive} + scaling={returnOne} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocWithTimecode} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + {this.uIButtons} + {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} />} </div> - {this.uIButtons} - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 69f797880..37f268823 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -194,7 +194,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._iframe?.removeEventListener('wheel', this.iframeWheel); } - onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } + onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); }; @undoBatch @action @@ -280,8 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; } - onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; } + onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; }; + onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; }; onPostPointer = (e: React.PointerEvent) => { if (this._ignore !== e.timeStamp) e.stopPropagation(); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ac5ea66ff..e06a324d2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -230,7 +230,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100); FormattedTextBox.SelectOnLoad = target[Id]; return target; - } + }; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, { dragComplete: e => { @@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp DocListCast(this.dataDoc.links).map((l, i) => { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - this._linkTime = NumCast(la1.audioStart, NumCast(la2.audioStart)); + this._linkTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime)); audioState = la2.audioState; if (Doc.AreProtosEqual(la2, this.dataDoc)) { la1 = l.anchor2 as Doc; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 992194e2b..dc630af74 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -604,7 +604,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { e.stopPropagation(); self.TextView.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush"); - } + }; let label = "Stored marks: "; if (this.brushMarks && this.brushMarks.size > 0) { |
