aboutsummaryrefslogtreecommitdiff
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
parent888fb3b3933e7aa48e9ac3abe85536328fcca336 (diff)
parent41bb365dd4f787aec2262dcb07508e0de3837e10 (diff)
more merge
-rw-r--r--src/Utils.ts31
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/views/DocComponent.tsx7
-rw-r--r--src/client/views/DocumentDecorations.scss38
-rw-r--r--src/client/views/DocumentDecorations.tsx8
-rw-r--r--src/client/views/MainView.scss2
-rw-r--r--src/client/views/MarqueeAnnotator.tsx6
-rw-r--r--src/client/views/StyleProvider.tsx4
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss406
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx293
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx41
-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
-rw-r--r--src/fields/documentSchemas.ts2
25 files changed, 524 insertions, 1080 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 3cf695a30..c7074c3da 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -500,9 +500,9 @@ export function addStyleSheet(styleType: string = "text/css") {
const sheets = document.head.appendChild(style);
return (sheets as any).sheet;
}
-export function addStyleSheetRule(sheet: any, selector: any, css: any) {
+export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = ".") {
const propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
- return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
+ return sheet.insertRule(selectorPrefix + selector + "{" + propText + "}", sheet.cssRules.length);
}
export function removeStyleSheetRule(sheet: any, rule: number) {
if (sheet.rules.length) {
@@ -600,17 +600,32 @@ export function hasDescendantTarget(x: number, y: number, target: HTMLDivElement
return entered;
}
+export function StopEvent(e: React.PointerEvent | React.MouseEvent) {
+ e.stopPropagation();
+ e.preventDefault();
+}
+
export function setupMoveUpEvents(
target: object,
e: React.PointerEvent,
moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
- upEvent: (e: PointerEvent, movement: number[]) => any,
+ upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => any,
clickEvent: (e: PointerEvent, doubleTap?: boolean) => any,
stopPropagation: boolean = true,
- stopMovePropagation: boolean = true
+ stopMovePropagation: boolean = true,
+ noDoubleTapTimeout?: () => void
) {
+ const doubleTapTimeout = 300;
+ (target as any)._doubleTap = (Date.now() - (target as any)._lastTap < doubleTapTimeout);
+ (target as any)._lastTap = Date.now();
(target as any)._downX = (target as any)._lastX = e.clientX;
(target as any)._downY = (target as any)._lastY = e.clientY;
+ if (!(target as any)._doubleTime && noDoubleTapTimeout) {
+ (target as any)._doubleTime = setTimeout(() => {
+ noDoubleTapTimeout?.();
+ (target as any)._doubleTime = undefined;
+ }, doubleTapTimeout);
+ }
const _moveEvent = (e: PointerEvent): void => {
if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
@@ -628,12 +643,10 @@ export function setupMoveUpEvents(
(target as any)._lastY = e.clientY;
stopMovePropagation && e.stopPropagation();
};
- (target as any)._doubleTap = false;
const _upEvent = (e: PointerEvent): void => {
- (target as any)._doubleTap = (Date.now() - (target as any)._lastTap < 300);
- (target as any)._lastTap = Date.now();
- upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY]);
- if (Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4) {
+ const isClick = Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4;
+ upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY], isClick);
+ if (isClick) {
if ((target as any)._doubleTime && (target as any)._doubleTap) {
clearTimeout((target as any)._doubleTime);
(target as any)._doubleTime = undefined;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 080270321..1a4aae17e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -153,8 +153,6 @@ export interface DocumentOptions {
_currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
_timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays)
_timecodeToHide?: number; // the time that a document should be hidden
- anchorStartTime?: number; // the time when an annotation starts on a timeline (e.g., when an audio anchor starts on an audio/video timeline)
- anchorEndTime?: number; // the time when an annotaiton ends on a timeline
lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
activeFrame?: number; // the active frame of a document in a frame base collection
appearFrame?: number; // the frame in which the document appears
@@ -722,8 +720,6 @@ export namespace Docs {
linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document.
linkDocProto.anchor1 = source.doc;
linkDocProto.anchor2 = target.doc;
- linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc._timecodeToShow;
- linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc._timecodeToShow;
if (linkDocProto.linkBoxExcludedKeys === undefined) {
Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]);
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 8d545c61b..b800ba777 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -74,6 +74,7 @@ export interface ViewBoxAnnotatableProps {
fieldKey: string;
layerProvider?: (doc: Doc) => boolean;
active: () => boolean;
+ select: (isCtrlPressed: boolean) => void;
whenActiveChanged: (isActive: boolean) => void;
isSelected: (outsideReaction?: boolean) => boolean;
rootSelected: (outsideReaction?: boolean) => boolean;
@@ -131,7 +132,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
if (docs.length) {
const docs = doc instanceof Doc ? [doc] : doc;
- docs.map(doc => doc.isPushpin = doc.annotationOn = undefined);
+ docs.map(doc => {
+ Doc.SetInPlace(doc, "isPushpin", undefined, true);
+ Doc.SetInPlace(doc, "annotationOn", undefined, true);
+ });
const targetDataDoc = this.dataDoc;
const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
const toRemove = value.filter(v => docs.includes(v));
@@ -142,6 +146,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true);
});
+ this.props.select(false);
return true;
}
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index f9b8c1940..22e120167 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -79,7 +79,7 @@ $linkGap : 3px;
grid-column: 5;
grid-row: 4;
border-radius: 100%;
- background: dimgray;
+ background: black;
height: 8;
right: -12;
top: 12;
@@ -145,7 +145,7 @@ $linkGap : 3px;
.documentDecorations-topRightResizer:hover,
.documentDecorations-bottomLeftResizer:hover {
cursor: nesw-resize;
- background: dimGray;
+ background: black;
opacity: 1;
}
@@ -169,6 +169,14 @@ $linkGap : 3px;
cursor: pointer;
}
+ .documentDecorations-titleBackground {
+ background: #ffffffcf;
+ border-radius: 8px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ }
+
.documentDecorations-title {
opacity: 1;
grid-column-start: 2;
@@ -177,26 +185,22 @@ $linkGap : 3px;
overflow: hidden;
text-align: center;
display: flex;
- border-bottom: solid 1px;
- margin-left: 10px;
- width: calc(100% - 10px);
+ margin-left: 5px;
+ height: 22px;
+ position: absolute;
+ .documentDecorations-titleSpan {
+ width: 100%;
+ border-radius: 8px;
+ background: #ffffffcf;
+ position: absolute;
+ display: inline-block;
+ cursor: move;
+ }
}
.focus-visible {
margin-left: 0px;
}
-
- .publishBox {
- width: 20px;
- height: 22px;
- grid-column-start: 3;
- grid-column-end: 4;
- pointer-events: all;
- background: darkgray;
- display: inline-block;
- position: absolute;
- right: 0;
- }
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index fb85684fb..8d8f4cd3b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -577,8 +577,8 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
const titleArea = this._edtingTitle ?
<input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle}
onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> :
- <div className="documentDecorations-title" style={{ gridColumnEnd: 5 }} key="title" onPointerDown={this.onTitleDown} >
- <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span>
+ <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
+ <span className="documentDecorations-titleSpan">{`${this.selectionTitle}`}</span>
</div>;
let inMainMenuPanel = false;
@@ -612,8 +612,8 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
}}>
{closeIcon}
- {bounds.r - bounds.x < 100 ? null : titleArea}
- {seldoc.rootDoc.anchorStartTime !== undefined ? (null) :
+ {titleArea}
+ {seldoc.props.hideResizeHandles ? (null) :
<>
{SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
<Tooltip key="i" title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top">
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index d6a455a22..8ccb64744 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -61,7 +61,7 @@
}
.mainView-container {
- color: dimgray;
+ color: black;
.lm_title {
background: #cacaca;
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 8ef69802b..4ffccbc90 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -21,6 +21,7 @@ export interface MarqueeAnnotatorProps {
rootDoc: Doc;
down: number[];
scaling?: () => number;
+ containerOffset?: () => number[];
mainCont: HTMLDivElement;
savedAnnotations: Dictionary<number, HTMLDivElement[]>;
annotationLayer: HTMLDivElement;
@@ -69,9 +70,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
if ((this.props.savedAnnotations.values()[0][0] as any).marqueeing) {
const scale = this.props.scaling?.() || 1;
const anno = this.props.savedAnnotations.values()[0][0];
+ const containerOffset = this.props.containerOffset?.() || [0, 0];
const mainAnnoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title });
- if (anno.style.left) mainAnnoDoc.x = parseInt(anno.style.left) / scale;
- if (anno.style.top) mainAnnoDoc.y = (NumCast(this.props.rootDoc._scrollTop) + parseInt(anno.style.top)) / scale;
+ if (anno.style.left) mainAnnoDoc.x = (parseInt(anno.style.left) - containerOffset[0]) / scale;
+ if (anno.style.top) mainAnnoDoc.y = (parseInt(anno.style.top) - containerOffset[1] + NumCast(this.props.rootDoc._scrollTop)) / scale;
if (anno.style.height) mainAnnoDoc._height = parseInt(anno.style.height) / scale;
if (anno.style.width) mainAnnoDoc._width = parseInt(anno.style.width) / scale;
mainAnnoDoc.group = mainAnnoDoc;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index a91de3c4b..8939eac3e 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -114,7 +114,7 @@ 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 || doc?.anchorStartTime !== undefined || doc?._timecodeToShow !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break;
+ case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== 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;
@@ -147,7 +147,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps |
`${darkScheme() ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(doc.boxShadow, "0.2vw 0.2vw 0.8vw")}`);
case DocumentType.LABEL:
- if (doc?.anchorStartTime !== undefined) return "black 2px 2px 1px";
+ if (doc?.annotationOn !== undefined) return "black 2px 2px 1px";
default:
return doc.z ? `#9c9396 ${StrCast(doc?.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow
props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK ? (`${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.ContentScaling?.() || 1)}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 1bb5bc720..cc56831f3 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,373 +1,61 @@
-.audiobox-container,
-.audiobox-container-interactive {
+.collectionStackedTimeline {
+ position: absolute;
width: 100%;
height: 100%;
- position: inherit;
- display: flex;
- position: relative;
- cursor: default;
-
- .audiobox-inner {
- width:100%;
- height: 100%;
- }
-
- .audiobox-buttons {
- display: flex;
- width: 100%;
- align-items: center;
- height: 100%;
-
- .audiobox-dictation {
- position: relative;
- width: 30px;
- height: 100%;
- align-items: center;
- display: inherit;
- background: dimgray;
- left: 0px;
- }
-
- .audiobox-dictation:hover {
- color: white;
- cursor: pointer;
- }
- }
-
- .audiobox-handle {
- width: 20px;
- height: 100%;
- display: inline-block;
- }
-
- .audiobox-control,
- .audiobox-control-interactive {
- top: 0;
- max-height: 32px;
- width: 100%;
- display: inline-block;
- pointer-events: none;
- }
-
- .audiobox-control-interactive {
- pointer-events: all;
- }
-
- .audiobox-record {
- pointer-events: all;
- width: 100%;
+ border: gray solid 1px;
+ border-radius: 3px;
+ z-index: 1000;
+ overflow: hidden;
+ top: 0px;
+
+ .collectionStackedTimeline-selector {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 95%;
+ background: lightblue;
+ border-radius: 5px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: darkblue;
+ border-width: 1px;
+ }
+
+ .collectionStackedTimeline-current {
+ width: 1px;
height: 100%;
- position: relative;
+ background-color: red;
+ position: absolute;
+ top: 0px;
pointer-events: none;
}
- .audiobox-record-interactive {
- pointer-events: all;
- width: 100%;
- height: 100%;
- position: relative;
-
-
- }
-
- .recording {
- margin-top: auto;
- margin-bottom: auto;
- width: 100%;
- height: 100%;
- position: relative;
- padding-right: 5px;
- display: flex;
- background-color: red;
-
- .time {
- position: relative;
- height: 100%;
- width: 100%;
- font-size: 20;
- text-align: center;
- top: 5;
- }
-
- .buttons {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- padding: 5px;
- }
-
- .buttons:hover {
- background-color: crimson;
+ .collectionStackedTimeline-marker-timeline {
+ position: absolute;
+ top: 2.5%;
+ height: 95%;
+ border-radius: 4px;
+ &:hover {
+ opacity: 1;
}
- }
-
- .audiobox-controls {
- width: 100%;
- height: 100%;
- position: relative;
- display: flex;
- padding-left: 2px;
- background: black;
- .audiobox-dictation {
+ .collectionStackedTimeline-left-resizer,
+ .collectionStackedTimeline-resizer {
+ background: dimgrey;
position: absolute;
- width: 30px;
+ top: 0;
height: 100%;
- align-items: center;
- display: inherit;
- background: dimgray;
- left: 0px;
+ width: 10px;
+ pointer-events: all;
+ cursor: ew-resize;
+ z-index: 100;
}
-
- .audiobox-player {
- margin-top: auto;
- margin-bottom: auto;
- width: 100%;
- position: relative;
- padding-right: 5px;
- display: flex;
-
- .audiobox-playhead {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- margin-right: 2px;
- height: 25px;
- padding: 2px;
- border-radius: 50%;
- background-color: black;
- color: white;
- }
-
- .audiobox-playhead:hover {
- // background-color: black;
- // border-radius: 5px;
- background-color: grey;
- color: lightgrey;
- }
-
- .audiobox-dictation {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- padding: 2px;
- align-items: center;
- display: inherit;
- background: dimgray;
- }
-
- .audiobox-timeline {
- position: absolute;
- width: 100%;
- border: gray solid 1px;
- border-radius: 3px;
- z-index: 1000;
- overflow: hidden;
-
- .audiobox-container {
- position: absolute;
- width: 10px;
- top: 2.5%;
- height: 0px;
- background: lightblue;
- border-radius: 5px;
- // box-shadow: black 2px 2px 1px;
- opacity: 0.3;
- z-index: 500;
- border-style: solid;
- border-color: darkblue;
- border-width: 1px;
- }
-
- .audiobox-current {
- width: 1px;
- height: 100%;
- background-color: red;
- position: absolute;
- top: 0px;
- }
-
- .waveform {
- position: relative;
- width: 100%;
- height: 100%;
- overflow: hidden;
- z-index: -1000;
- bottom: 0;
- pointer-events: none;
- div {
- height: 100% !important;
- width: 100% !important;
- }
- canvas {
- height: 100% !important;
- width: 100% !important;
- }
- }
-
- .audiobox-linker,
- .audiobox-linker-mini {
- position: absolute;
- width: 15px;
- min-height: 10px;
- height: 15px;
- margin-left: -2.55px;
- background: gray;
- border-radius: 100%;
- opacity: 0.9;
- box-shadow: black 2px 2px 1px;
-
- .linkAnchorBox-cont {
- position: relative !important;
- height: 100% !important;
- width: 100% !important;
- left: unset !important;
- top: unset !important;
- }
- }
-
- .audiobox-linker-mini {
- width: 8px;
- min-height: 8px;
- height: 8px;
- box-shadow: black 1px 1px 1px;
- margin-left: -1;
- margin-top: -2;
-
- .linkAnchorBox-cont {
- position: relative !important;
- height: 100% !important;
- width: 100% !important;
- left: unset !important;
- top: unset !important;
- }
- }
-
- .audiobox-linker:hover,
- .audiobox-linker-mini:hover {
- opacity: 1;
- }
-
- .audiobox-marker-container,
- .audiobox-marker-minicontainer {
- position: absolute;
- width: 10px;
- height: 10px;
- top: 2.5%;
- background: gray;
- border-radius: 50%;
- box-shadow: black 2px 2px 1px;
- overflow: visible;
- cursor: pointer;
-
- .audiobox-marker {
- position: relative;
- height: 100%;
- // height: calc(100% - 15px);
- width: 100%;
- //margin-top: 15px;
- }
-
- .audio-marker:hover {
- border: orange 2px solid;
- }
- }
-
- .audiobox-marker-timeline,
- .audiobox-marker-minicontainer {
- position: absolute;
- width: 10px;
- height: 90%;
- top: 2.5%;
- border-radius: 5px;
-
- .left-resizer {
- background: dimgrey;
- }
- .resizer {
- background: dimgrey;
- }
-
- .audiobox-marker {
- position: relative;
- height: calc(100% - 15px);
- margin-top: 15px;
- }
-
- .audio-marker:hover {
- border: orange 2px solid;
- }
-
- .resizer {
- position: absolute;
- top: 0;
- right: 0;
- pointer-events: all;
- cursor: ew-resize;
- height: 100%;
- width: 10px;
- z-index: 100;
- }
-
- .click {
- position: relative;
- height: 100%;
- width: 100%;
- z-index: 100;
- }
-
- .left-resizer {
- position: absolute;
- left: 0;
- top : 0;
- pointer-events: all;
- cursor: ew-resize;
- height: 100%;
- width: 10px;
- z-index: 100;
- }
- }
-
- .audiobox-marker-timeline:hover,
- .audiobox-marker-minicontainer:hover {
- opacity: 0.8;
- }
-
- .audiobox-marker-minicontainer {
- width: 5px;
- border-radius: 1px;
-
- .audiobox-marker {
- position: relative;
- height: calc(100% - 8px);
- margin-top: 8px;
- }
- }
- }
+ .collectionStackedTimeline-resizer {
+ right: 0;
+ }
+ .collectionStackedTimeline-left-resizer {
+ left: 0;
}
- }
-}
-
-@media only screen and (max-device-width: 480px) {
- .audiobox-dictation {
- font-size: 5em;
- display: flex;
- width: 100;
- justify-content: center;
- flex-direction: column;
- align-items: center;
- }
-
- .audiobox-container .audiobox-record,
- .audiobox-container-interactive .audiobox-record {
- font-size: 3em;
- }
-
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
- .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
- width: 70px;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 1775250fa..d4eb66fcc 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,17 +1,18 @@
import React = require("react");
-import { action, computed, IReactionDisposer, observable } from "mobx";
+import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
-import { Doc, Opt } from "../../../fields/Doc";
+import { Doc, Opt, DocListCast } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec, makeInterface } from "../../../fields/Schema";
import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents, StopEvent } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { Scripting } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
import { CollectionSubView } from "../collections/CollectionSubView";
import { DocumentView } from "../nodes/DocumentView";
import { LabelBox } from "../nodes/LabelBox";
@@ -28,69 +29,80 @@ export type CollectionStackedTimelineProps = {
playing: () => boolean;
setTime: (time: number) => void;
isChildActive: () => boolean;
+ startTag: string;
+ endTag: string;
+ fieldKeySuffix?: string;
};
@observer
export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) {
+ @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
static RangeScript: ScriptField;
static LabelScript: ScriptField;
static RangePlayScript: ScriptField;
static LabelPlayScript: ScriptField;
- _disposers: { [name: string]: IReactionDisposer } = {};
- _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name
- _ele: HTMLAudioElement | null = null;
- _start: number = 0;
- _left: boolean = false;
- _dragging = false;
- _play: any = null;
- _audioRef = React.createRef<HTMLDivElement>();
- _timeline: Opt<HTMLDivElement>;
- _markerStart: number = 0;
- _currAnchor: Opt<Doc>;
-
- @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
+ private _timeline: HTMLDivElement | null = null;
+ private _markerStart: number = 0;
@observable _markerEnd: number = 0;
- @observable _position: number = 0;
- @computed get anchorDocs() { return this.childDocs; }
- @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); }
+
+ get duration() { return this.props.duration; }
+ @computed get anchorDocs() { return this.props.fieldKeySuffix ? this.childDocs.concat(...DocListCast(this.rootDoc[this.props.fieldKey + this.props.fieldKeySuffix])) : this.childDocs; }
+ @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
+ @computed get selectionContainer() {
+ return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{
+ left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`,
+ width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`
+ }} />;
+ }
constructor(props: any) {
super(props);
// onClick play scripts
- CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
- CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
- CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
- CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
+ CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
+ CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
+ CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
+ CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!;
+ }
+
+ componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); }
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.keyEvents, true);
+ if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined);
+ }
+
+ anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
+ anchorEnd = (anchor: Doc, val: any = null) => {
+ const endVal = NumCast(anchor[this.props.endTag], val);
+ return NumCast(anchor._timecodeToHide, endVal === undefined ? null : endVal);
}
+ toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration));
+ rangeClickScript = () => CollectionStackedTimeline.RangeScript;
+ labelClickScript = () => CollectionStackedTimeline.LabelScript;
+ rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
+ labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
// for creating key anchors with key events
@action
keyEvents = (e: KeyboardEvent) => {
- if (e.target instanceof HTMLInputElement) return;
- if (!this.props.playing()) return; // can't create if video is not playing
- switch (e.key) {
- case "x": // currently set to x, but can be a different key
- const currTime = this.currentTime;
- if (this._start) {
- this._markerStart = currTime;
- // this._start = false;
- // this._visible = true;
- } else {
- this.createAnchor(this._markerStart, currTime);
- // this._start = true;
- // this._visible = false;
- }
+ if (!(e.target instanceof HTMLInputElement) && this.props.isSelected(true)) {
+ switch (e.key) {
+ case " ":
+ if (!CollectionStackedTimeline.SelectingRegion) {
+ this._markerStart = this._markerEnd = this.currentTime;
+ CollectionStackedTimeline.SelectingRegion = this;
+ } else {
+ CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
+ CollectionStackedTimeline.SelectingRegion = undefined;
+ }
+ }
}
}
- anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)));
- anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)));
-
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
+ const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag]));
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
@@ -98,132 +110,134 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
return { la1, la2, linkTime };
}
- // ref for timeline
- timelineRef = (timeline: HTMLDivElement) => {
- this._timeline = timeline;
- }
-
- // updates the anchor with the new time
- @action
- changeAnchor = (anchor: Opt<Doc>, time: number) => {
- anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time);
- }
-
- // checks if the two anchors are the same with start and end time
- isSame = (m1: any, m2: any) => {
- return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2);
- }
-
- @computed get selectionContainer() {
- return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
- left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.props.duration * 100}%`,
- width: `${Math.abs(this._markerStart - this._markerEnd) / this.props.duration * 100}%`, height: "100%", top: "0%"
- }} />;
- }
-
// starting the drag event for anchor resizing
@action
onPointerDownTimeline = (e: React.PointerEvent): void => {
- const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect();
- if (rect && e.target !== this._audioRef.current && this.props.active()) {
+ const rect = this._timeline?.getBoundingClientRect();
+ const clientX = e.clientX;
+ if (rect && this.props.active()) {
const wasPlaying = this.props.playing();
if (wasPlaying) this.props.Pause();
- else if (!this._doubleTime) {
- this._doubleTime = setTimeout(() => {
- this._doubleTime = undefined;
- this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration);
- }, 300);
- }
- this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
- CollectionStackedTimeline.SelectingRegion = this;
+ const wasSelecting = CollectionStackedTimeline.SelectingRegion === this;
setupMoveUpEvents(this, e,
action(e => {
+ if (!wasSelecting && CollectionStackedTimeline.SelectingRegion !== this) {
+ this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
+ CollectionStackedTimeline.SelectingRegion = this;
+ }
this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
return false;
}),
- action((e, movement) => {
+ action((e, movement, isClick) => {
this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
if (this._markerEnd < this._markerStart) {
const tmp = this._markerStart;
this._markerStart = this._markerEnd;
this._markerEnd = tmp;
}
- CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
- CollectionStackedTimeline.SelectingRegion = undefined;
+ if (!isClick && CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15)) {
+ CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag,
+ this._markerStart, this._markerEnd);
+ }
+ (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined);
}),
(e, doubleTap) => {
this.props.select(false);
- e.shiftKey && this.createAnchor(this.currentTime);
+ e.shiftKey && CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
!wasPlaying && doubleTap && this.props.Play();
- }
- , this.props.isSelected(true) || this.props.isChildActive());
+ },
+ this.props.isSelected(true) || this.props.isChildActive(), undefined,
+ () => !wasPlaying && this.props.setTime((clientX - rect.x) / rect.width * this.duration));
}
}
+ @undoBatch
@action
- createAnchor(anchorStartTime?: number, anchorEndTime?: number) {
- if (anchorStartTime === undefined) return this.props.Document;
+ static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) {
+ if (anchorStartTime === undefined) return rootDoc;
const anchor = Docs.Create.LabelDocument({
- title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any,
+ title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any,
useLinkSmallAnchor: true,
hideLinkButton: true,
- anchorStartTime,
- anchorEndTime,
- annotationOn: this.props.Document
+ annotationOn: rootDoc
});
- if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) {
- Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor);
+ Doc.GetProto(anchor)[startTag] = anchorStartTime;
+ Doc.GetProto(anchor)[endTag] = anchorEndTime;
+ if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+ Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
} else {
- this.dataDoc[this.props.fieldKey] = new List<Doc>([anchor]);
+ dataDoc[fieldKey] = new List<Doc>([anchor]);
}
return anchor;
}
- // play back the audio from time
@action
- playOnClick = (anchorDoc: Doc) => {
- this.props.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.props.duration));
+ playOnClick = (anchorDoc: Doc, clientX: number) => {
+ const seekTimeInSeconds = this.anchorStart(anchorDoc);
+ const endTime = this.anchorEnd(anchorDoc);
+ if (this.layoutDoc.autoPlay) {
+ if (this.props.playing()) this.props.Pause();
+ else this.props.playFrom(seekTimeInSeconds, endTime);
+ } else {
+ if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
+ if (!this.layoutDoc.autoPlay && this.props.playing()) {
+ this.props.Pause();
+ } else {
+ this.props.Play();
+ }
+ } else {
+ this.props.playFrom(seekTimeInSeconds, endTime);
+ }
+ }
return { select: true };
}
- // play back the audio from time
@action
- clickAnchor = (anchorDoc: Doc) => {
- if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc);
- this.props.setTime(this.anchorStart(anchorDoc));
+ clickAnchor = (anchorDoc: Doc, clientX: number) => {
+ const seekTimeInSeconds = this.anchorStart(anchorDoc);
+ const endTime = this.anchorEnd(anchorDoc);
+ if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
+ if (this.props.playing()) this.props.Pause();
+ else if (this.layoutDoc.autoPlay) this.props.Play();
+ else if (!this.layoutDoc.autoPlay) {
+ const rect = this._timeline?.getBoundingClientRect();
+ rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
+ }
+ } else {
+ if (this.layoutDoc.autoPlay) this.props.playFrom(seekTimeInSeconds, endTime);
+ else this.props.setTime(seekTimeInSeconds);
+ }
return { select: true };
}
- toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.duration));
+
// starting the drag event for anchor resizing
- onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => {
- this._currAnchor = m;
- this._left = left;
+ onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
this._timeline?.setPointerCapture(e.pointerId);
+ const newTime = (e: PointerEvent) => {
+ const rect = (e.target as any).getBoundingClientRect();
+ return this.toTimeline(e.clientX - rect.x, rect.width);
+ };
+ const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
+ const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
+ if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
+ else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time;
+ return false;
+ };
setupMoveUpEvents(this, e,
+ (e) => changeAnchor(anchor, left, newTime(e)),
(e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width));
- return false;
- },
- (e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width));
+ this.props.setTime(newTime(e));
this._timeline?.releasePointerCapture(e.pointerId);
},
emptyFunction);
}
- rangeClickScript = () => CollectionStackedTimeline.RangeScript;
- labelClickScript = () => CollectionStackedTimeline.LabelScript;
- rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
- labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
-
// makes sure no anchors overlaps each other by setting the correct position and width
getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
const timelineContentWidth = this.props.PanelWidth();
const x1 = this.anchorStart(m);
- const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration);
+ const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration);
let max = 0;
const overlappedLevels = new Set(placed.map(p => {
const y1 = p.anchorStartTime;
@@ -248,21 +262,19 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
ref={action((r: DocumentView | null) => anchor.view = r)}
Document={mark}
DataDoc={undefined}
- PanelWidth={() => width}
- PanelHeight={() => height}
renderDepth={this.props.renderDepth + 1}
- focus={() => this.props.playLink(mark)}
- rootSelected={returnFalse}
LayoutTemplate={undefined}
LayoutTemplateString={LabelBox.LayoutString("data")}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={this.props.removeDocument}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)}
- parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()}
- whenActiveChanged={this.props.whenActiveChanged}
+ focus={() => this.props.playLink(mark)}
+ parentActive={out => this.props.isSelected(out) || this.props.isChildActive()}
+ rootSelected={returnFalse}
onClick={script}
onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
ignoreAutoHeight={false}
+ hideResizeHandles={true}
bringToFront={emptyFunction}
scriptContext={this} />
};
@@ -273,8 +285,8 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
{inner.view}
{!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.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)} />
+ <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, mark, true)} />
+ <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, mark, false)} />
</>}
</>;
});
@@ -285,39 +297,28 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument
const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- return <div className="audiobox-timeline" style={{ height: "100%", width: "100%" }} ref={this.timelineRef}
- onClick={e => {
- if (this.props.isChildActive() || this.props.isSelected(false)) {
- e.stopPropagation(); e.preventDefault();
- }
- }}
- onPointerDown={e => {
- if (this.props.isChildActive() || this.props.isSelected(false)) {
- e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e);
- }
- }}>
+ const isActive = this.props.isChildActive() || this.props.isSelected(false);
+ return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline}
+ onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}>
{drawAnchors.map(d => {
- const m = d.anchor;
- const start = this.anchorStart(m);
- const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.props.duration);
- const left = start / this.props.duration * timelineContentWidth;
+ const start = this.anchorStart(d.anchor);
+ const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration);
+ const left = start / this.duration * timelineContentWidth;
const top = d.level / maxLevel * timelineContentHeight;
const timespan = end - start;
return this.props.Document.hideAnchors ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]}
- style={{ left, top, width: `${timespan / this.props.duration * 100}%`, height: `${1 / maxLevel * 100}%` }}
- onClick={e => { this.props.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} >
- {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript,
+ <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]}
+ style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${timelineContentHeight / maxLevel}px` }}
+ onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} >
+ {this.renderAnchor(d.anchor, this.rangeClickScript, this.rangePlayScript,
left,
top,
- timelineContentWidth * timespan / this.props.duration,
+ timelineContentWidth * timespan / this.duration,
timelineContentHeight / maxLevel)}
</div>;
})}
{this.selectionContainer}
- <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- style={{ left: `${this.currentTime / this.props.duration * 100}%`, pointerEvents: "none" }}
- />
+ <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} />
</div>;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6619205af..6c7512f7c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -816,8 +816,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
else if (this.props.active(true)) {
e.stopPropagation();
+ e.preventDefault();
if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true);
- else if (!this.props.isAnnotationOverlay) this.zoom(e.clientX, e.clientY, e.deltaY);
+ else this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+
}
this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]);
}
@@ -929,14 +931,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
} else {
const layoutdoc = Doc.Layout(doc);
- const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
+ const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
- willZoom && this.setScaleToZoom(layoutdoc, scale);
- const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0);
- const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0);
const newState = HistoryUtil.getState();
- newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
- HistoryUtil.pushState(newState);
+ let newPanX = savedState.px;
+ let newPanY = savedState.py;
+ if (!layoutdoc.annotationOn) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
+ willZoom && this.setScaleToZoom(layoutdoc, scale);
+ newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0);
+ newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0);
+ newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
+ HistoryUtil.pushState(newState);
+ }
if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
// glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
@@ -1651,18 +1657,15 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
const vfTop: number = NumCast(activeItem.presPinViewY);
const vfWidth: number = 100;
const vfHeight: number = 100;
- return (
- <>
- {!this.props.presPinView ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}>
- <div className='resizers' key={'resizer' + activeItem.id}>
- <div id="resizer-tl" className='resizer top-left' onPointerDown={this.onPointerDown}></div>
- <div id="resizer-tr" className='resizer top-right' onPointerDown={this.onPointerDown}></div>
- <div id="resizer-bl" className='resizer bottom-left' onPointerDown={this.onPointerDown}></div>
- <div id="resizer-br" className='resizer bottom-right' onPointerDown={this.onPointerDown}></div>
- </div>
- </div>}
- </>
- );
+ return !this.props.presPinView ? (null) :
+ <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}>
+ <div className='resizers' key={'resizer' + activeItem.id}>
+ <div className='resizer top-left' onPointerDown={this.onPointerDown}></div>
+ <div className='resizer top-right' onPointerDown={this.onPointerDown}></div>
+ <div className='resizer bottom-left' onPointerDown={this.onPointerDown}></div>
+ <div className='resizer bottom-right' onPointerDown={this.onPointerDown}></div>
+ </div>
+ </div>;
}
}
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") };
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index d52f3a928..b10c2b015 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -21,8 +21,6 @@ export const documentSchema = createSchema({
_currentTimecode: "number", // current play back time of a temporal document (video / audio)
_timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
isLabel: "boolean", // whether the document is a label or not (video / audio)
- anchorStartTime: "number", // the time code where a document activates (eg in Audio or video timelines)
- anchorEndTime: "number", // the time code where a document deactivates
markers: listSpec(Doc), // list of markers for audio / video
x: "number", // x coordinate when in a freeform view
y: "number", // y coordinate when in a freeform view