aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/views/DocComponent.tsx6
-rw-r--r--src/client/views/MarqueeAnnotator.tsx4
-rw-r--r--src/client/views/StyleProvider.tsx3
-rw-r--r--src/client/views/collections/TabDocView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx11
-rw-r--r--src/client/views/nodes/AudioBox.tsx48
-rw-r--r--src/client/views/nodes/DocumentView.tsx6
-rw-r--r--src/client/views/nodes/LabelBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.scss146
-rw-r--r--src/client/views/nodes/VideoBox.tsx436
-rw-r--r--src/client/views/nodes/WebBox.tsx6
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx2
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) {