From b3e2520f81a9956320b1aeba14ea880aa2887b15 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 21 Jan 2021 10:22:51 -0500 Subject: fixed fitWidth for WebBox --- src/client/views/collections/CollectionSubView.tsx | 3 +-- src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 26cb4d156..287bc56c2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -268,7 +268,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - this.addDocument(Docs.Create.WebDocument(href, { ...options, _fitWidth: true, title: href })); + this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 })); @@ -385,7 +385,6 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, - _fitWidth: true, title: uriList.split("#annotations:")[0], _width: 400, _height: 512, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 258e839d0..9ef37ecc2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -89,7 +89,7 @@ export class MarqueeView extends React.Component this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _fitWidth: true, _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); -- 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/collections') 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 d0025200b23f57ec25d3452d425de8725a032c6a Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 22 Jan 2021 21:14:37 -0500 Subject: fixed selection bounds for video box annotations, especially when in full screen view. --- src/client/views/collections/TabDocView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 6 +- src/client/views/nodes/AudioBox.tsx | 18 ++-- src/client/views/nodes/DocumentView.tsx | 6 +- src/client/views/nodes/VideoBox.scss | 20 ++-- src/client/views/nodes/VideoBox.tsx | 113 +++++++++++---------- 6 files changed, 88 insertions(+), 77 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 5ca069fb9..e5f05c407 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -358,7 +358,7 @@ export class TabDocView extends React.Component { } active = () => this._isActive; ScreenToLocalTransform = () => { - const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0]?.firstChild as HTMLElement); + const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY); } PanelWidth = () => this._panelWidth; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bc86ecd19..013472a04 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -137,11 +137,11 @@ export class CollectionFreeFormView extends CollectionSubView dispTime && curTime < endTime); + return dispTime === -1 || ((curTime - dispTime) >= -0.1 && curTime <= endTime); } public getActiveDocuments = () => { diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 4ddb0502b..6ebd16bf3 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -91,10 +91,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); this.playFrom(seekTimeInSeconds, endTime); + return true; } // play back the audio from time @action clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); - else { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); - this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); - } + if (this.layoutDoc.playOnClick) return this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); + this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + return true; } // play back the audio from time @action diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 6217f473f..e3da48749 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -410,7 +410,7 @@ export class DocumentViewInternal extends DocComponent func().result === true ? this.props.select(false) : "", "on click"); } else func(); }; if (this.onDoubleClickHandler) { @@ -935,7 +935,9 @@ export class DocumentView extends React.Component { PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; ContentScale = () => this.nativeScaling; - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + screenToLocalTransform = () => { + return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + } componentDidMount() { !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 8bba5d1ff..19f605278 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -183,14 +183,18 @@ pointer-events:all; } -// .timeline-button { -// position: absolute; -// bottom: 35px; -// right: 235px; -// color: lightgrey; -// width: 20px; - -// } +.timeline-button { + position: absolute; + display: flex; + align-items: center; + z-index: 1010; + bottom: 35px; + right: 235px; + color: white; + background: dimgrey; + width: 20px; + height: 20px; +} .videoBox-play { width: 25px; height: 20px; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 608d7daa3..c5e61eedd 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -49,7 +49,7 @@ 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(); @@ -68,7 +66,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent = []; _duration = 0; _start: boolean = true; - private _currMarker: any; + _currMarker: any; + @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; @@ -76,7 +78,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { @@ -311,7 +313,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { @@ -337,10 +339,9 @@ export class VideoBox extends ViewBoxAnnotatableComponentLoading
:
-
+
,
, @@ -520,9 +517,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.layoutDoc._showTimeline = !this.layoutDoc._showTimeline // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { - this._timeline = timeline; - } + timelineRef = (timeline: HTMLDivElement) => { this._timeline = timeline; } // starting the drag event creating a range marker @action @@ -579,18 +574,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); this.playFrom(seekTimeInSeconds, endTime); + return true; // select } // play back the video from time @action clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { - if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); - else { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); - this.player && (this.player.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); - } + if (this.layoutDoc.playOnClick) return this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); + this.player && (this.player.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + return true; // select } // starting the drag event for marker resizing @@ -644,12 +637,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent width} PanelHeight={() => height} + renderDepth={this.props.renderDepth + 1} rootSelected={returnFalse} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutString("data")} ContainingCollectionDoc={this.props.Document} removeDocument={(doc: Doc | Doc[]) => this.removeDocument(doc, annotationKey)} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).translate(-x - 4, -y - 3)} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(-x, -y)} parentActive={(out) => this.props.isSelected(out) || this._isChildActive} whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} onClick={script} @@ -675,13 +669,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent ({ 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) : -
{ if (this._isChildActive || this.props.isSelected()) { e.stopPropagation(); e.preventDefault(); } }} +
{ + 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); @@ -758,7 +756,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent VideoBox.RangePlayScript; labelPlayScript = () => VideoBox.LabelPlayScript; - screenToLocalTransform = () => this.props.ScreenToLocalTransform(); contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; @computed get annotationLayer() { @@ -774,33 +771,43 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.props.scaling?.() || 1; + panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; + panelHeight = () => this.props.PanelHeight() * this.heightPercent / 100; + screenToLocalTransform = () => { + const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); + return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); + } + render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; + const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; return (
-
- 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} - addDocument={this.addDocumentWithTimestamp} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - {this.contentFunc} - +
+
+ + {this.contentFunc} + +
{this.uIButtons} {this.annotationLayer} {this.renderTimeline} -- 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/collections') 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 From eb2e88ef810eed9c1d31b3b2fdc3ba848f067c53 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 26 Jan 2021 20:12:55 -0500 Subject: made StackedTimeline a collectionview and renamed CollectionStackedTimeline. Now timeline anchors will observe filters. --- src/client/views/GlobalKeyHandler.ts | 6 +- .../collections/CollectionStackedTimeline.scss | 373 ++++++++++++++++++++ .../collections/CollectionStackedTimeline.tsx | 324 ++++++++++++++++++ src/client/views/collections/CollectionView.tsx | 3 +- src/client/views/nodes/AudioBox.tsx | 49 ++- src/client/views/nodes/StackedTimeline.scss | 374 --------------------- src/client/views/nodes/StackedTimeline.tsx | 335 ------------------ src/client/views/nodes/VideoBox.tsx | 27 +- 8 files changed, 752 insertions(+), 739 deletions(-) create mode 100644 src/client/views/collections/CollectionStackedTimeline.scss create mode 100644 src/client/views/collections/CollectionStackedTimeline.tsx delete mode 100644 src/client/views/nodes/StackedTimeline.scss delete mode 100644 src/client/views/nodes/StackedTimeline.tsx (limited to 'src/client/views/collections') diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index a07ba0a77..e56ba38dd 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -24,7 +24,7 @@ import { DocumentDecorations } from "./DocumentDecorations"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; -import { StackedTimeline } from "./nodes/StackedTimeline"; +import { CollectionStackedTimeline } from "./collections/CollectionStackedTimeline"; import { AnchorMenu } from "./pdf/AnchorMenu"; import { SearchBox } from "./search/SearchBox"; @@ -121,8 +121,8 @@ export class KeyManager { DragManager.AbortDrag(); } else if (CollectionDockingView.Instance.HasFullScreen) { CollectionDockingView.Instance.CloseFullScreen(); - } else if (StackedTimeline.SelectingRegion) { - StackedTimeline.SelectingRegion = undefined; + } else if (CollectionStackedTimeline.SelectingRegion) { + CollectionStackedTimeline.SelectingRegion = undefined; doDeselect = false; } else { doDeselect = !ContextMenu.Instance.closeMenu(); diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss new file mode 100644 index 000000000..1bb5bc720 --- /dev/null +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -0,0 +1,373 @@ +.audiobox-container, +.audiobox-container-interactive { + width: 100%; + height: 100%; + position: inherit; + display: flex; + position: relative; + cursor: default; + + .audiobox-inner { + width:100%; + height: 100%; + } + + .audiobox-buttons { + display: flex; + width: 100%; + align-items: center; + height: 100%; + + .audiobox-dictation { + position: relative; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: dimgray; + left: 0px; + } + + .audiobox-dictation:hover { + color: white; + cursor: pointer; + } + } + + .audiobox-handle { + width: 20px; + height: 100%; + display: inline-block; + } + + .audiobox-control, + .audiobox-control-interactive { + top: 0; + max-height: 32px; + width: 100%; + display: inline-block; + pointer-events: none; + } + + .audiobox-control-interactive { + pointer-events: all; + } + + .audiobox-record { + pointer-events: all; + width: 100%; + height: 100%; + position: relative; + pointer-events: none; + } + + .audiobox-record-interactive { + pointer-events: all; + width: 100%; + height: 100%; + position: relative; + + + } + + .recording { + margin-top: auto; + margin-bottom: auto; + width: 100%; + height: 100%; + position: relative; + padding-right: 5px; + display: flex; + background-color: red; + + .time { + position: relative; + height: 100%; + width: 100%; + font-size: 20; + text-align: center; + top: 5; + } + + .buttons { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 5px; + } + + .buttons:hover { + background-color: crimson; + } + } + + .audiobox-controls { + width: 100%; + height: 100%; + position: relative; + display: flex; + padding-left: 2px; + background: black; + + .audiobox-dictation { + position: absolute; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: dimgray; + left: 0px; + } + + .audiobox-player { + margin-top: auto; + margin-bottom: auto; + width: 100%; + position: relative; + padding-right: 5px; + display: flex; + + .audiobox-playhead { + position: relative; + margin-top: auto; + margin-bottom: auto; + margin-right: 2px; + height: 25px; + padding: 2px; + border-radius: 50%; + background-color: black; + color: white; + } + + .audiobox-playhead:hover { + // background-color: black; + // border-radius: 5px; + background-color: grey; + color: lightgrey; + } + + .audiobox-dictation { + position: relative; + margin-top: auto; + margin-bottom: auto; + width: 25px; + padding: 2px; + align-items: center; + display: inherit; + background: dimgray; + } + + .audiobox-timeline { + position: absolute; + width: 100%; + border: gray solid 1px; + border-radius: 3px; + z-index: 1000; + overflow: hidden; + + .audiobox-container { + position: absolute; + width: 10px; + top: 2.5%; + height: 0px; + background: lightblue; + border-radius: 5px; + // box-shadow: black 2px 2px 1px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: darkblue; + border-width: 1px; + } + + .audiobox-current { + width: 1px; + height: 100%; + background-color: red; + position: absolute; + top: 0px; + } + + .waveform { + position: relative; + width: 100%; + 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-linker, + .audiobox-linker-mini { + position: absolute; + width: 15px; + min-height: 10px; + height: 15px; + margin-left: -2.55px; + background: gray; + border-radius: 100%; + opacity: 0.9; + box-shadow: black 2px 2px 1px; + + .linkAnchorBox-cont { + position: relative !important; + height: 100% !important; + width: 100% !important; + left: unset !important; + top: unset !important; + } + } + + .audiobox-linker-mini { + width: 8px; + min-height: 8px; + height: 8px; + box-shadow: black 1px 1px 1px; + margin-left: -1; + margin-top: -2; + + .linkAnchorBox-cont { + position: relative !important; + height: 100% !important; + width: 100% !important; + left: unset !important; + top: unset !important; + } + } + + .audiobox-linker:hover, + .audiobox-linker-mini:hover { + opacity: 1; + } + + .audiobox-marker-container, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 10px; + top: 2.5%; + background: gray; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: visible; + cursor: pointer; + + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-timeline, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 90%; + top: 2.5%; + border-radius: 5px; + + .left-resizer { + background: dimgrey; + } + .resizer { + background: dimgrey; + } + + .audiobox-marker { + position: relative; + height: calc(100% - 15px); + margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + + .resizer { + position: absolute; + top: 0; + right: 0; + pointer-events: all; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + top : 0; + pointer-events: all; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + } + + .audiobox-marker-timeline:hover, + .audiobox-marker-minicontainer:hover { + opacity: 0.8; + } + + .audiobox-marker-minicontainer { + width: 5px; + border-radius: 1px; + + .audiobox-marker { + position: relative; + height: calc(100% - 8px); + margin-top: 8px; + } + } + } + } + } +} + +@media only screen and (max-device-width: 480px) { + .audiobox-dictation { + font-size: 5em; + display: flex; + width: 100; + justify-content: center; + flex-direction: column; + align-items: center; + } + + .audiobox-container .audiobox-record, + .audiobox-container-interactive .audiobox-record { + font-size: 3em; + } + + .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, + .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, + .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { + width: 70px; + } +} \ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx new file mode 100644 index 000000000..1775250fa --- /dev/null +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -0,0 +1,324 @@ +import React = require("react"); +import { action, computed, IReactionDisposer, observable } from "mobx"; +import { observer } from "mobx-react"; +import { computedFn } from "mobx-utils"; +import { Doc, Opt } 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 } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { Scripting } from "../../util/Scripting"; +import { SelectionManager } from "../../util/SelectionManager"; +import { CollectionSubView } from "../collections/CollectionSubView"; +import { DocumentView } from "../nodes/DocumentView"; +import { LabelBox } from "../nodes/LabelBox"; +import "./CollectionStackedTimeline.scss"; + +type PanZoomDocument = makeInterface<[]>; +const PanZoomDocument = makeInterface(); +export type CollectionStackedTimelineProps = { + duration: number; + Play: () => void; + Pause: () => void; + playLink: (linkDoc: Doc) => void; + playFrom: (seekTimeInSeconds: number, endTime?: number) => void; + playing: () => boolean; + setTime: (time: number) => void; + isChildActive: () => boolean; +}; + +@observer +export class CollectionStackedTimeline extends CollectionSubView(PanZoomDocument) { + static RangeScript: ScriptField; + static LabelScript: ScriptField; + static RangePlayScript: ScriptField; + static LabelPlayScript: ScriptField; + + _disposers: { [name: string]: IReactionDisposer } = {}; + _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name + _ele: HTMLAudioElement | null = null; + _start: number = 0; + _left: boolean = false; + _dragging = false; + _play: any = null; + _audioRef = React.createRef(); + _timeline: Opt; + _markerStart: number = 0; + _currAnchor: Opt; + + @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; + @observable _markerEnd: number = 0; + @observable _position: number = 0; + @computed get anchorDocs() { return this.childDocs; } + @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); } + + constructor(props: any) { + super(props); + // onClick play scripts + CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; + CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; + } + + // for creating key anchors with key events + @action + keyEvents = (e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; + if (!this.props.playing()) return; // can't create if video is not playing + switch (e.key) { + case "x": // currently set to x, but can be a different key + const currTime = this.currentTime; + if (this._start) { + this._markerStart = currTime; + // this._start = false; + // this._visible = true; + } else { + this.createAnchor(this._markerStart, currTime); + // this._start = true; + // this._visible = false; + } + } + } + + anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))); + anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))); + + getLinkData(l: Doc) { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + } + return { la1, la2, linkTime }; + } + + // ref for timeline + timelineRef = (timeline: HTMLDivElement) => { + this._timeline = timeline; + } + + // updates the anchor with the new time + @action + changeAnchor = (anchor: Opt, time: number) => { + anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time); + } + + // checks if the two anchors are the same with start and end time + isSame = (m1: any, m2: any) => { + return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2); + } + + @computed get selectionContainer() { + return CollectionStackedTimeline.SelectingRegion !== this ? (null) :
; + } + + // starting the drag event for anchor resizing + @action + onPointerDownTimeline = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); + if (rect && e.target !== this._audioRef.current && this.props.active()) { + const wasPlaying = this.props.playing(); + if (wasPlaying) this.props.Pause(); + else if (!this._doubleTime) { + this._doubleTime = setTimeout(() => { + this._doubleTime = undefined; + this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration); + }, 300); + } + this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); + CollectionStackedTimeline.SelectingRegion = this; + setupMoveUpEvents(this, e, + action(e => { + this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); + return false; + }), + action((e, movement) => { + this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); + if (this._markerEnd < this._markerStart) { + const tmp = this._markerStart; + this._markerStart = this._markerEnd; + this._markerEnd = tmp; + } + CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); + CollectionStackedTimeline.SelectingRegion = undefined; + }), + (e, doubleTap) => { + this.props.select(false); + e.shiftKey && this.createAnchor(this.currentTime); + !wasPlaying && doubleTap && this.props.Play(); + } + , this.props.isSelected(true) || this.props.isChildActive()); + } + } + + @action + createAnchor(anchorStartTime?: number, anchorEndTime?: number) { + if (anchorStartTime === undefined) return this.props.Document; + const anchor = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, + useLinkSmallAnchor: true, + hideLinkButton: true, + anchorStartTime, + anchorEndTime, + annotationOn: this.props.Document + }); + if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor); + } else { + this.dataDoc[this.props.fieldKey] = new List([anchor]); + } + return anchor; + } + + // play back the audio from time + @action + playOnClick = (anchorDoc: Doc) => { + this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration)); + return { select: true }; + } + + // play back the audio from time + @action + clickAnchor = (anchorDoc: Doc) => { + if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc); + this.props.setTime(this.anchorStart(anchorDoc)); + return { select: true }; + } + + toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.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) => { + const rect = (e.target as any).getBoundingClientRect(); + this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width)); + return false; + }, + (e) => { + const rect = (e.target as any).getBoundingClientRect(); + this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width)); + this._timeline?.releasePointerCapture(e.pointerId); + }, + emptyFunction); + } + + rangeClickScript = () => CollectionStackedTimeline.RangeScript; + labelClickScript = () => CollectionStackedTimeline.LabelScript; + rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; + labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript; + + // 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(); + const x1 = this.anchorStart(m); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration); + let max = 0; + const overlappedLevels = new Set(placed.map(p => { + 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); + return p.level; + } + })); + let level = max + 1; + for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); + + placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); + return level; + } + + renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + const anchor = observable({ view: undefined as any }); + return { + anchor, view: anchor.view = r)} + Document={mark} + DataDoc={undefined} + PanelWidth={() => width} + PanelHeight={() => height} + renderDepth={this.props.renderDepth + 1} + focus={() => this.props.playLink(mark)} + rootSelected={returnFalse} + LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutString("data")} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} + parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()} + whenActiveChanged={this.props.whenActiveChanged} + onClick={script} + onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + }; + }); + 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); + return <> + {inner.view} + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : + <> +
this.onPointerDown(e, mark, true)} /> +
this.onPointerDown(e, mark, false)} /> + } + ; + }); + + render() { + const timelineContentWidth = this.props.PanelWidth(); + const timelineContentHeight = this.props.PanelHeight(); + 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
{ + if (this.props.isChildActive() || this.props.isSelected(false)) { + e.stopPropagation(); e.preventDefault(); + } + }} + onPointerDown={e => { + if (this.props.isChildActive() || this.props.isSelected(false)) { + 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.props.duration); + const left = start / this.props.duration * timelineContentWidth; + const top = d.level / maxLevel * timelineContentHeight; + const timespan = end - start; + return this.props.Document.hideAnchors ? (null) : +
{ this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, + left, + top, + timelineContentWidth * timespan / this.props.duration, + timelineContentHeight / maxLevel)} +
; + })} + {this.selectionContainer} +
{ e.stopPropagation(); e.preventDefault(); }} + style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }} + /> +
; + } +} +Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 6b9b1a3c0..03d8606d7 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -59,7 +59,8 @@ export enum CollectionViewType { //Staff = "staff", Map = "map", Grid = "grid", - Pile = "pileup" + Pile = "pileup", + StackedTimeline = "stacked timeline" } export interface CollectionViewProps extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index f509bfd64..c8bec74fb 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -9,14 +9,15 @@ import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { formatTime, numberRange, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils"; import { DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SnappingManager } from "../../util/SnappingManager"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -24,7 +25,6 @@ import "./AudioBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; -import { StackedTimeline } from "./StackedTimeline"; declare class MediaRecorder { // whatever MediaRecorder has constructor(e: any); @@ -46,7 +46,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent(); - _stackedTimeline = React.createRef(); + _stackedTimeline = React.createRef(); _recorder: any; _recordStart = 0; _pauseStart = 0; @@ -327,30 +327,43 @@ export class AudioBox extends ViewBoxAnnotatableComponent { return this.audioState === "playing"; } + playing = () => this.audioState === "playing"; playLink = (link: Doc) => { if (link.annotationOn === this.rootDoc) { if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link)); else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0); } - else this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { - const { la1, la2 } = this.getLinkData(l); - const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); - const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); - if (startTime !== undefined) { - if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); - else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; - } - }); + else { + this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { + const { la1, la2 } = this.getLinkData(l); + const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); + const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); + if (startTime !== undefined) { + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; + } + }); + } } @computed get renderTimeline() { - return this._ele!.currentTime = this.layoutDoc._currentTimecode = time} diff --git a/src/client/views/nodes/StackedTimeline.scss b/src/client/views/nodes/StackedTimeline.scss deleted file mode 100644 index da7310794..000000000 --- a/src/client/views/nodes/StackedTimeline.scss +++ /dev/null @@ -1,374 +0,0 @@ -.audiobox-container, -.audiobox-container-interactive { - width: 100%; - height: 100%; - position: inherit; - display: flex; - position: relative; - cursor: default; - - .audiobox-inner { - width:100%; - height: 100%; - } - - .audiobox-buttons { - display: flex; - width: 100%; - align-items: center; - height: 100%; - - .audiobox-dictation { - position: relative; - width: 30px; - height: 100%; - align-items: center; - display: inherit; - background: dimgray; - left: 0px; - } - - .audiobox-dictation:hover { - color: white; - cursor: pointer; - } - } - - .audiobox-handle { - width: 20px; - height: 100%; - display: inline-block; - } - - .audiobox-control, - .audiobox-control-interactive { - top: 0; - max-height: 32px; - width: 100%; - display: inline-block; - pointer-events: none; - } - - .audiobox-control-interactive { - pointer-events: all; - } - - .audiobox-record { - pointer-events: all; - width: 100%; - height: 100%; - position: relative; - pointer-events: none; - } - - .audiobox-record-interactive { - pointer-events: all; - width: 100%; - height: 100%; - position: relative; - - - } - - .recording { - margin-top: auto; - margin-bottom: auto; - width: 100%; - height: 100%; - position: relative; - padding-right: 5px; - display: flex; - background-color: red; - - .time { - position: relative; - height: 100%; - width: 100%; - font-size: 20; - text-align: center; - top: 5; - } - - .buttons { - position: relative; - margin-top: auto; - margin-bottom: auto; - width: 25px; - padding: 5px; - } - - .buttons:hover { - background-color: crimson; - } - } - - .audiobox-controls { - width: 100%; - height: 100%; - position: relative; - display: flex; - padding-left: 2px; - background: black; - - .audiobox-dictation { - position: absolute; - width: 30px; - height: 100%; - align-items: center; - display: inherit; - background: dimgray; - left: 0px; - } - - .audiobox-player { - margin-top: auto; - margin-bottom: auto; - width: 100%; - position: relative; - padding-right: 5px; - display: flex; - - .audiobox-playhead { - position: relative; - margin-top: auto; - margin-bottom: auto; - margin-right: 2px; - height: 25px; - padding: 2px; - border-radius: 50%; - background-color: black; - color: white; - } - - .audiobox-playhead:hover { - // background-color: black; - // border-radius: 5px; - background-color: grey; - color: lightgrey; - } - - .audiobox-dictation { - position: relative; - margin-top: auto; - margin-bottom: auto; - width: 25px; - padding: 2px; - align-items: center; - display: inherit; - background: dimgray; - } - - .audiobox-timeline { - position: absolute; - width: 100%; - border: gray solid 1px; - border-radius: 3px; - z-index: 1000; - overflow: hidden; - - .audiobox-container { - position: absolute; - width: 10px; - top: 2.5%; - height: 0px; - background: lightblue; - border-radius: 5px; - // box-shadow: black 2px 2px 1px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } - - .audiobox-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - } - - .waveform { - position: relative; - width: 100%; - 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-linker, - .audiobox-linker-mini { - position: absolute; - width: 15px; - min-height: 10px; - height: 15px; - margin-left: -2.55px; - background: gray; - border-radius: 100%; - opacity: 0.9; - box-shadow: black 2px 2px 1px; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker-mini { - width: 8px; - min-height: 8px; - height: 8px; - box-shadow: black 1px 1px 1px; - margin-left: -1; - margin-top: -2; - - .linkAnchorBox-cont { - position: relative !important; - height: 100% !important; - width: 100% !important; - left: unset !important; - top: unset !important; - } - } - - .audiobox-linker:hover, - .audiobox-linker-mini:hover { - opacity: 1; - } - - .audiobox-marker-container, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 10px; - top: 2.5%; - background: gray; - border-radius: 50%; - box-shadow: black 2px 2px 1px; - overflow: visible; - cursor: pointer; - - .audiobox-marker { - position: relative; - height: 100%; - // height: calc(100% - 15px); - width: 100%; - //margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - } - - .audiobox-marker-timeline, - .audiobox-marker-minicontainer { - position: absolute; - width: 10px; - height: 90%; - top: 2.5%; - border-radius: 5px; - - .left-resizer { - background: dimgrey; - } - .resizer { - background: dimgrey; - } - - .audiobox-marker { - position: relative; - height: calc(100% - 15px); - margin-top: 15px; - } - - .audio-marker:hover { - border: orange 2px solid; - } - - .resizer { - position: absolute; - top: 0; - right: 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - - .click { - position: relative; - height: 100%; - width: 100%; - z-index: 100; - } - - .left-resizer { - position: absolute; - left: 0; - top : 0; - pointer-events: all; - cursor: ew-resize; - height: 100%; - width: 10px; - z-index: 100; - } - } - - .audiobox-marker-timeline:hover, - .audiobox-marker-minicontainer:hover { - opacity: 0.8; - } - - .audiobox-marker-minicontainer { - width: 5px; - border-radius: 1px; - - .audiobox-marker { - position: relative; - height: calc(100% - 8px); - margin-top: 8px; - } - } - } - } - } -} - - -@media only screen and (max-device-width: 480px) { - .audiobox-dictation { - font-size: 5em; - display: flex; - width: 100; - justify-content: center; - flex-direction: column; - align-items: center; - } - - .audiobox-container .audiobox-record, - .audiobox-container-interactive .audiobox-record { - font-size: 3em; - } - - .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, - .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, - .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { - width: 70px; - } -} \ No newline at end of file diff --git a/src/client/views/nodes/StackedTimeline.tsx b/src/client/views/nodes/StackedTimeline.tsx deleted file mode 100644 index 808ca982d..000000000 --- a/src/client/views/nodes/StackedTimeline.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import React = require("react"); -import { action, computed, IReactionDisposer, observable } from "mobx"; -import { observer } from "mobx-react"; -import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { Scripting } from "../../util/Scripting"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Transform } from "../../util/Transform"; -import "./StackedTimeline.scss"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { LabelBox } from "./LabelBox"; - -export interface StackedTimelineProps { - Document: Doc; - dataDoc: Doc; - anchorProps: DocumentViewProps; - renderDepth: number; - annotationKey: string; - duration: number; - Play: () => void; - Pause: () => void; - playLink: (linkDoc: Doc) => void; - playFrom: (seekTimeInSeconds: number, endTime?: number) => void; - playing: () => boolean; - setTime: (time: number) => void; - select: (ctrlKey: boolean) => void; - isSelected: (outsideReaction: boolean) => boolean; - whenActiveChanged: (isActive: boolean) => void; - removeDocument: (doc: Doc | Doc[]) => boolean; - ScreenToLocalTransform: () => Transform; - isChildActive: () => boolean; - active: () => boolean; - PanelWidth: () => number; - PanelHeight: () => number; -} - -@observer -export class StackedTimeline extends React.Component { - static RangeScript: ScriptField; - static LabelScript: ScriptField; - static RangePlayScript: ScriptField; - static LabelPlayScript: ScriptField; - - _disposers: { [name: string]: IReactionDisposer } = {}; - _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name - _ele: HTMLAudioElement | null = null; - _start: number = 0; - _left: boolean = false; - _dragging = false; - _play: any = null; - _audioRef = React.createRef(); - _timeline: Opt; - _markerStart: number = 0; - _currAnchor: Opt; - - @observable static SelectingRegion: StackedTimeline | undefined = undefined; - @observable _markerEnd: number = 0; - @observable _position: number = 0; - @computed get anchorDocs() { return DocListCast(this.props.dataDoc[this.props.annotationKey]); } - @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); } - - constructor(props: Readonly) { - super(props); - // onClick play scripts - StackedTimeline.RangeScript = StackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.LabelScript = StackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.RangePlayScript = StackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; - StackedTimeline.LabelPlayScript = StackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; - } - - // for creating key anchors with key events - @action - keyEvents = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement) return; - if (!this.props.playing()) return; // can't create if video is not playing - switch (e.key) { - case "x": // currently set to x, but can be a different key - const currTime = this.currentTime; - if (this._start) { - this._markerStart = currTime; - // this._start = false; - // this._visible = true; - } else { - this.createAnchor(this._markerStart, currTime); - // this._start = true; - // this._visible = false; - } - } - } - - anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))) - anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))) - - getLinkData(l: Doc) { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); - if (Doc.AreProtosEqual(la1, this.props.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - } - return { la1, la2, linkTime }; - } - - // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { - this._timeline = timeline; - } - - // updates the anchor with the new time - @action - changeAnchor = (anchor: Opt, time: number) => { - anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time); - } - - // checks if the two anchors are the same with start and end time - isSame = (m1: any, m2: any) => { - return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2); - } - - @computed get selectionContainer() { - return StackedTimeline.SelectingRegion !== this ? (null) :
; - } - - // starting the drag event for anchor resizing - @action - onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); - if (rect && e.target !== this._audioRef.current && this.props.active()) { - const wasPlaying = this.props.playing(); - if (wasPlaying) this.props.Pause(); - else if (!this._doubleTime) { - this._doubleTime = setTimeout(() => { - this._doubleTime = undefined; - this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration); - }, 300); - } - this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); - StackedTimeline.SelectingRegion = this; - setupMoveUpEvents(this, e, - action(e => { - this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); - return false; - }), - action((e, movement) => { - this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); - if (this._markerEnd < this._markerStart) { - const tmp = this._markerStart; - this._markerStart = this._markerEnd; - this._markerEnd = tmp; - } - StackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); - StackedTimeline.SelectingRegion = undefined; - }), - (e, doubleTap) => { - this.props.select(false); - e.shiftKey && this.createAnchor(this.currentTime); - !wasPlaying && doubleTap && this.props.Play(); - } - , this.props.isSelected(true) || this.props.isChildActive()); - } - } - - @action - createAnchor(anchorStartTime?: number, anchorEndTime?: number) { - if (anchorStartTime === undefined) return this.props.Document; - const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, - useLinkSmallAnchor: true, - hideLinkButton: true, - anchorStartTime, - anchorEndTime, - annotationOn: this.props.Document - }); - if (Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), null) !== undefined) { - Cast(this.props.dataDoc[this.props.annotationKey], listSpec(Doc), []).push(anchor); - } else { - this.props.dataDoc[this.props.annotationKey] = new List([anchor]); - } - return anchor; - } - - // play back the audio from time - @action - playOnClick = (anchorDoc: Doc) => { - this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration)); - return { select: true }; - } - - // play back the audio from time - @action - clickAnchor = (anchorDoc: Doc) => { - if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc); - this.props.setTime(this.anchorStart(anchorDoc)); - return { select: true }; - } - - toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.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) => { - const rect = (e.target as any).getBoundingClientRect(); - this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width)); - return false; - }, - (e) => { - const rect = (e.target as any).getBoundingClientRect(); - this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width)); - this._timeline?.releasePointerCapture(e.pointerId); - }, - emptyFunction); - } - - rangeClickScript = () => StackedTimeline.RangeScript; - labelClickScript = () => StackedTimeline.LabelScript; - rangePlayScript = () => StackedTimeline.RangePlayScript; - labelPlayScript = () => StackedTimeline.LabelPlayScript; - - // 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(); - const x1 = this.anchorStart(m); - const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration); - let max = 0; - const overlappedLevels = new Set(placed.map(p => { - 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); - return p.level; - } - })); - let level = max + 1; - for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - - placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); - return level; - } - - renderInner = computedFn(function (this: StackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const anchor = observable({ view: undefined as any }); - return { - anchor, view: anchor.view = r)} - Document={mark} - DataDoc={undefined} - PanelWidth={() => width} - PanelHeight={() => height} - renderDepth={this.props.renderDepth + 1} - focus={() => this.props.playLink(mark)} - rootSelected={returnFalse} - LayoutTemplate={undefined} - LayoutTemplateString={LabelBox.LayoutString("data")} - ContainingCollectionDoc={this.props.Document} - removeDocument={this.props.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()} - whenActiveChanged={this.props.whenActiveChanged} - onClick={script} - onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript} - ignoreAutoHeight={false} - bringToFront={emptyFunction} - scriptContext={this} /> - }; - }); - renderAnchor = computedFn(function (this: StackedTimeline, 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.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : - <> -
this.onPointerDown(e, mark, true)} /> -
this.onPointerDown(e, mark, false)} /> - } - ; - }); - - render() { - const timelineContentWidth = this.props.PanelWidth(); - const timelineContentHeight = this.props.PanelHeight(); - 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
{ - if (this.props.isChildActive() || this.props.isSelected(false)) { - e.stopPropagation(); e.preventDefault(); - } - }} - onPointerDown={e => { - if (this.props.isChildActive() || this.props.isSelected(false)) { - 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.props.duration); - const left = start / this.props.duration * timelineContentWidth; - const top = d.level / maxLevel * timelineContentHeight; - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : -
{ this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > - {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, - left, - top, - timelineContentWidth * timespan / this.props.duration, - timelineContentHeight / maxLevel)} -
; - })} - {this.selectionContainer} -
{ e.stopPropagation(); e.preventDefault(); }} - style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }} - /> -
- } -} -Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index f89f54309..bfac7dc1c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -16,6 +16,7 @@ import { Networking } from "../../Network"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -25,7 +26,6 @@ import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; -import { StackedTimeline } from "./StackedTimeline"; import "./VideoBox.scss"; const path = require('path'); @@ -46,7 +46,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent(); + private _stackedTimeline = React.createRef(); private _mainCont: React.RefObject = React.createRef(); private _annotationLayer: React.RefObject = React.createRef(); private _playRegionTimer: any = null; @@ -72,8 +72,8 @@ 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))) + anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))); + anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))); getAnchor = () => { return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; @@ -484,12 +484,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent - this.player!.currentTime = this.layoutDoc._currentTimecode = time} -- cgit v1.2.3-70-g09d2