diff options
-rw-r--r-- | src/client/documents/Documents.ts | 3 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 6 | ||||
-rw-r--r-- | src/client/views/MarqueeAnnotator.tsx | 4 | ||||
-rw-r--r-- | src/client/views/StyleProvider.tsx | 3 | ||||
-rw-r--r-- | src/client/views/collections/TabDocView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 11 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 48 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/LabelBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.scss | 146 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 436 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/RichTextMenu.tsx | 2 |
14 files changed, 589 insertions, 88 deletions
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<P extends ViewBoxAnnotatableProps, T public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } @action.bound - removeDocument(doc: Doc | Doc[]): boolean { + removeDocument(doc: Doc | Doc[], annotationKey?: string): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); const indocs = doc instanceof Doc ? [doc] : doc; const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); @@ -132,13 +132,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const docs = doc instanceof Doc ? [doc] : doc; docs.map(doc => 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<MarqueeAnnotatorProps> { 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<Doc>, props: Opt<FieldViewProps | case DocumentType.FILTER: docColor = docColor || (darkScheme() ? "#2d2d2d" : "rgba(105, 105, 105, 0.432)"); break; case DocumentType.INK: docColor = doc?.isInkMask ? "rgba(0,0,0,0.7)" : undefined; break; case DocumentType.SLIDER: break; - case DocumentType.LABEL: docColor = docColor || (doc?.audioStart !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; + case DocumentType.LABEL: docColor = docColor || (doc?.audioStart !== undefined || doc?.displayTimecode !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.LINK: return "transparent"; + case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; docColor = docColor ? docColor : 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<TabDocViewProps> { } 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 2bc716928..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<PanZoomDocument, P } @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections + return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / this.parentScaling / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections + return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / this.parentScaling / scaling;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -196,7 +196,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); docs.map(doc => 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) >= -0.1 && 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 c3c876b75..e854d40be 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -24,11 +24,12 @@ import { SnappingManager } from "../../util/SnappingManager"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; -import "./AudioBox.scss"; import { DocumentView } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; +import "./AudioBox.scss"; +import { Id } from "../../../fields/FieldSymbols"; declare class MediaRecorder { // whatever MediaRecorder has @@ -90,10 +91,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD AudioBox.Instance = this; // onClick play scripts - AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + 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" })!; } getLinkData(l: Doc) { @@ -188,18 +189,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // play back the audio from time @action playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); this.playFrom(seekTimeInSeconds, endTime); + 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 @@ -264,6 +263,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -364,15 +364,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD setupMoveUpEvents(this, e, action(e => { this._markerEnd = toTimeline(e.clientX - rect.x); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); if (this._markerEnd < this._markerStart) { const tmp = this._markerStart; this._markerStart = this._markerEnd; this._markerEnd = tmp; } - return false; - }), - action((e, movement) => { - AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); + AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); AudioBox.SelectingRegion = undefined; }), e => { @@ -388,9 +389,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (audioStart === undefined) return this.rootDoc; const marker = Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`"#" + formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, - useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false, - isLabel: audioEnd === undefined, - _autoHeight: true, annotationOn: this.props.Document + useLinkSmallAnchor: true, + hideLinkButton: true, + audioStart, + audioEnd, + annotationOn: this.props.Document }); if (this.dataDoc[this.annotationKey]) { this.dataDoc[this.annotationKey].push(marker); @@ -456,7 +459,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @computed get selectionContainer() { return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ - left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`, + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.audioDuration * 100}%`, width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%" }} />; } @@ -501,6 +504,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD Document={mark} PanelWidth={() => width} PanelHeight={() => height} + renderDepth={this.props.renderDepth + 1} focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} @@ -534,7 +538,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const timelineContentHeight = (this.props.PanelHeight() * AudioBox.heightPercent / 100) * AudioBox.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) const overlaps: { audioStart: number, audioEnd: number, level: number }[] = []; const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); - const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 1; + const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} @@ -569,13 +573,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="waveform"> {this.waveform} </div> - {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) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={i} + <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]} style={{ left, top, width: `${timespan / this.audioDuration * 100}%`, height: `${1 / maxLevel * 100}%` }} onClick={e => { this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, 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<DocumentViewInternalProps if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) && !this.onClickHandler.script.originalScript.includes("selectMainMenu")) { - UndoManager.RunInBatch(func, "on click"); + UndoManager.RunInBatch(() => func().result === true ? this.props.select(false) : "", "on click"); } else func(); }; if (this.onDoubleClickHandler) { @@ -935,7 +935,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelWidth = () => this.panelWidth; PanelHeight = () => this.panelHeight; ContentScale = () => this.nativeScaling; - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + screenToLocalTransform = () => { + return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + } componentDidMount() { !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 3448a4abd..87d5b07a2 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -67,7 +67,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... - const label = StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title)); + const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); return ( <div className="labelBox-outerDiv" onClick={action(() => this.clicked = !this.clicked)} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 07e8e0951..19f605278 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -10,6 +10,139 @@ .inkingCanvas-paths-markers { opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } + + .audiobox-timeline { + position: absolute; + width: 100%; + background: beige; + border: gray solid 1px; + border-radius: 3px; + z-index: 1000; + overflow: hidden; + bottom: 0; + + .audiobox-current { + width: 1px; + height: 100%; + background-color: red; + position: absolute; + top: 0px; + } + + .audiobox-container { + position: absolute; + width: 10px; + top: 2.5%; + height: 0px; + background: lightblue; + border-radius: 5px; + // box-shadow: black 2px 2px 1px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: darkblue; + border-width: 1px; + } + + .audiobox-marker-timeline, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 10px; + top: 2.5%; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: visible; + cursor: pointer; + + .left-resizer { + background: dimgrey; + } + .resizer { + background: dimgrey; + } + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-timeline, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 90%; + top: 2.5%; + border-radius: 5px; + box-shadow: black 2px 2px 1px; + + .audiobox-marker { + position: relative; + height: calc(100% - 15px); + margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + + .resizer { + position: absolute; + top: 0; + right: 0; + pointer-events: all; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + top: 0; + cursor: ew-resize; + height: 100%; + width: 10px; + z-index: 100; + } + + // .contentFittingDocumentView-previewDoc { + // width: 100% !important; + // transform: none !important; + // } + } + + .audiobox-marker-container1:hover, + .audiobox-marker-minicontainer:hover { + opacity: 0.8; + } + + .audiobox-marker-minicontainer { + width: 5px; + border-radius: 1px; + + .audiobox-marker { + position: relative; + height: calc(100% - 8px); + margin-top: 8px; + } + } + } } .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen, @@ -49,6 +182,19 @@ transform-origin: left top; pointer-events:all; } + +.timeline-button { + position: absolute; + display: flex; + align-items: center; + z-index: 1010; + bottom: 35px; + right: 235px; + color: white; + background: dimgrey; + width: 20px; + height: 20px; +} .videoBox-play { width: 25px; height: 20px; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 6354c677e..c5e61eedd 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,12 +3,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt, DocListCast } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { createSchema, makeInterface } from "../../../fields/Schema"; import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils"; +import { Utils, emptyFunction, returnOne, returnZero, OmitKeys, setupMoveUpEvents, returnFalse, returnTrue, formatTime } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -21,13 +21,18 @@ import { documentSchema } from "../../../fields/documentSchemas"; import { Networking } from "../../Network"; import { SnappingManager } from "../../util/SnappingManager"; import { SelectionManager } from "../../util/SelectionManager"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +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 { AnchorMenu } from "../pdf/AnchorMenu"; +import { Id } from "../../../fields/FieldSymbols"; +import { LabelBox } from "./LabelBox"; const path = require('path'); export const timeSchema = createSchema({ @@ -39,6 +44,12 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { static _youtubeIframeCounter: number = 0; + static Instance: VideoBox; + static RangeScript: ScriptField; + static LabelScript: ScriptField; + static RangePlayScript: ScriptField; + static LabelPlayScript: ScriptField; + static heightPercent = 60; // height of timeline in percent of height of videoBox. private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; @@ -47,19 +58,46 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private _isResetClick = 0; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + _play: any = null; + _timeline: Opt<HTMLDivElement>; + _audioRef = React.createRef<HTMLDivElement>(); + _markerStart: number = 0; + _left: boolean = false; + _count: Array<any> = []; + _duration = 0; + _start: boolean = true; + _currMarker: any; @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable _screenCapture = false; + @observable static _showControls: boolean; + @observable static SelectingRegion: VideoBox | undefined = undefined; + @observable _visible: boolean = false; + @observable _markerEnd: number = 0; @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable _playing = false; - @observable static _showControls: boolean; + @computed get heightPercent() { return this.layoutDoc._showTimeline ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 100; } + @computed get videoDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } + @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } public get player(): HTMLVideoElement | null { return this._videoRef; } + constructor(props: Readonly<FieldViewProps>) { + super(props); + VideoBox.Instance = this; + + // onClick play scripts + VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.displayTimecode, this.undisplayTimecode)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.displayTimecode)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.displayTimecode, this.undisplayTimecode)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.displayTimecode)`, { self: Doc.name, scriptContext: "any" })!; + } + videoLoad = () => { const aspect = this.player!.videoWidth / this.player!.videoHeight; Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); @@ -69,6 +107,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action public Play = (update: boolean = true) => { + document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); + document.addEventListener("keydown", VideoBox.keyEventsWrapper, true); this._playing = true; try { update && this.player?.play(); @@ -205,7 +245,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { const delay = this.player ? 0 : 250; // wait for mainCont and try again to play setTimeout(() => this.player && this.Play(), delay); - setTimeout(() => { this.Document._videoStart = undefined; }, 10 + delay); + setTimeout(() => this.Document._videoStart = undefined, 10 + delay); } } }, @@ -238,7 +278,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); - Object.values(this._disposers).forEach(disposer => disposer?.()); + this._disposers.reactionDisposer?.(); + this._disposers.youtubeReactionDisposer?.(); + this._disposers.videoStart?.(); + document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); } @action @@ -270,7 +313,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD console.log("VideoBox :" + e); } } - @observable _screenCapture = false; + specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { @@ -285,26 +328,33 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnClick ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } + // returns the video and timeline @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : - <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} - onCanPlay={this.videoLoad} - controls={VideoBox._showControls} - onPlay={() => this.Play()} - onSeeked={this.updateTimecode} - onPause={() => this.Pause()} - onClick={e => e.preventDefault()}> - <source src={field.url.href} type="video/mp4" /> - Not supported. - </video>; + <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}> + <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}> + <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }} + onCanPlay={this.videoLoad} + controls={VideoBox._showControls} + onPlay={() => this.Play()} + onSeeked={this.updateTimecode} + onPause={() => this.Pause()} + onClick={e => e.preventDefault()}> + <source src={field.url.href} type="video/mp4" /> + Not supported. + </video> + </div> + </div>; } @computed get youtubeVideoId() { @@ -356,22 +406,32 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private get uIButtons() { const curTime = (this.layoutDoc._currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > - <span>{"" + Math.round(curTime)}</span> + <span>{"" + formatTime(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, + <div className="timeline-button" key="timeline-button" onPointerDown={this.toggleTimeline} style={{ + transform: `scale(${this.scaling()})`, + right: this.scaling() * 10 - 10, + bottom: this.scaling() * 10 - 10 + }}> + <FontAwesomeIcon icon={this.layoutDoc._showTimeline ? "eye-slash" : "eye"} style={{ width: "100%" }} /> + </div>, VideoBox._showControls ? (null) : [ + // <div className="control-background"> <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} > <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> </div>, <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} > F + {/* </div> */} </div> ]]); } + onPlayDown = () => this._playing ? this.Pause() : this.Play(); onFullDown = (e: React.PointerEvent) => { @@ -426,52 +486,334 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return this.addDocument(doc); } - screenToLocalTransform = () => this.props.ScreenToLocalTransform(); + // play back the video from time + @action + playFrom = (seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + clearTimeout(this._play); + this._duration = endTime - seekTimeInSeconds; + if (Number.isNaN(this.player?.duration)) { + setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + } else if (this.player) { + if (seekTimeInSeconds < 0) { + if (seekTimeInSeconds > -1) { + setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); + } else { + this.Pause(); + } + } else if (seekTimeInSeconds <= this.player.duration) { + this.player.currentTime = seekTimeInSeconds; + this.player.play(); + runInAction(() => this._playing = true); + if (endTime !== this.videoDuration) { + this._play = setTimeout(() => this.Pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + } + } else { + this.Pause(); + } + } + } + + @action + toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._showTimeline = !this.layoutDoc._showTimeline + + // ref for timeline + timelineRef = (timeline: HTMLDivElement) => { this._timeline = timeline; } + + // starting the drag event creating a range marker + @action + onPointerDownTimeline = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); + if (rect && e.target !== this._audioRef.current && this.active()) { + const wasPaused = !this._playing; + this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; + wasPaused && this.Pause(); + + const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.videoDuration; + this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); + VideoBox.SelectingRegion = this; + setupMoveUpEvents(this, e, + action(e => { + this._markerEnd = toTimeline(e.clientX - rect.x); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); + if (this._markerEnd < this._markerStart) { + const tmp = this._markerStart; + this._markerStart = this._markerEnd; + this._markerEnd = tmp; + } + VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + VideoBox.SelectingRegion = undefined; + }), + e => { + this.props.select(false); + e.shiftKey && this.createMarker(this.player!.currentTime); + } + , this.props.isSelected(true) || this._isChildActive); + } + } + + @action + createMarker(displayTimecode: number, undisplayTimecode?: number) { + const marker = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.displayTimecode) + "-" + formatToTime(self.undisplayTimecode)`) as any, + useLinkSmallAnchor: true, // bcz: note this also flags that the annotation is not on the video itself, just the timeline + hideLinkButton: true, + displayTimecode, + undisplayTimecode, + annotationOn: this.props.Document + }); + if (this.dataDoc[this.annotationKey + "-timeline"]) { + this.dataDoc[this.annotationKey + "-timeline"].push(marker); + } else { + this.dataDoc[this.annotationKey + "-timeline"] = new List<Doc>([marker]); + } + } + + // play back the video from time + @action + playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + 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) 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 + onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { + this._currMarker = m; + this._left = left; + this._timeline?.setPointerCapture(e.pointerId); + const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.videoDuration; + setupMoveUpEvents(this, e, + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); + return false; + }, + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this.player!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width); + this._timeline?.releasePointerCapture(e.pointerId); + }, + emptyFunction); + } + + // makes sure no markers overlaps each other by setting the correct position and width + getLevel = (m: any, placed: { videoStart: 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; + let max = 0; + const overlappedLevels = new Set(placed.map(p => { + 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); + return p.level; + } + })); + let level = max + 1; + for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); + + 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, annotationKey: string) { + const marker = observable({ view: undefined as any }); + return { + marker, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => marker.view = r)} + Document={mark} + DataDoc={undefined} + PanelWidth={() => 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.scaling()).translate(-x, -y)} + parentActive={(out) => this.props.isSelected(out) || this._isChildActive} + whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + onClick={script} + onDoubleClick={this.layoutDoc.playOnClick ? undefined : doublescript} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + }; + }); + + renderMarker = computedFn(function (this: 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) : + <> + <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} /> + <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} /> + </>} + </>; + }); + + // returns the timeline + @computed get renderTimeline() { + const timelineContentWidth = this.props.PanelWidth(); + const timelineContentHeight = this.props.PanelHeight() * (100 - this.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) : + <div className="audiobox-timeline" ref={this.timelineRef} style={{ height: `${100 - this.heightPercent}%` }} + onClick={e => { + 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 start = NumCast(m.displayTimecode, NumCast(m.displayTimecode, null)); + const left = start / this.videoDuration * 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) : + <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]} + style={{ left, top, width: `${timespan / this.videoDuration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(start, Cast(m.undisplayTimecode, "number", null)); e.stopPropagation(); }} > + {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, + left, + top + (this.props.PanelHeight() - timelineContentHeight), + timelineContentWidth * timespan / this.videoDuration, + timelineContentHeight / maxLevel, this.annotationKey + (m.useLinkSmallAnchor ? "-timeline" : ""))} + </div>; + })} + {this.selectionContainer} + <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.videoDuration * 100}%`, pointerEvents: "none" }} /> + </div>; + } + + // updates the marker with the new time + @action + changeMarker = (m: any, time: any) => { + this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => + this._left ? marker.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.displayTimecode === m2.displayTimecode && m1.undisplayTimecode === m2.undisplayTimecode; + } + + // returns the blue container when dragging + @computed get selectionContainer() { + return VideoBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.videoDuration * 100}%`, + width: `${Math.abs(this._markerStart - this._markerEnd) / this.videoDuration * 100}%`, height: "100%", top: "0%" + }} />; + } + + static keyEventsWrapper = (e: KeyboardEvent) => { + VideoBox.Instance.keyEvents(e); + } + + // for creating key markers with key events + @action + keyEvents = (e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; + if (!this._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.player!.currentTime; + if (this._start) { + this._markerStart = this.player!.currentTime; + this._start = false; + this._visible = true; + } else { + this.createMarker(this._markerStart, currTime); + this._start = true; + this._visible = false; + } + } + } + + rangeClickScript = () => VideoBox.RangeScript; + labelClickScript = () => VideoBox.LabelScript; + rangePlayScript = () => VideoBox.RangePlayScript; + labelPlayScript = () => VideoBox.LabelPlayScript; + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ height: "100%" }} ref={this._annotationLayer} />; } marqueeDown = action((e: React.PointerEvent) => { if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; - }) + }); finishMarquee = action(() => { this._marqueeing = undefined; this.props.select(true); - }) + }); + + scaling = () => this.props.scaling?.() || 1; + panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; + panelHeight = () => this.props.PanelHeight() * this.heightPercent / 100; + screenToLocalTransform = () => { + const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); + return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); + } render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad; + const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius }} > - <div className="videoBox-viewer" onPointerDown={this.marqueeDown}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - forceScaling={true} - fieldKey={this.annotationKey} - isAnnotationOverlay={true} - select={emptyFunction} - active={this.annotationsActive} - scaling={returnOne} - ScreenToLocalTransform={this.screenToLocalTransform} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWithTimestamp} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - {this.contentFunc} - </CollectionFreeFormView> + <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > + <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + fieldKey={this.annotationKey} + isAnnotationOverlay={true} + forceScaling={true} + select={emptyFunction} + active={this.annotationsActive} + scaling={returnOne} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocumentWithTimestamp} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + {this.uIButtons} + {this.annotationLayer} + {this.renderTimeline} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div> - {this.uIButtons} - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 69f797880..37f268823 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -194,7 +194,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._iframe?.removeEventListener('wheel', this.iframeWheel); } - onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); } + onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); }; @undoBatch @action @@ -280,8 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } _ignore = 0; - onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; } - onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; } + onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; }; + onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; }; onPostPointer = (e: React.PointerEvent) => { if (this._ignore !== e.timeStamp) e.stopPropagation(); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ac5ea66ff..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<AntimodeMenuProps> { e.stopPropagation(); self.TextView.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush"); - } + }; let label = "Stored marks: "; if (this.brushMarks && this.brushMarks.size > 0) { |