From e4791f989024410443059bee424606c0567bc5f7 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 22 Mar 2021 00:30:21 -0400 Subject: fixed audio/video synchronizatoin for screengrab videos. fixed following isLinkButton's in stackedTimelines. Made links from screenGrab videos to document whn presented in lightbox. --- .../views/collections/CollectionStackedTimeline.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 16a1c02f7..cdb2468f2 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -18,6 +18,7 @@ import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; import { Transform } from "../../util/Transform"; +import { LinkManager } from "../../util/LinkManager"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -59,9 +60,7 @@ export class CollectionStackedTimeline extends CollectionSubView Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); rangeClickScript = () => CollectionStackedTimeline.RangeScript; - labelClickScript = () => CollectionStackedTimeline.LabelScript; rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; - labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript; // for creating key anchors with key events @action @@ -175,12 +172,12 @@ export class CollectionStackedTimeline extends CollectionSubView { const seekTimeInSeconds = this.anchorStart(anchorDoc); const endTime = this.anchorEnd(anchorDoc); - if (this.layoutDoc.autoPlay) { + if (this.layoutDoc.autoPlayAnchors) { if (this.props.playing()) this.props.Pause(); else this.props.playFrom(seekTimeInSeconds, endTime); } else { if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) { - if (!this.layoutDoc.autoPlay && this.props.playing()) { + if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { this.props.Play(); @@ -194,17 +191,18 @@ export class CollectionStackedTimeline extends CollectionSubView { + if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); 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.props.playing()) this.props.Pause(); - else if (this.layoutDoc.autoPlay) this.props.Play(); - else if (!this.layoutDoc.autoPlay) { + else if (this.layoutDoc.autoPlayAnchors) this.props.Play(); + else if (!this.layoutDoc.autoPlayAnchors) { const rect = this._timeline?.getBoundingClientRect(); rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); } } else { - if (this.layoutDoc.autoPlay) this.props.playFrom(seekTimeInSeconds, endTime); + if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime); else this.props.setTime(seekTimeInSeconds); } return { select: true }; @@ -276,7 +274,7 @@ export class CollectionStackedTimeline extends CollectionSubView this.props.isSelected(out) || this.props.isChildActive()} rootSelected={returnFalse} onClick={script} - onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} + onDoubleClick={this.layoutDoc.autoPlayAnchors ? undefined : doublescript} ignoreAutoHeight={false} hideResizeHandles={true} bringToFront={emptyFunction} -- cgit v1.2.3-70-g09d2 From 3c8325a99c4cec82148039d381df8377dcd4ee3a Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 22 Mar 2021 01:36:24 -0400 Subject: made stackedTimeline capable of auto showing the annotations that were made on it when played back. --- src/client/util/LinkManager.ts | 4 +- .../collections/CollectionStackedTimeline.tsx | 181 ++++++++++++++------- 2 files changed, 120 insertions(+), 65 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index dde68401e..bf973c3d6 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -100,7 +100,7 @@ export class LinkManager { // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); - public static FollowLink = (linkDoc: Opt, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => { + public static FollowLink = (linkDoc: Opt, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { const batch = UndoManager.StartBatch("follow link click"); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { @@ -131,7 +131,7 @@ export class LinkManager { docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); } }; - LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } public static traverseLink(link: Opt, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index cdb2468f2..0c960b935 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,7 +1,6 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, runInAction, reaction } from "mobx"; import { observer } from "mobx-react"; -import { computedFn } from "mobx-utils"; import { Doc, Opt, DocListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; @@ -14,11 +13,12 @@ import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionSubView } from "../collections/CollectionSubView"; -import { DocumentView, DocAfterFocusFunc } from "../nodes/DocumentView"; +import { DocumentView, DocAfterFocusFunc, DocFocusFunc, DocumentViewProps } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; import { Transform } from "../../util/Transform"; import { LinkManager } from "../../util/LinkManager"; +import { computedFn } from "mobx-utils"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -209,28 +209,6 @@ export class CollectionStackedTimeline extends CollectionSubView { - this._timeline?.setPointerCapture(e.pointerId); - const newTime = (e: PointerEvent) => { - const rect = (e.target as any).getBoundingClientRect(); - return this.toTimeline(e.clientX - rect.x, rect.width); - }; - const changeAnchor = (anchor: Doc, left: boolean, time: number) => { - const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; - if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; - return false; - }; - setupMoveUpEvents(this, e, - (e) => changeAnchor(anchor, left, newTime(e)), - (e) => { - this.props.setTime(newTime(e)); - this._timeline?.releasePointerCapture(e.pointerId); - }, - emptyFunction); - } - // 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(); @@ -252,8 +230,116 @@ export class CollectionStackedTimeline extends CollectionSubView this.currentTime; + render() { + const timelineContentWidth = this.props.PanelWidth(); + const timelineContentHeight = this.props.PanelHeight(); + const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; + const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); + const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; + const isActive = this.props.isChildActive() || this.props.isSelected(false); + return
this._timeline = timeline} + onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> + {drawAnchors.map(d => { + const start = this.anchorStart(d.anchor); + const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * timelineContentWidth; + const top = d.level / maxLevel * timelineContentHeight; + const timespan = end - start; + return this.props.Document.hideAnchors ? (null) : +
{ this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > + +
; + })} + {this.selectionContainer} +
+
; + } +} - renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { +interface StackedTinelineAnchorProps { + mark: Doc; + rangeClickScript: () => ScriptField; + rangePlayScript: () => ScriptField; + left: number; + top: number; + width: number; + height: number; + toTimeline: (screen_delta: number, width: number) => number; + playLink: (linkDoc: Doc) => void; + setTime: (time: number) => void; + isChildActive: () => boolean; + startTag: string; + endTag: string; + renderDepth: number; + layoutDoc: Doc; + ScreenToLocalTransform: () => Transform; + _timeline: HTMLDivElement | null; + focus: DocFocusFunc; + currentTimecode: () => number; + isSelected: (outsideReaction?: boolean) => boolean; + stackedTimeline: CollectionStackedTimeline; +} +@observer +class StackedTimelineAnchor extends React.Component { + _lastTimecode: number; + _disposer: IReactionDisposer | undefined; + constructor(props: any) { + super(props); + this._lastTimecode = this.props.currentTimecode(); + } + componentDidMount() { + this._disposer = reaction(() => this.props.currentTimecode(), + (time) => { + if (DocListCast(this.props.mark.links).length && + time > NumCast(this.props.mark[this.props.startTag]) && + time < NumCast(this.props.mark[this.props.endTag]) && + this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { + LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true); + } + this._lastTimecode = time; + }); + } + componentWillUnmount() { + this._disposer?.(); + } + // starting the drag event for anchor resizing + onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { + this.props._timeline?.setPointerCapture(e.pointerId); + const newTime = (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + return this.props.toTimeline(e.clientX - rect.x, rect.width); + }; + const changeAnchor = (anchor: Doc, left: boolean, time: number) => { + const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; + if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); + else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + return false; + }; + setupMoveUpEvents(this, e, + (e) => changeAnchor(anchor, left, newTime(e)), + (e) => { + this.props.setTime(newTime(e)); + this.props._timeline?.releasePointerCapture(e.pointerId); + }, + emptyFunction); + } + renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { const anchor = observable({ view: undefined as any }); const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { this.props.playLink(mark); @@ -274,54 +360,23 @@ export class CollectionStackedTimeline extends CollectionSubView this.props.isSelected(out) || this.props.isChildActive()} rootSelected={returnFalse} onClick={script} - onDoubleClick={this.layoutDoc.autoPlayAnchors ? undefined : doublescript} + onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} ignoreAutoHeight={false} hideResizeHandles={true} bringToFront={emptyFunction} - scriptContext={this} /> + scriptContext={this.props.stackedTimeline} /> }; }); - renderAnchor = computedFn(function (this: CollectionStackedTimeline, 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); + render() { + const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height); return <> {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : <> -
this.onAnchorDown(e, mark, true)} /> -
this.onAnchorDown(e, mark, false)} /> +
this.onAnchorDown(e, this.props.mark, true)} /> +
this.onAnchorDown(e, this.props.mark, false)} /> } ; - }); - - render() { - const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight(); - const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; - const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); - const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - const isActive = this.props.isChildActive() || this.props.isSelected(false); - return
this._timeline = timeline} - onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> - {drawAnchors.map(d => { - const start = this.anchorStart(d.anchor); - const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); - const left = start / this.duration * timelineContentWidth; - const top = d.level / maxLevel * timelineContentHeight; - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : -
{ this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > - {this.renderAnchor(d.anchor, this.rangeClickScript, this.rangePlayScript, - left, - top, - timelineContentWidth * timespan / this.duration, - timelineContentHeight / maxLevel)} -
; - })} - {this.selectionContainer} -
-
; } } Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file -- cgit v1.2.3-70-g09d2 From d8445ef3b2402530b5139b94eeb0f333c1fed661 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 22 Mar 2021 14:55:45 -0400 Subject: adjusted playLink in videos to start 1/4 sec early to trigger following link to anchor. changed text sidebars to show titles. fixed title autoHeight computations in formattedTextBox. fixed styleprovider to compute headerMargin correctly by inquiring style for ShowTitle. --- src/client/views/SidebarAnnos.tsx | 8 +++++++- src/client/views/StyleProvider.tsx | 4 +++- .../collections/CollectionStackedTimeline.tsx | 4 ++-- src/client/views/nodes/VideoBox.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 22 ++++++++++++---------- 5 files changed, 25 insertions(+), 15 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 2d49938a9..9d085e2b6 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -1,6 +1,6 @@ import { computed } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, StrListCast } from "../../fields/Doc"; +import { Doc, DocListCast, StrListCast, Opt } from "../../fields/Doc"; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; @@ -15,6 +15,7 @@ import { SearchBox } from './search/SearchBox'; import "./SidebarAnnos.scss"; import { StyleProp } from './StyleProvider'; import React = require("react"); +import { DocumentViewProps } from './nodes/DocumentView'; interface ExtraProps { fieldKey: string; @@ -71,6 +72,10 @@ export class SidebarAnnos extends React.Component { removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey()); docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; + sidebarStyleProvider = (doc: Opt, props: Opt, property: string) => { + if (property === StyleProp.ShowTitle) return "title"; + return this.props.styleProvider?.(doc, props, property) + } render() { const renderTag = (tag: string) => { const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); @@ -94,6 +99,7 @@ export class SidebarAnnos extends React.Component { PanelWidth={this.panelWidth} xMargin={0} yMargin={0} + styleProvider={this.sidebarStyleProvider} docFilters={this.docFilters} scaleField={this.sidebarKey() + "-scale"} isAnnotationOverlay={false} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 1ee99817c..617652ab7 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -69,6 +69,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt StrListCast(doc?._layerTags).includes(StyleLayers.Background); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); + const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); switch (property.split(":")[0]) { case StyleProp.DocContents: return undefined; @@ -88,7 +89,8 @@ export function DefaultStyleProvider(doc: Opt, props: Opt { - const seekTimeInSeconds = this.anchorStart(anchorDoc); + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlayAnchors) { if (this.props.playing()) this.props.Pause(); @@ -192,7 +192,7 @@ export class CollectionStackedTimeline extends CollectionSubView { if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); - const seekTimeInSeconds = this.anchorStart(anchorDoc); + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this.props.playing()) this.props.Pause(); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 42258168c..5fd897629 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -493,7 +493,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { - const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0; + const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0) - .25); const endTime = this._stackedTimeline.current?.anchorEnd(doc); if (startTime !== undefined) { if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ada409081..0c0b09b9a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -759,8 +759,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.props.contentsActive?.(this.active); this._cachedLinks = DocListCast(this.Document.links); this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight()); - this._disposers.autoHeight = reaction(() => ({ scrollHeight: this.scrollHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight }) => width && this.autoHeight && this.resetNativeHeight(scrollHeight) + this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight) ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight }), @@ -1393,14 +1393,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } tryUpdateScrollHeight() { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const proseHeight = this.ProseRef?.scrollHeight || 0; - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { - setScrollHeight(); - } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - } + setTimeout(() => { // bcz: don't know why this is needed, but without it, the size of the textbox is too big as it includes the size of the title header. after the timeout, the size seems to get computed correctly. + const proseHeight = this.ProseRef?.scrollHeight || 0; + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... + } + }); } } fitToBox = () => this.props.Document._fitToBox; -- cgit v1.2.3-70-g09d2 From d0515c81be9f4292eaf165762ce15e7bc8d1737a Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 25 Mar 2021 02:39:52 -0400 Subject: moved dictation view to be a component of the screenshotbox --- src/client/util/CurrentUserUtils.ts | 4 +- src/client/util/LinkManager.ts | 6 +- src/client/views/LightboxView.tsx | 8 +-- .../collections/CollectionStackedTimeline.tsx | 3 +- src/client/views/nodes/ScreenshotBox.tsx | 77 ++++++++++++++++------ src/client/views/nodes/VideoBox.tsx | 4 +- src/fields/util.ts | 2 +- 7 files changed, 70 insertions(+), 34 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 05e560f51..0fb32970a 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -423,7 +423,7 @@ export class CurrentUserUtils { ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyScreenshot === undefined) { - doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List(["system"]) }); + doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _fitWidth: true, _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List(["system"]) }); } if (doc.emptyAudio === undefined) { doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List(["system"]) }); @@ -453,7 +453,7 @@ export class CurrentUserUtils { { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true }, { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc }, { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, - { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc }, + { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc, noviceMode: true }, { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc }, { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index bd27c9e56..159011516 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -135,14 +135,14 @@ export class LinkManager { // follows a link - if the target is on screen, it highlights/pans to it. - // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place + // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); public static FollowLink = (linkDoc: Opt, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { const batch = UndoManager.StartBatch("follow link click"); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { const createTabForTarget = (didFocus: boolean) => new Promise(res => { - const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation) || followLoc; + const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc); docViewProps.addDocTab(doc, where); setTimeout(() => { const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); @@ -195,7 +195,7 @@ export class LinkManager { const containerDoc = Cast(target.annotationOn, Doc, null) || target; const targetContext = Cast(containerDoc?.context, Doc, null); const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished); } } else { finished?.(); diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 5715b62b0..84738112f 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -114,11 +114,11 @@ export class LightboxView extends React.Component { @action public static Next() { const doc = LightboxView._doc!; const target = LightboxView._docTarget = LightboxView._future?.pop(); - const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); - if (docView && target) { - const l = DocUtils.MakeLinkToActiveAudio(target); + const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (targetDocView && target) { + const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target); l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen"); - docView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); + targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); } else { if (!target && LightboxView.path.length) { diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index de75a3c4a..db02ab986 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -19,6 +19,7 @@ import "./CollectionStackedTimeline.scss"; import { Transform } from "../../util/Transform"; import { LinkManager } from "../../util/LinkManager"; import { computedFn } from "mobx-utils"; +import { LightboxView } from "../LightboxView"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -306,7 +307,7 @@ class StackedTimelineAnchor extends React.Component componentDidMount() { this._disposer = reaction(() => this.props.currentTimecode(), (time) => { - if (DocListCast(this.props.mark.links).length && + if (!LightboxView.LightboxDoc && DocListCast(this.props.mark.links).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index dba730d12..d1da2fcd5 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -24,6 +24,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; import { TraceMobx } from "../../../fields/util"; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has } @@ -40,6 +41,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent { const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", @@ -59,6 +64,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent { + if (this.dataDoc[this.fieldKey + "-dictation"]) return; const dictationText = CurrentUserUtils.GetNewTextDoc("", NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); @@ -133,32 +142,58 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent [this.content]; + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()) render() { TraceMobx(); return
- - {this.contentFunc} - +
+ + {this.contentFunc} +
+
+ +
{!this.props.isSelected() ? (null) :
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 5a60f9312..4e03589d6 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -182,7 +182,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { const url = this.choosePath(Utils.prepend(relative)); - const width = this.layoutDoc._width || 0; + const width = this.layoutDoc._width || 1; const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), @@ -548,7 +548,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * this.heightPercent / 100; + panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : 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); diff --git a/src/fields/util.ts b/src/fields/util.ts index 6c9c9d45c..ea91cc057 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -19,7 +19,7 @@ function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -const tracing = true; +const tracing = false; export function TraceMobx() { tracing && trace(); } -- cgit v1.2.3-70-g09d2 From 5db728e099e25a807f69b316d813f662daf3eb7b Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 25 Mar 2021 13:29:58 -0400 Subject: fixed videos in lightbox to not auto-follow links. fixed audiotags to higlight more clearly when target of a link follow. --- src/client/views/collections/CollectionStackedTimeline.tsx | 6 +++--- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index db02ab986..2dcdf58c2 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -273,7 +273,7 @@ export class CollectionStackedTimeline extends CollectionSubView ScriptField; rangePlayScript: () => ScriptField; @@ -297,7 +297,7 @@ interface StackedTinelineAnchorProps { stackedTimeline: CollectionStackedTimeline; } @observer -class StackedTimelineAnchor extends React.Component { +class StackedTimelineAnchor extends React.Component { _lastTimecode: number; _disposer: IReactionDisposer | undefined; constructor(props: any) { @@ -307,7 +307,7 @@ class StackedTimelineAnchor extends React.Component componentDidMount() { this._disposer = reaction(() => this.props.currentTimecode(), (time) => { - if (!LightboxView.LightboxDoc && DocListCast(this.props.mark.links).length && + if (!Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc) && DocListCast(this.props.mark.links).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2a1cc854c..369f5ab39 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -762,9 +762,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; - addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" }); + addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow", transform: "scale(3)", "transform-origin": "left bottom" }); setTimeout(() => this._focusSpeed = undefined, this._focusSpeed); - setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 1500)); + setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); } } -- cgit v1.2.3-70-g09d2 From c7619302a639c61096249362167e75dca6dbb376 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 25 Mar 2021 15:41:06 -0400 Subject: added a dictation area to the stackedTimeline view for A/V. Now dictation annotations scroll in place. --- src/client/documents/Documents.ts | 2 +- src/client/views/LightboxView.tsx | 2 +- .../collections/CollectionStackedTimeline.tsx | 29 +++++++++++++++++++--- src/client/views/nodes/ScreenshotBox.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 3 +++ .../views/nodes/formattedText/FormattedTextBox.tsx | 3 ++- 6 files changed, 34 insertions(+), 7 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4e05793d4..9406b374e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1051,7 +1051,7 @@ export namespace DocUtils { export function MakeLinkToActiveAudio(doc: Doc, broadcastEvent = true) { broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1); return DocUtils.ActiveRecordings.map(audio => - DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")).lastElement(); + DocUtils.MakeLink({ doc: doc }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline")); } export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 84738112f..07ebe5fa4 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -116,7 +116,7 @@ export class LightboxView extends React.Component { const target = LightboxView._docTarget = LightboxView._future?.pop(); const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); if (targetDocView && target) { - const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target); + const l = DocUtils.MakeLinkToActiveAudio(targetDocView.ComponentView?.getAnchor?.() || target).lastElement(); l && (Cast(l.anchor2, Doc, null).backgroundColor = "lightgreen"); targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 2dcdf58c2..66b74277b 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -7,7 +7,7 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent } from "../../../Utils"; +import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent, returnOne } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; @@ -20,6 +20,7 @@ import { Transform } from "../../util/Transform"; import { LinkManager } from "../../util/LinkManager"; import { computedFn } from "mobx-utils"; import { LightboxView } from "../LightboxView"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -234,7 +235,8 @@ export class CollectionStackedTimeline extends CollectionSubView this.currentTime; render() { const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight(); + const timelineContentHeight = this.props.PanelHeight() * 2 / 3; + const dictationHeight = this.props.PanelHeight() / 3; const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; @@ -268,6 +270,25 @@ export class CollectionStackedTimeline extends CollectionSubView; })} {this.selectionContainer} +
+ dictationHeight} + isAnnotationOverlay={true} + select={emptyFunction} + active={returnFalse} + scaling={returnOne} + xMargin={25} + yMargin={10} + whenActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1}> + +
+
; } @@ -307,7 +328,9 @@ class StackedTimelineAnchor extends React.Component componentDidMount() { this._disposer = reaction(() => this.props.currentTimecode(), (time) => { - if (!Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc) && DocListCast(this.props.mark.links).length && + const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null); + const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); + if ((isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc)) && DocListCast(this.props.mark.links).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index d1da2fcd5..6c019d6f2 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -117,7 +117,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent
, +
+ +
, VideoBox._showControls ? (null) : [ //
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 369f5ab39..aaf3a938e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -667,7 +667,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (this._break) { const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" }); this.addDocument(textanchor); - const link = DocUtils.MakeLinkToActiveAudio(textanchor, false); + const link = DocUtils.MakeLinkToActiveAudio(textanchor, false).lastElement(); + link && (Doc.GetProto(link).isDictation = true); if (!link) return; const audioanchor = Cast(link.anchor2, Doc, null); if (!audioanchor) return; -- cgit v1.2.3-70-g09d2 From 36028677c61be7ab5937e864e881fa91a3b82d8c Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 26 Mar 2021 13:22:13 -0400 Subject: extracted AudioWaveform out of AudioBox in order to use in CollectionStackedTimelineView to share between audioBox and videoBox. --- src/client/util/DictationManager.ts | 2 +- src/client/util/DragManager.ts | 2 +- src/client/util/LinkManager.ts | 20 +++--- src/client/views/AudioWaveform.scss | 17 +++++ src/client/views/AudioWaveform.tsx | 61 +++++++++++++++++ src/client/views/DocumentDecorations.tsx | 2 +- src/client/views/SidebarAnnos.tsx | 2 +- .../collections/CollectionStackedTimeline.scss | 7 ++ .../collections/CollectionStackedTimeline.tsx | 80 +++++++++++++--------- src/client/views/nodes/AudioBox.scss | 18 ----- src/client/views/nodes/AudioBox.tsx | 41 +---------- src/client/views/nodes/DocumentView.tsx | 8 +-- src/client/views/nodes/ScreenshotBox.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 9 +-- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- src/server/DashUploadUtils.ts | 5 +- 16 files changed, 165 insertions(+), 113 deletions(-) create mode 100644 src/client/views/AudioWaveform.scss create mode 100644 src/client/views/AudioWaveform.tsx (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index f00cdce1e..a93b2f573 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -89,7 +89,7 @@ export namespace DictationManager { export const listen = async (options?: Partial) => { if (pendingListen instanceof Promise) return pendingListen.then(pl => innerListen(options)); return innerListen(options); - } + }; const innerListen = async (options?: Partial) => { let results: string | undefined; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1809a77bf..38d0ecaa6 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -388,7 +388,7 @@ export namespace DragManager { if (dragElement !== ele) { const children = [Array.from(ele.children), Array.from(dragElement.children)]; while (children[0].length) { - const childs = [children[0].pop(), children[1].pop()] + const childs = [children[0].pop(), children[1].pop()]; if (childs[0]?.children) { children[0].push(...Array.from(childs[0].children)); children[1].push(...Array.from(childs[1]!.children)); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 159011516..3c3d5c3b8 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -33,7 +33,7 @@ export class LinkManager { constructor() { LinkManager._instance = this; setTimeout(() => { - LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase as Doc)]; + LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)]; const addLinkToDoc = (link: Doc): any => { const a1 = link?.anchor1; const a2 = link?.anchor2; @@ -43,7 +43,7 @@ export class LinkManager { Doc.GetProto(a2)[DirectLinksSym].add(link); Doc.GetProto(link)[DirectLinksSym].add(link); } - } + }; const remLinkFromDoc = (link: Doc): any => { const a1 = link?.anchor1; const a2 = link?.anchor2; @@ -53,23 +53,23 @@ export class LinkManager { Doc.GetProto(a2)[DirectLinksSym].delete(link); Doc.GetProto(link)[DirectLinksSym].delete(link); } - } + }; const watchUserLinks = (userLinks: List) => { const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields observe(userLinks, change => { - switch (change.type) { + switch (change.type as any) { case "splice": (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); break; - case "update": let oldValue = change.oldValue; + case "update": //let oldValue = change.oldValue; } }, true); - } + }; observe(LinkManager.userDocs, change => { - switch (change.type) { + switch (change.type as any) { case "splice": (change as any).added.forEach(watchUserLinks); break; - case "update": let oldValue = change.oldValue; + case "update": //let oldValue = change.oldValue; } }, true); }); @@ -82,7 +82,9 @@ export class LinkManager { public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor + public getAllDirectLinks(anchor: Doc): Doc[] { + return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); + } // finds all links that contain the given anchor public getAllLinks(): Doc[] { return []; }//this.allLinks(); } // allLinks = computedFn(function allLinks(this: any): Doc[] { diff --git a/src/client/views/AudioWaveform.scss b/src/client/views/AudioWaveform.scss new file mode 100644 index 000000000..e20434a25 --- /dev/null +++ b/src/client/views/AudioWaveform.scss @@ -0,0 +1,17 @@ +.audioWaveform { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + z-index: -1000; + bottom: 0; + pointer-events: none; + div { + height: 100% !important; + width: 100% !important; + } + canvas { + height: 100% !important; + width: 100% !important; + } +} diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx new file mode 100644 index 000000000..7ff9c1071 --- /dev/null +++ b/src/client/views/AudioWaveform.tsx @@ -0,0 +1,61 @@ +import React = require("react"); +import axios from "axios"; +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import Waveform from "react-audio-waveform"; +import { Doc } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; +import { Cast } from "../../fields/Types"; +import { numberRange } from "../../Utils"; +import "./AudioWaveform.scss"; + +export interface AudioWaveformProps { + duration: number; + mediaPath: string; + dataDoc: Doc; + PanelHeight: () => number; +} + +@observer +export class AudioWaveform extends React.Component { + public static NUMBER_OF_BUCKETS = 100; + @computed get _waveHeight() { return Math.max(50, this.props.PanelHeight()); } + componentDidMount() { + const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); + if (!audioBuckets.length) { + this.props.dataDoc.audioBuckets = new List([0, 0]); /// "lock" to prevent other views from computing the same data + setTimeout(this.createWaveformBuckets); + } + } + // decodes the audio file into peaks for generating the waveform + createWaveformBuckets = async () => { + axios({ url: this.props.mediaPath, responseType: "arraybuffer" }) + .then(response => { + const context = new window.AudioContext(); + context.decodeAudioData(response.data, + action(buffer => { + const decodedAudioData = buffer.getChannelData(0); + const bucketDataSize = Math.floor(decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS); + const brange = Array.from(Array(bucketDataSize)); + this.props.dataDoc.audioBuckets = new List( + numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map((i: number) => + brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); + })); + }); + } + + render() { + const audioBuckets = Cast(this.props.dataDoc.audioBuckets, listSpec("number"), []); + return
+ +
; + } +} \ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index da50ba4d8..a92891ee5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -94,7 +94,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); this._edtingTitle = true; - this._keyinput.current && setTimeout(this._keyinput.current!.focus); + this._keyinput.current && setTimeout(this._keyinput.current.focus); })); } diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 87887483f..6b0b928b3 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -79,7 +79,7 @@ export class SidebarAnnos extends React.Component { sidebarStyleProvider = (doc: Opt, props: Opt, property: string) => { if (property === StyleProp.ShowTitle) return StrCast(this.props.layoutDoc["sidebar-childShowTitle"], "title"); - return this.props.styleProvider?.(doc, props, property) + return this.props.styleProvider?.(doc, props, property); } render() { const renderTag = (tag: string) => { diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index cc56831f3..2b9f3d782 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -58,4 +58,11 @@ left: 0; } } + + .collectionStackedTimeline-waveform { + position: absolute; + width: 100%; + top: 0; + left: 0; + } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 66b74277b..2191a3df5 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,26 +1,26 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, runInAction, reaction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast } from "../../../fields/Doc"; +import { computedFn } from "mobx-utils"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent, returnOne } from "../../../Utils"; +import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from "../../../Utils"; import { Docs } from "../../documents/Documents"; +import { LinkManager } from "../../util/LinkManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; +import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; -import { DocumentView, DocAfterFocusFunc, DocFocusFunc, DocumentViewProps } from "../nodes/DocumentView"; +import { LightboxView } from "../LightboxView"; +import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; -import { Transform } from "../../util/Transform"; -import { LinkManager } from "../../util/LinkManager"; -import { computedFn } from "mobx-utils"; -import { LightboxView } from "../LightboxView"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -35,6 +35,7 @@ export type CollectionStackedTimelineProps = { isChildActive: () => boolean; startTag: string; endTag: string; + mediaPath: string; }; @observer @@ -232,11 +233,42 @@ export class CollectionStackedTimeline extends CollectionSubView this.props.PanelHeight() / 3; + timelineContentHeight = () => this.props.PanelHeight() * 2 / 3; + @computed get renderDictation() { + const dictation = Cast(this.dataDoc[this.props.fieldKey.replace("annotations", "dictation")], Doc, null); + return !dictation ? (null) :
+ + +
; + } + @computed get renderAudioWaveform() { + return !this.props.mediaPath ? (null) :
+ +
; + } currentTimecode = () => this.currentTime; render() { const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight() * 2 / 3; - const dictationHeight = this.props.PanelHeight() / 3; const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; @@ -247,11 +279,11 @@ export class CollectionStackedTimeline extends CollectionSubView { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > ; })} {this.selectionContainer} -
- dictationHeight} - isAnnotationOverlay={true} - select={emptyFunction} - active={returnFalse} - scaling={returnOne} - xMargin={25} - yMargin={10} - whenActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - -
+ {this.renderAudioWaveform} + {this.renderDictation}
; diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index fc881ca25..3fcb024df 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -144,24 +144,6 @@ border-radius: 3px; z-index: 1000; overflow: hidden; - - .waveform { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - z-index: -1000; - bottom: 0; - pointer-events: none; - div { - height: 100% !important; - width: 100% !important; - } - canvas { - height: 100% !important; - width: 100% !important; - } - } } .audioBox-total-time, diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 2f7a6cfd8..06a27c22a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,18 +1,15 @@ import React = require("react"); 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 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 { makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -35,7 +32,6 @@ const AudioDocument = makeInterface(documentSchema); export class AudioBox extends ViewBoxAnnotatableComponent(AudioDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; - public static NUMBER_OF_BUCKETS = 100; static playheadWidth = 30; // width of playhead static heightPercent = 80; // height of timeline in percent of height of audioBox. static Instance: AudioBox; @@ -295,35 +291,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent this.createWaveformBuckets()); - return ; - } - - // decodes the audio file into peaks for generating the waveform - createWaveformBuckets = async () => { - axios({ url: this.path, responseType: "arraybuffer" }) - .then(response => (new (window.AudioContext)()).decodeAudioData(response.data, - action(buffer => { - const decodedAudioData = buffer.getChannelData(0); - const bucketDataSize = Math.floor(decodedAudioData.length / AudioBox.NUMBER_OF_BUCKETS); - const brange = Array.from(Array(bucketDataSize)); - this.dataDoc.audioBuckets = new List( - numberRange(AudioBox.NUMBER_OF_BUCKETS).map(i => - brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2)); - })) - ); - } - playing = () => this.mediaState === "playing"; playLink = (link: Doc) => { const stack = this._stackedTimeline.current; @@ -353,6 +320,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent
-
- {this.waveform} -
{this.renderTimeline}
{this.audio} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index df769a407..8a2a755ac 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1102,16 +1102,16 @@ export class DocumentView extends React.Component { render() { TraceMobx(); - const xshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; - const yshift = this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); return (
{!this.props.Document || !this.props.PanelWidth() ? (null) : (
[this.content]; videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth(); - formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()) + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { TraceMobx(); return
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 4e03589d6..da05b0c13 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -297,9 +297,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent Not supported. - {!this.audiopath ? (null) : + {!this.audiopath || this.audiopath === field.url.href ? (null) :