From e369336c413a337e02afc027cc1aecdd76a496c5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 20 Jan 2021 23:36:16 -0500 Subject: fixed hiding AnchorMenu in text boxes. encapsulated fadeOut of AnchorMenu within class for simplicity. --- .../views/nodes/formattedText/FormattedTextBox.tsx | 80 +++++++++++----------- 1 file changed, 41 insertions(+), 39 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f9982f747..ac5ea66ff 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -59,7 +59,7 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } import React = require("react"); import { LinkManager } from '../../../util/LinkManager'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; -import { CollectionViewType, CollectionViewProps } from '../../collections/CollectionView'; +import { CollectionViewType } from '../../collections/CollectionView'; import { SnappingManager } from '../../../util/SnappingManager'; import { LinkDocPreview } from '../LinkDocPreview'; import { SubCollectionViewProps } from '../../collections/CollectionSubView'; @@ -85,6 +85,7 @@ type PullHandler = (exportState: Opt, da export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); + public static CanAnnotate = true; public static Instance: FormattedTextBox; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } @@ -211,6 +212,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.clear(); } + @action + setupAnchorMenu = () => { + AnchorMenu.Instance.Status = "marquee"; + AnchorMenu.Instance.Highlight = action((color: string) => { + this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); + return undefined; + }); + /** + * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = () => { + 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 => { + if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { + e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); + e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc; + } + e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]); + e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument + } + }); + }); + const coordsT = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(Math.min(coordsT.left, coordsB.left), Math.max(coordsT.bottom, coordsB.bottom)); + } + dispatchTransaction = (tx: Transaction) => { let timeStamp; clearTimeout(timeStamp); @@ -254,42 +291,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); - if (!this._editorView.state.selection.empty) { - runInAction(() => { - AnchorMenu.Instance.Status = "marquee"; - AnchorMenu.Instance.Highlight = action((color: string) => { - this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView?.state, this._editorView?.dispatch); - return undefined; - }); - /** - * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. - * It also initiates a Drag/Drop interaction to place the text annotation. - */ - AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { - e.preventDefault(); - e.stopPropagation(); - const targetCreator = () => { - 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 => { - if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); - e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc; - } - e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]); - e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument - } - }); - }); - }); - const coordsT = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); - const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); - AnchorMenu.Instance.jumpTo(Math.min(coordsT.left, coordsB.left), Math.max(coordsT.bottom, coordsB.bottom)); - } + if (!this._editorView.state.selection.empty && FormattedTextBox.CanAnnotate) this.setupAnchorMenu(); const removeSelection = (json: string | undefined) => { return json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); @@ -658,6 +660,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const uicontrols: ContextMenuProps[] = []; + uicontrols.push({ description: `${FormattedTextBox.CanAnnotate ? "Hide" : "Show"} Annotation Bar`, event: () => FormattedTextBox.CanAnnotate = !FormattedTextBox.CanAnnotate, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); @@ -999,7 +1002,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action((selected) => { this._recording = false; - AnchorMenu.Instance.fadeOut(true); if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -1296,9 +1298,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); this.unhighlightSearchTerms(); - Object.values(this._disposers).forEach(disposer => disposer?.()); this._editorView?.destroy(); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); } -- cgit v1.2.3-70-g09d2 From b9326dfc3e15683190a7d520daca6791ef049dea Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 22 Jan 2021 14:12:41 -0500 Subject: fixed up videoBox timeline. changed video annotations to use displayTimecode and undisplayTimecode --- src/client/documents/Documents.ts | 3 +- src/client/views/DocComponent.tsx | 6 +- src/client/views/MarqueeAnnotator.tsx | 4 +- src/client/views/StyleProvider.tsx | 3 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 7 +- src/client/views/nodes/AudioBox.tsx | 13 +- src/client/views/nodes/LabelBox.tsx | 2 +- src/client/views/nodes/VideoBox.scss | 24 ++-- src/client/views/nodes/VideoBox.tsx | 143 +++++++++++---------- src/client/views/nodes/WebBox.tsx | 6 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 2 +- 12 files changed, 119 insertions(+), 96 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index df08345f9..286b7afa9 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -152,6 +152,7 @@ export interface DocumentOptions { _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + undisplayTimecode?: number; // the time that a document should be hidden lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection appearFrame?: number; // the frame in which the document appears @@ -227,7 +228,7 @@ export interface DocumentOptions { isLabel?: boolean; // whether the document is a label or not (video / audio) useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox audioStart?: number; // the time frame where the audio should begin playing - audioEnd?: number; // the time frame where the audio should stop playing + audioEnd?: number; // the time frame where the audio should stop playing border?: string; //for searchbox hovercolor?: string; } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 2c7d15ae0..db0b626a1 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -124,7 +124,7 @@ export function ViewBoxAnnotatableComponent

effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); @@ -132,13 +132,13 @@ export function ViewBoxAnnotatableComponent

doc.isPushpin = doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; - const value = DocListCast(targetDataDoc[this.annotationKey]); + const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { - Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey + "-annotations", doc); + Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); }); return true; diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 0ab2d1ecf..8ef69802b 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -163,13 +163,13 @@ export class MarqueeAnnotator extends React.Component { const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100); FormattedTextBox.SelectOnLoad = target[Id]; return target; - } + }; const anchorCreator = () => { const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color annoDoc.isLinkButton = true; // prevents link button from showing up --- maybe not a good thing? this.props.addDocument(annoDoc); return annoDoc; - } + }; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, anchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 1d822439a..95a28b33e 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -108,9 +108,10 @@ export function DefaultStyleProvider(doc: Opt, props: Opt DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); } - public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public isCurrent(doc: Doc) { + const dispTime = NumCast(doc.displayTimecode, -1); + const endTime = NumCast(doc.undisplayTimecode, dispTime + 1.5); + const curTime = NumCast(this.Document._currentTimecode, -1); + return dispTime === -1 || (curTime > dispTime && curTime < endTime); + } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 77777ff76..4ddb0502b 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -29,6 +29,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; import "./AudioBox.scss"; +import { Id } from "../../../fields/FieldSymbols"; declare class MediaRecorder { // whatever MediaRecorder has @@ -390,9 +391,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent {this.waveform} - {drawMarkers.map((d, i) => { + {drawMarkers.map(d => { 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) : -

{ this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, 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 !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 (
this.clicked = !this.clicked)} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 76edda847..8bba5d1ff 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -13,15 +13,13 @@ .audiobox-timeline { position: absolute; - height: 20%; width: 100%; - bottom: 0px; - background: white; + background: beige; border: gray solid 1px; border-radius: 3px; z-index: 1000; overflow: hidden; - left: 0px; + bottom: 0; .audiobox-current { width: 1px; @@ -46,18 +44,23 @@ border-width: 1px; } - .audiobox-marker-container, + .audiobox-marker-timeline, .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; + .left-resizer { + background: dimgrey; + } + .resizer { + background: dimgrey; + } .audiobox-marker { position: relative; height: 100%; @@ -77,10 +80,8 @@ width: 10px; height: 90%; top: 2.5%; - background: gray; border-radius: 5px; box-shadow: black 2px 2px 1px; - opacity: 0.3; .audiobox-marker { position: relative; @@ -94,10 +95,12 @@ .resizer { position: absolute; + top: 0; right: 0; + pointer-events: all; cursor: ew-resize; height: 100%; - width: 2px; + width: 10px; z-index: 100; } @@ -111,9 +114,10 @@ .left-resizer { position: absolute; left: 0; + top: 0; cursor: ew-resize; height: 100%; - width: 2px; + width: 10px; z-index: 100; } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 2ac105545..608d7daa3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -26,10 +26,13 @@ import { List } from "../../../fields/List"; import { DocumentView } from "./DocumentView"; import { LinkDocPreview } from "./LinkDocPreview"; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; -import { Transform } from "../../util/Transform"; import { StyleProp } from "../StyleProvider"; import { computedFn } from "mobx-utils"; import { DocumentManager } from "../../util/DocumentManager"; +import { Dictionary } from "typescript-collections"; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { Id } from "../../../fields/FieldSymbols"; +import { LabelBox } from "./LabelBox"; const path = require('path'); export const timeSchema = createSchema({ @@ -47,16 +50,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent = React.createRef(); + private _annotationLayer: React.RefObject = React.createRef(); + @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations: Dictionary = new Dictionary(); _play: any = null; _timeline: Opt; _audioRef = React.createRef(); @@ -75,7 +78,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { @@ -240,7 +243,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.player && this.Play(), delay); - setTimeout(() => { this.Document._videoStart = undefined; }, 10 + delay); + setTimeout(() => this.Document._videoStart = undefined, 10 + delay); } } }, @@ -349,9 +352,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent Not supported. - {this.uIButtons}
- {this.renderTimeline}
; } @@ -412,9 +413,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent,
@@ -557,17 +560,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent([marker]); + this.dataDoc[this.annotationKey + "-timeline"] = new List([marker]); } } @@ -609,14 +614,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent { + getLevel = (m: any, placed: { videoStart: number, videoEnd: number, level: number }[]) => { const timelineContentWidth = this.props.PanelWidth(); - const x1 = m.audioStart; - const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.videoDuration : m.audioEnd; + const x1 = m.displayTimecode; + const x2 = m.undisplayTimecode === undefined ? m.displayTimecode + 10 / timelineContentWidth * this.videoDuration : m.undisplayTimecode; let max = 0; const overlappedLevels = new Set(placed.map(p => { - const y1 = p.audioStart; - const y2 = p.audioEnd; + const y1 = p.videoStart; + const y2 = p.videoEnd; if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { max = Math.max(max, p.level); @@ -626,23 +631,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent= 0; j--) !overlappedLevels.has(j) && (level = j); - placed.push({ audioStart: x1, audioEnd: x2, level }); + placed.push({ videoStart: x1, videoEnd: x2, level }); return level; } // renders the markers as a document - renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { const marker = observable({ view: undefined as any }); return { - marker, view: marker.view = r)} + marker, view: marker.view = r)} Document={mark} + DataDoc={undefined} PanelWidth={() => width} PanelHeight={() => height} rootSelected={returnFalse} LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutString("data")} ContainingCollectionDoc={this.props.Document} - removeDocument={this.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)} + removeDocument={(doc: Doc | Doc[]) => this.removeDocument(doc, annotationKey)} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).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} @@ -653,8 +660,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const inner = this.renderInner(mark, script, doublescript, x, y, width, height); + renderMarker = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { + const inner = this.renderInner(mark, script, doublescript, x, y, width, height, annotationKey); return <> {inner.view} {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) : @@ -667,32 +674,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent ({ level: this.getLevel(m, overlaps), marker: m })); + const timelineContentHeight = this.props.PanelHeight() * VideoBox.heightPercent / 100; + const overlaps: { videoStart: number, videoEnd: number, level: number }[] = []; + const drawMarkers: { level: number, marker: Doc }[] = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return !this.layoutDoc._showTimeline ? (null) : -
{ e.stopPropagation(); e.preventDefault(); }} style={{ height: `${VideoBox.heightPercent}%` }} - onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> - {drawMarkers.map((d, i) => { +
{ if (this._isChildActive || this.props.isSelected()) { e.stopPropagation(); e.preventDefault(); } }} + onPointerDown={e => { + if (this._isChildActive || this.props.isSelected()) { + e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e); + } + }}> + {drawMarkers.map(d => { const m = d.marker; - const left = NumCast(m.audioStart) / this.videoDuration; - const l = `${NumCast(m.audioStart) / this.videoDuration * 100}%`; + const start = NumCast(m.displayTimecode, NumCast(m.displayTimecode, null)); + const left = start / this.videoDuration * timelineContentWidth; const top = d.level / maxLevel * timelineContentHeight; - const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.videoDuration : NumCast(m.audioEnd) - NumCast(m.audioStart); + const timespan = m.undisplayTimecode === undefined ? 10 / timelineContentWidth * this.videoDuration : NumCast(m.undisplayTimecode) - NumCast(m.displayTimecode); return this.layoutDoc.hideMarkers ? (null) : -
{ this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > +
{ this.playFrom(start, Cast(m.undisplayTimecode, "number", null)); e.stopPropagation(); }} > {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, left, - top, + top + (this.props.PanelHeight() - timelineContentHeight), timelineContentWidth * timespan / this.videoDuration, - timelineContentHeight / maxLevel)} + timelineContentHeight / maxLevel, this.annotationKey + (m.useLinkSmallAnchor ? "-timeline" : ""))}
; })} {this.selectionContainer} @@ -703,13 +712,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent { - DocListCast(this.dataDoc[this.annotationKey]).filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.audioStart = time : marker.audioEnd = time); + this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => + this._left ? marker.displayTimecode = time : marker.undisplayTimecode = 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; + return m1.displayTimecode === m2.displayTimecode && m1.undisplayTimecode === m2.undisplayTimecode; } // returns the blue container when dragging @@ -753,17 +762,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent [this.youtubeVideoId ? this.youtubeContent : this.content]; @computed get annotationLayer() { - return
; + return
; } 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); - }) + }); render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -781,7 +790,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.props.PanelWidth() * (this.layoutDoc._showTimeline ? .8 : 1)} + PanelHeight={() => this.props.PanelHeight() * (this.layoutDoc._showTimeline ? .8 : 1)} + ScreenToLocalTransform={() => this.screenToLocalTransform().scale(this.layoutDoc._showTimeline ? 1 / .8 : 1)} whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} @@ -790,14 +801,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent {this.contentFunc} + {this.uIButtons} + {this.annotationLayer} + {this.renderTimeline} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + }
-<<<<<<< HEAD -======= - {this.uIButtons} - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - } ->>>>>>> 10b759d2bd09af3a8e8a4effbc8fd2312dd873d2
); } } 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 { e.preventDefault(); } + onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); }; @undoBatch @action @@ -280,8 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent { 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..7348ebdd2 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 => { 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 { 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) { -- cgit v1.2.3-70-g09d2 From 20add09510fb02b144d421910a56d3f3896b1f90 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 26 Jan 2021 01:38:55 -0500 Subject: preparing to unify VideoBox timeline with AudioBox timeline. changed names from videoStart/audioStart,End to anchorStart,EndTime and _displayTimecode to _timecodeToShow etc --- src/client/documents/Documents.ts | 14 +- src/client/util/DocumentManager.ts | 4 +- src/client/views/DocComponent.tsx | 3 +- src/client/views/DocumentDecorations.tsx | 2 +- src/client/views/StyleProvider.tsx | 4 +- src/client/views/collections/CollectionView.tsx | 4 +- src/client/views/collections/TabDocView.tsx | 5 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 8 +- .../collections/collectionFreeForm/MarqueeView.tsx | 3 +- src/client/views/nodes/AudioBox.scss | 3 +- src/client/views/nodes/AudioBox.tsx | 217 +++++++++++---------- src/client/views/nodes/PresBox.tsx | 23 +-- src/client/views/nodes/VideoBox.tsx | 207 ++++++++++---------- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- src/fields/documentSchemas.ts | 6 +- 15 files changed, 270 insertions(+), 235 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 286b7afa9..080270321 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -151,8 +151,10 @@ export interface DocumentOptions { _curPage?: number; _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) - displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - undisplayTimecode?: number; // the time that a document should be hidden + _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) + _timecodeToHide?: number; // the time that a document should be hidden + anchorStartTime?: number; // the time when an annotation starts on a timeline (e.g., when an audio anchor starts on an audio/video timeline) + anchorEndTime?: number; // the time when an annotaiton ends on a timeline lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection appearFrame?: number; // the frame in which the document appears @@ -227,8 +229,6 @@ export interface DocumentOptions { linearViewIsExpanded?: boolean; // is linear view expanded isLabel?: boolean; // whether the document is a label or not (video / audio) useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox - audioStart?: number; // the time frame where the audio should begin playing - audioEnd?: number; // the time frame where the audio should stop playing border?: string; //for searchbox hovercolor?: string; } @@ -722,8 +722,8 @@ export namespace Docs { linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document. linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; - linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc.displayTimecode; - linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc.displayTimecode; + linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc._timecodeToShow; + linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc._timecodeToShow; if (linkDocProto.linkBoxExcludedKeys === undefined) { Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]); @@ -1252,7 +1252,7 @@ export namespace DocUtils { docList.forEach((d, i) => { d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2; d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2; - d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection }); }); if (x !== undefined && y !== undefined) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 1f2dd350b..41c7a1409 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -191,8 +191,8 @@ export class DocumentManager { targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); // now find the target document within the context - if (targetDoc.displayTimecode) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; - targetDocContext._currentTimecode = targetDoc.displayTimecode; + if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; + targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; finished?.(); } else { // no timecode means we need to find the context view and focus on our target const findView = (delay: number) => { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index db0b626a1..8d545c61b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -81,6 +81,8 @@ export interface ViewBoxAnnotatableProps { } export function ViewBoxAnnotatableComponent

(schemaCtor: (doc: Doc) => T) { class Component extends Touchable

{ + _annotationKey: string = "annotations"; + @observable _isChildActive = false; //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @@ -120,7 +122,6 @@ export function ViewBoxAnnotatableComponent

{closeIcon} {bounds.r - bounds.x < 100 ? null : titleArea} - {(seldoc.rootDoc.annotationOn as Doc)?.type === DocumentType.AUDIO ? (null) : + {seldoc.rootDoc.anchorStartTime !== undefined ? (null) : <> {SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : {`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}

} placement="top"> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 95a28b33e..8628be014 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -108,7 +108,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt, props: Opt { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) + _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) }); pushpin.isPushpin = true; Doc.GetProto(pushpin).annotationOn = doc.annotationOn; Doc.SetInPlace(doc, "annotationOn", undefined, true); Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); - doc.displayTimecode = undefined; + doc._timecodeToShow = undefined; } doc._stayInCollection = undefined; doc.context = this.props.Document; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e5f05c407..c66734556 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -182,12 +182,13 @@ export class TabDocView extends React.Component { const presArray: Doc[] = PresBox.Instance?.sortArray(); const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); Doc.AddDocToList(curPres, "data", pinDoc, presSelected); - if (!pinProps?.audioRange && (pinDoc.type === DocumentType.AUDIO || pinDoc.type === DocumentType.VID)) { + if (!pinProps?.audioRange && duration !== undefined) { pinDoc.mediaStart = "manual"; pinDoc.mediaStop = "manual"; pinDoc.presStartTime = 0; - pinDoc.presEndTime = pinDoc.type === DocumentType.AUDIO ? doc.duration : NumCast(doc["data-duration"]); + pinDoc.presEndTime = duration; } //save position if (pinProps?.setPosition || pinDoc.isInkMask) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 013472a04..6619205af 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -54,7 +54,7 @@ export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", _currentTimecode: "number", - displayTimecode: "number", + _timecodeToShow: "number", _currentFrame: "number", arrangeInit: ScriptField, _useClusters: "boolean", @@ -197,10 +197,10 @@ export class CollectionFreeFormView extends CollectionSubView DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); } public isCurrent(doc: Doc) { - const dispTime = NumCast(doc.displayTimecode, -1); - const endTime = NumCast(doc.undisplayTimecode, dispTime + 1.5); + const dispTime = NumCast(doc._timecodeToShow, -1); + const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); const curTime = NumCast(this.Document._currentTimecode, -1); - return dispTime === -1 || ((curTime - dispTime) >= -0.1 && curTime <= endTime); + return dispTime === -1 || ((curTime - dispTime) >= -1e-4 && curTime <= endTime); } public getActiveDocuments = () => { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 9ef37ecc2..0edbfe7a5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -426,7 +426,8 @@ export class MarqueeView extends React.Component(); _timeline: Opt; _markerStart: number = 0; - _currMarker: any; + _currAnchor: Opt; @observable static SelectingRegion: AudioBox | undefined = undefined; @observable static _scrubTime = 0; @@ -81,8 +81,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent { 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 @@ -90,17 +90,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent { + this.Document[this.fieldKey + "-duration"] = this.Document.duration; + }) + } + // onClick play scripts - AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; } + anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor.audioStart)) + anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor.audioEnd, defaultVal)) + 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; @@ -109,7 +118,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - return this.createMarker(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)); + return this.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)); } componentWillUnmount() { @@ -126,13 +135,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent 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 } @@ -148,23 +157,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - 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); + htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); this.links.map(l => { const { la1, linkTime } = this.getLinkData(l); if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { @@ -188,21 +185,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - this.playFrom(seekTimeInSeconds, endTime); + playOnClick = (anchorDoc: Doc) => { + this.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.duration)); return true; } // play back the audio from time @action - clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - if (this.layoutDoc.autoPlay) return this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); - this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + clickAnchor = (anchorDoc: Doc) => { + if (this.layoutDoc.autoPlay) return this.playOnClick(anchorDoc); + this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(anchorDoc)); return true; } // 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); @@ -217,7 +214,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent this.audioState = "playing"); - if (endTime !== this.audioDuration) { + if (endTime !== this.duration) { this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } } else { @@ -260,9 +257,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent { 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.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, 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" }); } @@ -270,7 +267,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent { 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); @@ -348,16 +345,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent { 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; + this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration; wasPaused && this.pause(); - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; + const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.duration; this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); AudioBox.SelectingRegion = this; setupMoveUpEvents(this, e, @@ -372,46 +369,46 @@ export class AudioBox extends ViewBoxAnnotatableComponent 15) && this.createMarker(this._markerStart, this._markerEnd); + AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); AudioBox.SelectingRegion = undefined; }), e => { this.props.select(false); - e.shiftKey && this.createMarker(this._ele!.currentTime); + e.shiftKey && this.createAnchor(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, + createAnchor(anchorStartTime?: number, anchorEndTime?: number) { + if (anchorStartTime === undefined) return this.rootDoc; + const anchor = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, - audioStart, - audioEnd, + anchorStartTime, + anchorEndTime, annotationOn: this.props.Document }); if (this.dataDoc[this.annotationKey]) { - this.dataDoc[this.annotationKey].push(marker); + this.dataDoc[this.annotationKey].push(anchor); } else { - this.dataDoc[this.annotationKey] = new List([marker]); + this.dataDoc[this.annotationKey] = new List([anchor]); } - return marker; + return anchor; } - // starting the drag event for marker resizing - onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { - this._currMarker = m; + // starting the drag event for anchor resizing + onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => { + this._currAnchor = m; this._left = left; this._timeline?.setPointerCapture(e.pointerId); - const toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.audioDuration, screen_delta / width * this.audioDuration)); + const toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); setupMoveUpEvents(this, e, (e) => { const rect = (e.target as any).getBoundingClientRect(); - this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); + this.changeAnchor(this._currAnchor, toTimeline(e.clientX - rect.x, rect.width)); return false; }, (e) => { @@ -422,27 +419,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.audioStart = time : marker.audioEnd = time); + changeAnchor = (anchor: Opt, time: number) => { + anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time); } - // checks if the two markers are the same with start and end time + // checks if the two anchors are the same with start and end time isSame = (m1: any, m2: any) => { - return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; + return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2); } - // makes sure no markers overlaps each other by setting the correct position and width - getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => { + // makes sure no anchors overlaps each other by setting the correct position and width + getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: 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; + const x1 = this.anchorStart(m); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); let max = 0; const overlappedLevels = new Set(placed.map(p => { - const y1 = p.audioStart; - const y2 = p.audioEnd; + const y1 = p.anchorStartTime; + const y2 = p.anchorEndTime; if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { max = Math.max(max, p.level); @@ -452,14 +448,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent= 0; j--) !overlappedLevels.has(j) && (level = j); - placed.push({ audioStart: x1, audioEnd: x2, level }); + placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); return level; } @computed get selectionContainer() { return AudioBox.SelectingRegion !== this ? (null) :
; } @@ -471,8 +467,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent; } @@ -496,17 +492,36 @@ export class AudioBox extends ViewBoxAnnotatableComponent AudioBox.LabelScript; rangePlayScript = () => AudioBox.RangePlayScript; labelPlayScript = () => AudioBox.LabelPlayScript; + + playLink = (link: Doc) => { + if (link.annotationOn === this.rootDoc) { + if (this.layoutDoc.playOnSelect) this.playFrom(this.anchorStart(link), this.anchorEnd(link)); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(link); + } + 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; + } + }); + } + 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 }); + const anchor = observable({ view: undefined as any }); return { - marker, view: marker.view = r)} + anchor, view: anchor.view = r)} Document={mark} + DataDoc={undefined} PanelWidth={() => width} PanelHeight={() => height} renderDepth={this.props.renderDepth + 1} focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutString("data")} ContainingCollectionDoc={this.props.Document} removeDocument={this.removeDocument} ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)} @@ -519,11 +534,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent }; }); - renderMarker = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + renderAnchor = 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) : + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : <>
this.onPointerDown(e, mark, true)} />
this.onPointerDown(e, mark, false)} /> @@ -535,8 +550,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent ({ level: this.getLevel(m, overlaps), marker: m })); + const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; + const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return
-
{ e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> -
+
+
{this.waveform}
- {drawMarkers.map(d => { - const m = d.marker; - const left = NumCast(m.audioStart) / this.audioDuration * timelineContentWidth; +
+
{ e.stopPropagation(); e.preventDefault(); }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> + {drawAnchors.map(d => { + const m = d.anchor; + const start = this.anchorStart(m); + const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * 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) : + const timespan = end - start; + return this.layoutDoc.hideAnchors ? (null) :
{ this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > - {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, + style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, left + AudioBox.playheadWidth, (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top, - timelineContentWidth * timespan / this.audioDuration, + timelineContentWidth * timespan / this.duration, timelineContentHeight / maxLevel)}
; })} {this.selectionContainer} -
{ e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> - {this.audio} +
{ e.stopPropagation(); e.preventDefault(); }} + style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }} + />
+ {this.audio}
{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
- {formatTime(Math.round(this.audioDuration))} + {formatTime(Math.round(this.duration))}
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 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 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 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 @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
- e.stopPropagation(); activeItem.presEndTime = Number(e.target.value); }} /> -
0 s
-
{activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} s
+
{duration / 10} s
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 12822b64a..988dd47d8 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -42,6 +42,7 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent(VideoDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } static _youtubeIframeCounter: number = 0; static Instance: VideoBox; static RangeScript: ScriptField; @@ -58,20 +59,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent = React.createRef(); private _annotationLayer: React.RefObject = React.createRef(); - _play: any = null; - _timeline: Opt; - _audioRef = React.createRef(); - _markerStart: number = 0; - _left: boolean = false; - _count: Array = []; - _duration = 0; - _start: boolean = true; - _currMarker: any; + private _play: any = null; + private _timeline: Opt; + private _audioRef = React.createRef(); + private _markerStart: number = 0; + private _left: boolean = false; + private _duration = 0; + private _start: boolean = true; + private _currAnchor: Doc | undefined; + @observable static _showControls: boolean; + @observable static SelectingRegion: VideoBox | undefined = undefined; @observable _marqueeing: number[] | undefined; @observable _savedAnnotations: Dictionary = new Dictionary(); @observable _screenCapture = false; - @observable static _showControls: boolean; - @observable static SelectingRegion: VideoBox | undefined = undefined; @observable _visible: boolean = false; @observable _markerEnd: number = 0; @observable _forceCreateYouTubeIFrame = false; @@ -79,11 +79,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent 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.createMarker(Cast(this.layoutDoc._currentTimecode, "number", null)); + return this.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)); } choosePath(url: string) { @@ -241,19 +242,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._videoStart, "number", null) : undefined, - videoStart => videoStart !== undefined && setTimeout(() => { + 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._videoStart = undefined, 10); + 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.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._videoStop, "number", null) : undefined, - videoStop => videoStop !== undefined && setTimeout(() => { + 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._videoStop = undefined, 10); + setTimeout(() => this.Document._triggerVideoStop = undefined, 10); }, this.player ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); @@ -321,7 +322,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, 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" }); } } @@ -408,7 +410,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent - +
, VideoBox._showControls ? (null) : [ //
@@ -470,16 +472,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent doc.displayTimecode = curTime); + docs.forEach(doc => doc._timecodeToShow = curTime); return this.addDocument(doc); } // play back the video from time @action - playFrom = (seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { clearTimeout(this._play); this._duration = endTime - seekTimeInSeconds; if (Number.isNaN(this.player?.duration)) { @@ -495,7 +497,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this._playing = true); - if (endTime !== this.videoDuration) { + if (endTime !== this.duration) { this._play = setTimeout(() => this.Pause(), (this._duration) * 1000); // use setTimeout to play a specific duration } } else { @@ -505,12 +507,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.layoutDoc._showTimeline = !this.layoutDoc._showTimeline + toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { this._timeline = timeline; }; + timelineRef = (timeline: HTMLDivElement) => this._timeline = timeline - // starting the drag event creating a range marker + // starting the drag event creating a range mark @action onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); @@ -520,7 +522,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { this._doubleTime = undefined; - this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; + this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration; }, 300); } @@ -537,12 +539,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent 15) && this.createMarker(this._markerStart, this._markerEnd); + VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); VideoBox.SelectingRegion = undefined; }), (e, doubleTap) => { this.props.select(false); - e.shiftKey && this.createMarker(this.player!.currentTime); + e.shiftKey && this.createAnchor(this.player!.currentTime); !wasPlaying && doubleTap && this.Play(); } , this.props.isSelected(true) || this._isChildActive); @@ -550,25 +552,27 @@ export class VideoBox extends ViewBoxAnnotatableComponent([marker]); + this.dataDoc[this.annotationKey + "-timeline"] = new List([anchor]); } - return marker; + return anchor; } @action - playOnClick = (anchorDoc: Doc, clientX: number, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + playOnClick = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlay) { if (this._playing) this.Pause(); else this.playFrom(seekTimeInSeconds, endTime); @@ -587,7 +591,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent { + clickAnchor = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this._playing) this.Pause(); else if (this.layoutDoc.autoPlay) this.Play(); @@ -602,16 +608,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent Math.max(0, Math.min(this.videoDuration, screen_delta / width * this.videoDuration)); - // starting the drag event for marker resizing - onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { - this._currMarker = m; + toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); + // starting the drag event for anchor resizing + onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => { + this._currAnchor = m; this._left = left; this._timeline?.setPointerCapture(e.pointerId); setupMoveUpEvents(this, e, (e: PointerEvent) => { const rect = (e.target as any).getBoundingClientRect(); - this.changeMarker(this._currMarker, this.toTimeline(e.clientX - rect.x, rect.width)); + this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width)); return false; }, (e: PointerEvent) => { @@ -622,14 +628,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent { + // makes sure no anchors overlaps each other by setting the correct position and width + getLevel = (m: any, placed: { anchorStartTime: number, videoEnd: number, level: number }[]) => { const timelineContentWidth = this.props.PanelWidth(); - const x1 = m.displayTimecode; - const x2 = m.undisplayTimecode === undefined ? m.displayTimecode + 10 / timelineContentWidth * this.videoDuration : m.undisplayTimecode; + const x1 = this.anchorStart(m); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); let max = 0; const overlappedLevels = new Set(placed.map(p => { - const y1 = p.videoStart; + const y1 = p.anchorStartTime; const y2 = p.videoEnd; if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { @@ -640,28 +646,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent= 0; j--) !overlappedLevels.has(j) && (level = j); - placed.push({ videoStart: x1, videoEnd: x2, level }); + placed.push({ anchorStartTime: x1, videoEnd: x2, level }); return level; } playLink = (doc: Doc) => { - const startTime = NumCast(doc.displayTimecode); - const endTime = NumCast(doc.undisplayTimecode, null); + const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow)); + const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null)); if (startTime !== undefined) { - this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this.Seek(startTime); } } - // renders the markers as a document + // renders the anchors as a document renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { - const marker = observable({ view: undefined as any }); + const anchor = observable({ view: undefined as any }); return { - marker, view: marker.view = r)} + anchor, view: anchor.view = r)} Document={mark} DataDoc={undefined} - focus={() => this.playLink(mark)} PanelWidth={() => width} PanelHeight={() => height} renderDepth={this.props.renderDepth + 1} + focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutString("data")} @@ -678,11 +685,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { + renderAnchor = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { const inner = this.renderInner(mark, script, doublescript, x, y, width, height, annotationKey); return <> {inner.view} - {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) : + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : <>
this.onPointerDown(e, mark, true)} />
this.onPointerDown(e, mark, false)} /> @@ -694,10 +701,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent ({ level: this.getLevel(m, overlaps), marker: m })); + const overlaps: { anchorStartTime: number, videoEnd: number, level: number }[] = []; + const drawAnchors: { level: number, anchor: Doc }[] = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - return !this.layoutDoc._showTimeline ? (null) : + return !this.layoutDoc._timelineShow ? (null) :
{ if (this._isChildActive || this.props.isSelected()) { @@ -709,47 +716,51 @@ export class VideoBox extends ViewBoxAnnotatableComponent - {drawMarkers.map(d => { - const m = d.marker; - const start = NumCast(m.displayTimecode, NumCast(m.displayTimecode, null)); - const left = start / this.videoDuration * timelineContentWidth; + {drawAnchors.map(d => { + const m = d.anchor; + const start = this.anchorStart(m); + const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * timelineContentWidth; const top = d.level / maxLevel * timelineContentHeight; - const timespan = m.undisplayTimecode === undefined ? 10 / timelineContentWidth * this.videoDuration : NumCast(m.undisplayTimecode) - NumCast(m.displayTimecode); - return this.layoutDoc.hideMarkers ? (null) : + const timespan = end - start; + return this.layoutDoc.hideAnchors ? (null) :
{ this.playFrom(start, Cast(m.undisplayTimecode, "number", null)); e.stopPropagation(); }} > - {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, + style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, left, top + (this.props.PanelHeight() - timelineContentHeight), - timelineContentWidth * timespan / this.videoDuration, - timelineContentHeight / maxLevel, this.annotationKey + (m.useLinkSmallAnchor ? "-timeline" : ""))} + timelineContentWidth * timespan / this.duration, + timelineContentHeight / maxLevel, this.annotationKey + (m.anchorStartTime !== undefined ? "-timeline" : ""))}
; })} {this.selectionContainer}
{ e.stopPropagation(); e.preventDefault(); }} - style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.videoDuration * 100}%`, pointerEvents: "none" }} + style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }} />
; } - // updates the marker with the new time + // updates the anchor with the new time @action - changeMarker = (m: any, time: any) => { - this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.displayTimecode = time : marker.undisplayTimecode = time); + changeAnchor = (anchor: Opt, time: number) => { + if (anchor) { + const timelineOnly = Cast(anchor.anchorStartTime, "number", null) !== undefined; + if (timelineOnly) this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time; + else this._left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + } } - // checks if the two markers are the same with start and end time + // checks if the two anchors are the same with start and end time isSame = (m1: any, m2: any) => { - return m1.displayTimecode === m2.displayTimecode && m1.undisplayTimecode === m2.undisplayTimecode; + return m1.anchorStartTime === m2.anchorStartTime && m1.anchorEndTime === m2.anchorEndTime && m1._timecodeToShow === m2._timecodeToShow && m1._timecodeToHide === m2._timecodeToHide; } // returns the blue container when dragging @computed get selectionContainer() { return VideoBox.SelectingRegion !== this ? (null) :
; } @@ -757,7 +768,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { if (e.target instanceof HTMLInputElement) return; @@ -770,7 +781,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent {this.contentFunc} @@ -838,7 +849,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent} + }
); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 7348ebdd2..e06a324d2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -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/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 67df56015..d52f3a928 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -19,10 +19,10 @@ export const documentSchema = createSchema({ lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) activeFrame: "number", // the active frame of a frame based animated document _currentTimecode: "number", // current play back time of a temporal document (video / audio) - displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) isLabel: "boolean", // whether the document is a label or not (video / audio) - audioStart: "number", // the time frame where the audio should begin playing - audioEnd: "number", // the time frame where the audio should stop playing + anchorStartTime: "number", // the time code where a document activates (eg in Audio or video timelines) + anchorEndTime: "number", // the time code where a document deactivates markers: listSpec(Doc), // list of markers for audio / video x: "number", // x coordinate when in a freeform view y: "number", // y coordinate when in a freeform view -- cgit v1.2.3-70-g09d2