aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
authorusodhi <61431818+usodhi@users.noreply.github.com>2021-02-01 18:42:38 -0500
committerusodhi <61431818+usodhi@users.noreply.github.com>2021-02-01 18:42:38 -0500
commitb160a00f80c4855e190d75f97482b5b019e77437 (patch)
tree4fd150d45a5c49b9d5572edcb663956c0b29dd60 /src/client/views/nodes
parent888fb3b3933e7aa48e9ac3abe85536328fcca336 (diff)
parent41bb365dd4f787aec2262dcb07508e0de3837e10 (diff)
more merge
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/AudioBox.scss224
-rw-r--r--src/client/views/nodes/AudioBox.tsx75
-rw-r--r--src/client/views/nodes/DocumentView.tsx2
-rw-r--r--src/client/views/nodes/FilterBox.tsx9
-rw-r--r--src/client/views/nodes/ImageBox.tsx16
-rw-r--r--src/client/views/nodes/PresBox.tsx13
-rw-r--r--src/client/views/nodes/VideoBox.scss139
-rw-r--r--src/client/views/nodes/VideoBox.tsx149
-rw-r--r--src/client/views/nodes/WebBox.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss15
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx75
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx3
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts38
13 files changed, 248 insertions, 514 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 4a3bbf8d8..fc881ca25 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -7,11 +7,6 @@
position: relative;
cursor: default;
- .audiobox-inner {
- width:100%;
- height: 100%;
- }
-
.audiobox-buttons {
display: flex;
width: 100%;
@@ -26,20 +21,13 @@
display: inherit;
background: dimgray;
left: 0px;
- }
-
- .audiobox-dictation:hover {
- color: white;
- cursor: pointer;
+ &:hover {
+ color: white;
+ cursor: pointer;
+ }
}
}
- .audiobox-handle {
- width: 20px;
- height: 100%;
- display: inline-block;
- }
-
.audiobox-control,
.audiobox-control-interactive {
top: 0;
@@ -53,21 +41,16 @@
pointer-events: all;
}
+ .audiobox-record-interactive,
.audiobox-record {
pointer-events: all;
width: 100%;
height: 100%;
position: relative;
- pointer-events: none;
}
- .audiobox-record-interactive {
- pointer-events: all;
- width: 100%;
- height: 100%;
- position: relative;
-
-
+ .audiobox-record {
+ pointer-events: none;
}
.recording {
@@ -95,10 +78,9 @@
margin-bottom: auto;
width: 25px;
padding: 5px;
- }
-
- .buttons:hover {
- background-color: crimson;
+ &:hover{
+ background-color: crimson;
+ }
}
}
@@ -138,13 +120,10 @@
border-radius: 50%;
background-color: black;
color: white;
- }
-
- .audiobox-playhead:hover {
- // background-color: black;
- // border-radius: 5px;
- background-color: grey;
- color: lightgrey;
+ &:hover {
+ background-color: grey;
+ color: lightgrey;
+ }
}
.audiobox-dictation {
@@ -166,29 +145,6 @@
z-index: 1000;
overflow: hidden;
- .audiobox-container {
- position: absolute;
- width: 10px;
- top: 2.5%;
- height: 0px;
- background: lightblue;
- border-radius: 5px;
- // box-shadow: black 2px 2px 1px;
- opacity: 0.3;
- z-index: 500;
- border-style: solid;
- border-color: darkblue;
- border-width: 1px;
- }
-
- .audiobox-current {
- width: 1px;
- height: 100%;
- background-color: red;
- position: absolute;
- top: 0px;
- }
-
.waveform {
position: relative;
width: 100%;
@@ -206,161 +162,21 @@
width: 100% !important;
}
}
-
- .audiobox-linker,
- .audiobox-linker-mini {
- position: absolute;
- width: 15px;
- min-height: 10px;
- height: 15px;
- margin-left: -2.55px;
- background: gray;
- border-radius: 100%;
- opacity: 0.9;
- box-shadow: black 2px 2px 1px;
-
- .linkAnchorBox-cont {
- position: relative !important;
- height: 100% !important;
- width: 100% !important;
- left: unset !important;
- top: unset !important;
- }
- }
-
- .audiobox-linker-mini {
- width: 8px;
- min-height: 8px;
- height: 8px;
- box-shadow: black 1px 1px 1px;
- margin-left: -1;
- margin-top: -2;
-
- .linkAnchorBox-cont {
- position: relative !important;
- height: 100% !important;
- width: 100% !important;
- left: unset !important;
- top: unset !important;
- }
- }
-
- .audiobox-linker:hover,
- .audiobox-linker-mini:hover {
- opacity: 1;
- }
-
- .audiobox-marker-container,
- .audiobox-marker-minicontainer {
- position: absolute;
- width: 10px;
- height: 10px;
- top: 2.5%;
- background: gray;
- border-radius: 50%;
- box-shadow: black 2px 2px 1px;
- overflow: visible;
- cursor: pointer;
-
- .audiobox-marker {
- position: relative;
- height: 100%;
- // height: calc(100% - 15px);
- width: 100%;
- //margin-top: 15px;
- }
-
- .audio-marker:hover {
- border: orange 2px solid;
- }
- }
-
- .audiobox-marker-timeline,
- .audiobox-marker-minicontainer {
- position: absolute;
- width: 10px;
- height: 90%;
- top: 2.5%;
- border-radius: 5px;
-
- .left-resizer {
- background: dimgrey;
- }
- .resizer {
- background: dimgrey;
- }
-
- .audiobox-marker {
- position: relative;
- height: calc(100% - 15px);
- margin-top: 15px;
- }
-
- .audio-marker:hover {
- border: orange 2px solid;
- }
-
- .resizer {
- position: absolute;
- top: 0;
- right: 0;
- pointer-events: all;
- cursor: ew-resize;
- height: 100%;
- width: 10px;
- z-index: 100;
- }
-
- .click {
- position: relative;
- height: 100%;
- width: 100%;
- z-index: 100;
- }
-
- .left-resizer {
- position: absolute;
- left: 0;
- top : 0;
- pointer-events: all;
- cursor: ew-resize;
- height: 100%;
- width: 10px;
- z-index: 100;
- }
- }
-
- .audiobox-marker-timeline:hover,
- .audiobox-marker-minicontainer:hover {
- opacity: 0.8;
- }
-
- .audiobox-marker-minicontainer {
- width: 5px;
- border-radius: 1px;
-
- .audiobox-marker {
- position: relative;
- height: 100%;
- margin-top: 8px;
- }
- }
}
- .current-time {
+ .audioBox-total-time,
+ .audioBox-current-time {
position: absolute;
font-size: 8;
top: 100%;
- left: 30px;
color: white;
}
+ .audioBox-current-time {
+ left: 30px;
+ }
- .total-time {
- position: absolute;
- top: 100%;
- font-size: 8;
+ .audioBox-total-time {
right: 2px;
- color: white;
}
}
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index c8bec74fb..692eaae66 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -26,13 +26,11 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
import { LinkDocPreview } from "./LinkDocPreview";
declare class MediaRecorder {
- // whatever MediaRecorder has
- constructor(e: any);
+ constructor(e: any); // whatever MediaRecorder has
}
-export const audioSchema = createSchema({ playOnSelect: "boolean" });
-type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
-const AudioDocument = makeInterface(documentSchema, audioSchema);
+type AudioDocument = makeInterface<[typeof documentSchema]>;
+const AudioDocument = makeInterface(documentSchema);
@observer
export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
@@ -45,7 +43,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
_disposers: { [name: string]: IReactionDisposer } = {};
_ele: HTMLAudioElement | null = null;
- _audioRef = React.createRef<HTMLDivElement>();
_stackedTimeline = React.createRef<CollectionStackedTimeline>();
_recorder: any;
_recordStart = 0;
@@ -83,7 +80,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
+ const linkTime = this._stackedTimeline.current?.anchorStart(la2) || this._stackedTimeline.current?.anchorStart(la1) || 0;
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
@@ -92,7 +89,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
getAnchor = () => {
- return this._stackedTimeline.current?.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc;
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "audioStart", "audioEnd", this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)) || this.rootDoc;
}
componentWillUnmount() {
@@ -107,26 +104,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this.audioState = this.path ? "paused" : undefined;
- this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime));
+ //this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime));
this._disposers.triggerAudio = reaction(
() => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined,
start => start !== undefined && setTimeout(() => {
- this._audioRef.current && this.playFrom(start);
+ this.playFrom(start);
setTimeout(() => {
this.Document._currentTimecode = start;
this.Document._triggerAudio = undefined;
}, 10);
- }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
+ }), // wait for mainCont and try again to play
{ fireImmediately: true }
);
this._disposers.audioStop = reaction(
() => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined,
audioStop => audioStop !== undefined && setTimeout(() => {
- this._audioRef.current && this.Pause();
+ this.Pause();
setTimeout(() => this.Document._audioStop = undefined, 10);
- }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
+ }), // wait for mainCont and try again to play
{ fireImmediately: true }
);
}
@@ -329,15 +326,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
playing = () => this.audioState === "playing";
playLink = (link: Doc) => {
+ const stack = this._stackedTimeline.current;
if (link.annotationOn === this.rootDoc) {
- if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link));
- else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0);
+ if (this.layoutDoc.playOnSelect) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
+ else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0);
}
else {
this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
const { la1, la2 } = this.getLinkData(l);
- const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null));
- const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null));
+ const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
+ const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
if (startTime !== undefined) {
if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
@@ -346,40 +344,35 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
}
+ isActiveChild = () => this._isChildActive;
+ timelineWhenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(runInAction(() => this._isChildActive = isActive));
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-AudioBox.playheadWidth, -(100 - this.heightPercent) / 200 * this.props.PanelHeight());
+ setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
+ timelineHeight = () => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
+ timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
@computed get renderTimeline() {
- return <CollectionStackedTimeline ref={this._stackedTimeline}
- Document={this.props.Document}
+ return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
fieldKey={this.annotationKey}
renderDepth={this.props.renderDepth + 1}
- parentActive={this.props.parentActive}
+ startTag={"audioStart"}
+ endTag={"audioEnd"}
focus={emptyFunction}
- styleProvider={this.props.styleProvider}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
- searchFilterDocs={this.props.searchFilterDocs}
- rootSelected={this.props.rootSelected}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
bringToFront={emptyFunction}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
CollectionView={undefined}
duration={this.duration}
playFrom={this.playFrom}
- setTime={(time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time}
+ setTime={this.setAnchorTime}
playing={this.playing}
- select={this.props.select}
- isSelected={this.props.isSelected}
- whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
+ whenActiveChanged={this.timelineWhenActiveChanged}
removeDocument={this.removeDocument}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(0, -(100 - this.heightPercent) / 200 * this.props.PanelHeight())}
- isChildActive={() => this._isChildActive}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ isChildActive={this.isActiveChild}
Play={this.Play}
Pause={this.Pause}
active={this.active}
playLink={this.playLink}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={() => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100}// panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
+ PanelWidth={this.timelineWidth}
+ PanelHeight={this.timelineHeight}
/>;
}
@@ -413,17 +406,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
<div className="audiobox-dictation" />
<div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} >
<div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div>
- <div className="audiobox-timeline" style={{ height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
+ <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
<div className="waveform">
{this.waveform}
</div>
+ {this.renderTimeline}
</div>
- {this.renderTimeline}
{this.audio}
- <div className="current-time">
+ <div className="audioBox-current-time">
{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
</div>
- <div className="total-time">
+ <div className="audioBox-total-time">
{formatTime(Math.round(this.duration))}
</div>
</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6dc7a822c..b653b35bc 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -30,7 +30,6 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { InkingStroke } from "../InkingStroke";
-import { InkStrokeProperties } from '../InkStrokeProperties';
import { StyleLayers, StyleProp } from "../StyleProvider";
import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
import { DocumentContentsView } from "./DocumentContentsView";
@@ -86,6 +85,7 @@ export interface DocumentViewSharedProps {
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
freezeDimensions?: boolean;
+ hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
treeViewDoc?: Doc;
contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 69c008dc9..a7227ecb8 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -290,6 +290,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
}
+ suppressChildClick = () => ScriptField.MakeScript("")!;
render() {
const facetCollection = this.props.Document;
// const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}>
@@ -303,8 +304,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
// const attributes = this.activeAttributes;
// const options = this._allFacets.filter(facet => !attributes.some(attribute => attribute.title === facet)).map(facet => ({ value: facet, label: facet }));
-<<<<<<< HEAD
- const options = this._allFacets.map(facet => ({ value: facet, label: facet }));
+ // const options = this._allFacets.map(facet => ({ value: facet, label: facet }));
// console.log(this.props.Document);
// console.log(Doc.UserDoc().currentFilter);
console.log(this.yPos);
@@ -315,9 +315,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
testing flyout
</div>
</>;
-=======
const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
->>>>>>> 5b0a4a154a6e68139d3d7e462ca421d3fbbdd224
return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}>
@@ -357,9 +355,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
<CollectionTreeView
Document={facetCollection}
DataDoc={Doc.GetProto(facetCollection)}
- fieldKey={`${this.props.fieldKey}`}
+ fieldKey={this.props.fieldKey}
CollectionView={undefined}
cantBrush={true}
+ onChildClick={this.suppressChildClick}
docFilters={returnEmptyFilter}
docRangeFilters={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 649fe8f40..92d6e2612 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,8 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Dictionary } from 'typescript-collections';
+import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc';
import { documentSchema } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
@@ -11,7 +12,7 @@ import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnOne, returnZero, Utils, OmitKeys } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnOne, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
@@ -22,15 +23,12 @@ import { ContextMenu } from "../../views/ContextMenu";
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { StyleProp } from '../StyleProvider';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { StyleProp } from '../StyleProvider';
-import { AnchorMenu } from '../pdf/AnchorMenu';
-import { Dictionary } from 'typescript-collections';
-import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { Annotation } from '../pdf/Annotation';
const path = require('path');
const { Howl } = require('howler');
@@ -423,7 +421,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
}
@action
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY];
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.active(true)) this._marqueeing = [e.clientX, e.clientY];
}
@action
finishMarquee = () => {
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 62e497e18..b6feace12 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -417,15 +417,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
self._dragArray.splice(0, self._dragArray.length, ...dragViewCache);
self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
});
- const openInTab = () => {
- collectionDocView ? collectionDocView.props.addDocTab(targetDoc, "") : this.props.addDocTab(targetDoc, ":left");
+ const openInTab = (doc: Doc, finished?: () => void) => {
+ collectionDocView ? collectionDocView.props.addDocTab(doc, "") : this.props.addDocTab(doc, ":left");
this.layoutDoc.presCollection = targetDoc;
// this still needs some fixing
setTimeout(resetSelection, 500);
+ doc !== targetDoc && setTimeout(() => finished?.(), 100); /// give it some time to create the targetDoc if we're opening up its context
};
// If openDocument is selected then it should open the document for the user
if (activeItem.openDocument) {
- openInTab();
+ openInTab(targetDoc);
} else if (curDoc.presMovement === PresMovement.Pan && targetDoc) {
await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right
} else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) {
@@ -714,9 +715,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
if (audio) {
audio.mediaStart = "manual";
audio.mediaStop = "manual";
- audio.presStartTime = NumCast(doc.anchorStartTime);
- audio.presEndTime = NumCast(doc.anchorEndTime);
- audio.presDuration = NumCast(doc.anchorEndTime) - NumCast(doc.anchorStartTime);
+ audio.presStartTime = NumCast(doc.audioStart, NumCast(doc.videoStart));
+ audio.presEndTime = NumCast(doc.audioEnd, NumCast(doc.videoEnd));
+ audio.presDuration = audio.presStartTime - audio.presEndTime;
TabDocView.PinDoc(audio, { audioRange: true });
setTimeout(() => this.removeDocument(doc), 0);
return false;
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 19f605278..b9123587b 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -10,138 +10,8 @@
.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%;
+ .collectionStackedTimeline {
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;
- }
- }
}
}
@@ -183,14 +53,15 @@
pointer-events:all;
}
-.timeline-button {
+.videoBox-timelineButton {
position: absolute;
display: flex;
align-items: center;
z-index: 1010;
- bottom: 35px;
- right: 235px;
+ bottom: 5px;
+ right: 5px;
color: white;
+ cursor: pointer;
background: dimgrey;
width: 20px;
height: 20px;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index bfac7dc1c..8a1cefbd9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -7,7 +7,7 @@ import { Dictionary } from "typescript-collections";
import { Doc, DocListCast } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { InkTool } from "../../../fields/InkField";
-import { createSchema, makeInterface } from "../../../fields/Schema";
+import { makeInterface } from "../../../fields/Schema";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { VideoField } from "../../../fields/URLField";
import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
@@ -29,11 +29,8 @@ import { LinkDocPreview } from "./LinkDocPreview";
import "./VideoBox.scss";
const path = require('path');
-export const timeSchema = createSchema({
- _currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
-});
-type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>;
-const VideoDocument = makeInterface(documentSchema, timeSchema);
+type VideoDocument = makeInterface<[typeof documentSchema]>;
+const VideoDocument = makeInterface(documentSchema);
@observer
export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
@@ -55,16 +52,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
@observable _marqueeing: number[] | undefined;
@observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@observable _screenCapture = false;
- @observable _visible: boolean = false;
+ @observable _clicking = false;
@observable _forceCreateYouTubeIFrame = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable _playing = false;
@computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get heightPercent() { return this.layoutDoc._timelineShow ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 100; }
+ @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); }
@computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
- @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); }
+ private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
public get player(): HTMLVideoElement | null { return this._videoRef; }
constructor(props: Readonly<FieldViewProps>) {
@@ -72,11 +69,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
VideoBox.Instance = this;
}
- anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)));
- anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)));
-
getAnchor = () => {
- return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc;
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey + "-timeline", "videoStart", "videoEnd", Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc;
}
choosePath(url: string) {
@@ -91,13 +85,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration;
}
- static keyEventsWrapper = (e: KeyboardEvent) => {
- VideoBox.Instance._stackedTimeline.current?.keyEvents(e);
- }
-
@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();
@@ -179,7 +167,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
//convert to desired file format
const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
- const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_")));
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
+ const filename = path.basename(encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_")));
VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
returnedFilename && this.createRealSummaryLink(returnedFilename));
}
@@ -191,14 +180,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
const height = this.layoutDoc._height || 0;
const imageSummary = Docs.Create.ImageDocument(url, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0),
+ x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), isLinkButton: true,
_width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-"
});
Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- imageSummary.isLinkButton = true;
this.props.addDocument?.(imageSummary);
- DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot");
+ DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
}
@action
@@ -253,7 +241,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
componentWillUnmount() {
this.Pause();
Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
- document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true);
}
@action
@@ -310,8 +297,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
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> :
- <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}>
+ return !field ? <div key="loading">Loading</div> :
+ <div className="container" key="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" }}
@@ -380,16 +367,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
<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} >
+ <div className="videoBox-snapshot" key="snap" onClick={this.onSnapshot} >
<FontAwesomeIcon icon="camera" size="lg" />
</div>,
- <div className="timeline-button" key="timeline-button" onPointerDown={action(e => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow)}
- style={{
- transform: `scale(${this.scaling()})`,
- right: this.scaling() * 10 - 10,
- bottom: this.scaling() * 10 - 10
- }}>
- <FontAwesomeIcon icon={this.layoutDoc._timelineShow ? "eye-slash" : "eye"} style={{ width: "100%" }} />
+ <div className="videoBox-timelineButton" key="timeline" onPointerDown={this.onTimelineHdlDown} style={{ bottom: `${100 - this.heightPercent}%` }}>
+ <FontAwesomeIcon icon={"eye"} size="lg" />
</div>,
VideoBox._showControls ? (null) : [
// <div className="control-background">
@@ -411,18 +393,37 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
e.preventDefault();
}
- onSnapshot = (e: React.PointerEvent) => {
+ onSnapshot = (e: React.MouseEvent) => {
this.Snapshot();
e.stopPropagation();
e.preventDefault();
}
+ onTimelineHdlDown = action((e: React.PointerEvent) => {
+ this._clicking = true;
+ setupMoveUpEvents(this, e,
+ action((e: PointerEvent) => {
+ this._clicking = false;
+ if (this.active()) {
+ const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
+ this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+ }
+ return false;
+ }), emptyFunction,
+ () => {
+ this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+ setTimeout(action(() => this._clicking = false), 500);
+ }, this.active(), this.active());
+ });
+
onResetDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e: PointerEvent) => {
- this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
- e.stopImmediatePropagation();
- return false;
- }, emptyFunction,
+ setupMoveUpEvents(this, e,
+ (e: PointerEvent) => {
+ this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
+ e.stopImmediatePropagation();
+ return false;
+ },
+ emptyFunction,
(e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
}
@@ -441,7 +442,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
const curTime = NumCast(this.layoutDoc._currentTimecode);
- docs.forEach(doc => doc._timecodeToShow = curTime);
+ docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
return this.addDocument(doc);
}
@@ -473,62 +474,54 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
playLink = (doc: Doc) => {
- const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow));
- const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null));
+ const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0;
+ const endTime = this._stackedTimeline.current?.anchorEnd(doc);
if (startTime !== undefined) {
if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
else this.Seek(startTime);
}
}
- // returns the timeline
+ playing = () => this._playing;
+ isActiveChild = () => this._isChildActive;
+ timelineWhenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(runInAction(() => this._isChildActive = isActive));
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+ setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+ timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
@computed get renderTimeline() {
- return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}>
- <CollectionStackedTimeline ref={this._stackedTimeline}
- Document={this.props.Document}
+ return <div style={{ width: "100%", transition: this.transition, height: `${100 - this.heightPercent}%`, position: "absolute" }}>
+ <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
fieldKey={this.annotationKey}
renderDepth={this.props.renderDepth + 1}
- parentActive={this.props.parentActive}
+ startTag={"videoStart"}
+ endTag={"videoEnd"}
+ fieldKeySuffix={"-timeline"}
focus={emptyFunction}
- styleProvider={this.props.styleProvider}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
- searchFilterDocs={this.props.searchFilterDocs}
- rootSelected={this.props.rootSelected}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
bringToFront={emptyFunction}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
CollectionView={undefined}
duration={this.duration}
playFrom={this.playFrom}
- setTime={(time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time}
- playing={() => this._playing}
- select={this.props.select}
- isSelected={this.props.isSelected}
- whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
+ setTime={this.setAnchorTime}
+ playing={this.playing}
+ whenActiveChanged={this.timelineWhenActiveChanged}
removeDocument={this.removeDocument}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())}
- isChildActive={() => this._isChildActive}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ isChildActive={this.isActiveChild}
Play={this.Play}
Pause={this.Pause}
active={this.active}
playLink={this.playLink}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100}
+ PanelHeight={this.timelineHeight}
/>
</div>;
}
- contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
@computed get annotationLayer() {
- return <div className="imageBox-annotationLayer" style={{ height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+ return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
marqueeDown = action((e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY];
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.active(true)) this._marqueeing = [e.clientX, e.clientY];
});
finishMarquee = action(() => {
@@ -536,6 +529,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
this.props.select(true);
});
+ contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
scaling = () => this.props.scaling?.() || 1;
panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * this.heightPercent / 100;
@@ -543,6 +537,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
}
+ marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
+ marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -553,7 +549,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
borderRadius
}} >
<div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
- <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}>
+ <div style={{ position: "absolute", transition: this.transition, 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}
@@ -577,7 +573,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
{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.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />}
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ down={this._marqueeing}
+ scaling={this.marqueeFitScaling}
+ containerOffset={this.marqueeOffset}
+ addDocument={this.addDocWithTimecode}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this._savedAnnotations}
+ annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current}
+ />}
</div>
</div >);
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 37f268823..4b7f0bf77 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -462,9 +462,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
@action
onMarqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.active(true)) {
- this._marqueeing = [e.clientX, e.clientY];
- }
+ if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY];
}
@action
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index b04f60500..81bca4c00 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -10,6 +10,21 @@
outline: none !important;
}
+audiotag {
+ left: 0;
+ position: absolute;
+ cursor: pointer;
+ border-radius: 10px;
+ width: 10px;
+ margin-top: -2px;
+ font-size: 4px;
+ background: lightblue;
+}
+audiotag:hover {
+ transform: scale(2);
+ transform-origin: bottom center;
+}
+
.formattedTextBox-cont {
touch-action: none;
background: inherit;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e06a324d2..d24ccd9ad 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -66,6 +66,7 @@ import { SubCollectionViewProps } from '../../collections/CollectionSubView';
import { StyleProp } from '../../StyleProvider';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
+import { DocumentManager } from '../../../util/DocumentManager';
export interface FormattedTextBoxProps {
makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
@@ -99,10 +100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _undoTyping?: UndoManager.Batch;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _dropDisposer?: DragManager.DragDropDisposer;
- private _first: Boolean = true;
private _recordingStart: number = 0;
- private _currentTime: number = 0;
- private _linkTime: number | null = null;
private _pause: boolean = false;
private _animatingScroll: number = 0; // hack to prevent scroll values from being written to document when scroll is animating
@@ -341,39 +339,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._editorView.updateState(EditorState.fromJSON(this.config, json));
}
}
+ if (window.getSelection()?.isCollapsed) AnchorMenu.Instance.fadeOut(true);
}
}
pause = () => this._pause = true;
- formatTime = (time: number) => {
- const hours = Math.floor(time / 60 / 60);
- const minutes = Math.floor(time / 60) - (hours * 60);
- const seconds = time % 60;
-
- return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
- }
-
// for inserting timestamps
insertTime = () => {
- let audioState;
- if (this._first) {
- DocListCast(this.dataDoc.links).map((l, i) => {
- let la1 = l.anchor1 as Doc;
- let la2 = l.anchor2 as Doc;
- this._linkTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime));
- audioState = la2.audioState;
- if (Doc.AreProtosEqual(la2, this.dataDoc)) {
- la1 = l.anchor2 as Doc;
- la2 = l.anchor1 as Doc;
- audioState = la1.audioState;
- }
- });
- }
- this._currentTime = Date.now();
- let time;
- this._linkTime ? time = this.formatTime(Math.round(this._linkTime + this._currentTime / 1000 - this._recordingStart / 1000)) : time = null;
-
+ let linkTime;
+ let linkAnchor;
+ DocListCast(this.dataDoc.links).forEach((l, i) => {
+ const anchor = (l.anchor1 as Doc).annotationOn ? l.anchor1 as Doc : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
+ if (anchor && (anchor.annotationOn as Doc).audioState === "recording") {
+ linkTime = NumCast(anchor.audioStart);
+ linkAnchor = anchor;
+ }
+ });
if (this._editorView) {
const state = this._editorView.state;
const now = Date.now();
@@ -388,13 +370,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
}
- if (time && audioState === "recording") {
- let value = "";
+
+ const path = (this._editorView.state.selection.$from as any).path;
+ if (linkAnchor && linkTime && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) {
+ const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000;
this._break = false;
- value = this.layoutDoc._timeStampOnEnter ? "[" + time + "] " : "\n" + "[" + time + "] ";
const from = state.selection.from;
- const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
- this._editorView.dispatch(this._editorView.state.tr.insertText(value));
+ const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] });
+ const replaced = this._editorView.state.tr.insert(from - 1, value);
+ this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
}
}
}
@@ -568,10 +552,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
return ret;
}
- static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"];
+ static _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"];
updateHighlights = () => {
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+ if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, "");
+ }
if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
}
@@ -643,8 +630,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
});
!Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" });
const highlighting: ContextMenuProps[] = [];
- const noviceHighlighting = ["My Text", "Text from Others"];
- const expertHighlighting = ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"];
+ const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others"];
+ const expertHighlighting = [...noviceHighlighting, "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"];
(Doc.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
highlighting.push({
description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
@@ -1302,6 +1289,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.endUndoTypingBatch();
this.unhighlightSearchTerms();
this._editorView?.destroy();
+ RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined);
FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
}
@@ -1311,6 +1299,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
_break = false;
_collapsed = false;
onPointerDown = (e: React.PointerEvent): void => {
+ if ((e.target as any).tagName === "AUDIOTAG") {
+ e.preventDefault();
+ e.stopPropagation();
+ const time = (e.target as any)?.dataset?.timecode || 0;
+ const audioid = (e.target as any)?.dataset?.audioid || 0;
+ DocServer.GetRefField(audioid).then(anchor => {
+ if (anchor instanceof Doc) {
+ const audiodoc = anchor.annotationOn as Doc;
+ audiodoc._triggerAudio = Number(time);
+ !DocumentManager.Instance.getDocumentView(audiodoc) && this.props.addDocTab(audiodoc, "add:bottom");
+ }
+ });
+ }
if (this._recording && !e.ctrlKey && e.button === 0) {
this.stopDictation(true);
this._break = true;
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
index d272b6b8c..abbb89780 100644
--- a/src/client/views/nodes/formattedText/RichTextSchema.tsx
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -136,6 +136,8 @@ export class DashDocView {
addDocument={returnFalse}
rootSelected={this._textBox.props.isSelected}
removeDocument={removeDoc}
+ layerProvider={this._textBox.props.layerProvider}
+ styleProvider={this._textBox.props.styleProvider}
ScreenToLocalTransform={this.getDocTransform}
addDocTab={this._textBox.props.addDocTab}
pinToPres={returnFalse}
@@ -143,7 +145,6 @@ export class DashDocView {
PanelWidth={finalLayout[WidthSym]}
PanelHeight={finalLayout[HeightSym]}
focus={this.outerFocus}
- styleProvider={DefaultStyleProvider}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
bringToFront={emptyFunction}
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 64f7d27e5..722c0a836 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -6,6 +6,14 @@ import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from "./Para
const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+function formatAudioTime(time: number) {
+ time = Math.round(time);
+ const hours = Math.floor(time / 60 / 60);
+ const minutes = Math.floor(time / 60) - (hours * 60);
+ const seconds = time % 60;
+
+ return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
+}
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes: { [index: string]: NodeSpec } = {
@@ -14,6 +22,34 @@ export const nodes: { [index: string]: NodeSpec } = {
content: "block+"
},
+ audiotag: {
+ group: "block",
+ attrs: {
+ timeCode: { default: 0 },
+ audioId: { default: "" }
+ },
+ toDOM(node) {
+ return ['audiotag',
+ {
+ // style: see FormattedTextBox.scss
+ "data-timecode": node.attrs.timeCode,
+ "data-audioid": node.attrs.audioId,
+ },
+ formatAudioTime(node.attrs.timeCode.toString())
+ ];
+ },
+ parseDOM: [
+ {
+ tag: "audiotag", getAttrs(dom: any) {
+ return {
+ timeCode: dom.getAttribute("data-timecode"),
+ audioId: dom.getAttribute("data-audioid")
+ };
+ }
+ },
+ ]
+ },
+
footnote: {
group: "inline",
content: "inline*",
@@ -315,7 +351,7 @@ export const nodes: { [index: string]: NodeSpec } = {
mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet"
visibility: { default: true }
},
- content: 'paragraph+ | (paragraph ordered_list)',
+ content: '(paragraph|audiotag)+ | ((paragraph|audiotag)+ ordered_list)',
parseDOM: [{
tag: "li", getAttrs(dom: any) {
return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") };