aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts8
-rw-r--r--src/client/documents/Documents.ts13
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/LinkManager.ts3
-rw-r--r--src/client/views/DocComponent.tsx9
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/GlobalKeyHandler.ts8
-rw-r--r--src/client/views/MarqueeAnnotator.tsx4
-rw-r--r--src/client/views/PropertiesView.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx5
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss373
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx324
-rw-r--r--src/client/views/collections/CollectionView.tsx7
-rw-r--r--src/client/views/collections/TabDocView.tsx7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx13
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx3
-rw-r--r--src/client/views/nodes/AudioBox.scss9
-rw-r--r--src/client/views/nodes/AudioBox.tsx374
-rw-r--r--src/client/views/nodes/DocumentView.tsx16
-rw-r--r--src/client/views/nodes/FilterBox.tsx18
-rw-r--r--src/client/views/nodes/LabelBox.tsx2
-rw-r--r--src/client/views/nodes/PresBox.tsx23
-rw-r--r--src/client/views/nodes/VideoBox.scss146
-rw-r--r--src/client/views/nodes/VideoBox.tsx325
-rw-r--r--src/client/views/nodes/WebBox.tsx6
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx2
-rw-r--r--src/fields/Doc.ts1
-rw-r--r--src/fields/documentSchemas.ts6
29 files changed, 1264 insertions, 453 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index f160df6f7..3cf695a30 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -614,6 +614,10 @@ export function setupMoveUpEvents(
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) {
+ if ((target as any)._doubleTime) {
+ clearTimeout((target as any)._doubleTime);
+ (target as any)._doubleTime = undefined;
+ }
if (moveEvent(e, [(target as any)._downX, (target as any)._downY],
[e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) {
document.removeEventListener("pointermove", _moveEvent);
@@ -630,6 +634,10 @@ export function setupMoveUpEvents(
(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) {
+ if ((target as any)._doubleTime && (target as any)._doubleTap) {
+ clearTimeout((target as any)._doubleTime);
+ (target as any)._doubleTime = undefined;
+ }
clickEvent(e, (target as any)._doubleTap);
}
document.removeEventListener("pointermove", _moveEvent);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index df08345f9..080270321 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -151,7 +151,10 @@ export interface DocumentOptions {
_curPage?: number;
_currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
_currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
- displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+ _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
@@ -226,8 +229,6 @@ export interface DocumentOptions {
linearViewIsExpanded?: boolean; // is linear view expanded
isLabel?: boolean; // whether the document is a label or not (video / audio)
useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox
- audioStart?: number; // the time frame where the audio should begin playing
- audioEnd?: number; // the time frame where the audio should stop playing
border?: string; //for searchbox
hovercolor?: string;
}
@@ -721,8 +722,8 @@ export namespace Docs {
linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document.
linkDocProto.anchor1 = source.doc;
linkDocProto.anchor2 = target.doc;
- linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc.displayTimecode;
- linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc.displayTimecode;
+ linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc._timecodeToShow;
+ linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc._timecodeToShow;
if (linkDocProto.linkBoxExcludedKeys === undefined) {
Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]);
@@ -1251,7 +1252,7 @@ export namespace DocUtils {
docList.forEach((d, i) => {
d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
- d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
});
});
if (x !== undefined && y !== undefined) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 1f2dd350b..41c7a1409 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -191,8 +191,8 @@ export class DocumentManager {
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
// now find the target document within the context
- if (targetDoc.displayTimecode) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
- targetDocContext._currentTimecode = targetDoc.displayTimecode;
+ if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
+ targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
finished?.();
} else { // no timecode means we need to find the context view and focus on our target
const findView = (delay: number) => {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index accf53676..bf927c5b8 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -54,7 +54,8 @@ export class LinkManager {
}, true);
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
- return DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).reduce((list, anno) =>
+ const lfield = Doc.LayoutFieldKey(anchor);
+ return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) =>
[...list, ...LinkManager.Instance.relatedLinker(anno)],
LinkManager.Instance.directLinker(anchor).slice());
}, true);
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 2c7d15ae0..8d545c61b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -81,6 +81,8 @@ export interface ViewBoxAnnotatableProps {
}
export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
+ _annotationKey: string = "annotations";
+
@observable _isChildActive = false;
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document(): T { return schemaCtor(this.props.Document); }
@@ -120,11 +122,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- _annotationKey: string = "annotations";
public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; }
@action.bound
- removeDocument(doc: Doc | Doc[]): boolean {
+ removeDocument(doc: Doc | Doc[], annotationKey?: string): boolean {
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
const indocs = doc instanceof Doc ? [doc] : doc;
const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
@@ -132,13 +133,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
const docs = doc instanceof Doc ? [doc] : doc;
docs.map(doc => doc.isPushpin = doc.annotationOn = undefined);
const targetDataDoc = this.dataDoc;
- const value = DocListCast(targetDataDoc[this.annotationKey]);
+ const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
const toRemove = value.filter(v => docs.includes(v));
if (toRemove.length !== 0) {
const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc;
toRemove.forEach(doc => {
- Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey + "-annotations", doc);
+ Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true);
});
return true;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e1b9fe8aa..fb85684fb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -613,7 +613,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
}}>
{closeIcon}
{bounds.r - bounds.x < 100 ? null : titleArea}
- {(seldoc.rootDoc.annotationOn as Doc)?.type === DocumentType.AUDIO ? (null) :
+ {seldoc.rootDoc.anchorStartTime !== undefined ? (null) :
<>
{SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
<Tooltip key="i" title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top">
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 6dc927877..3f34c0f20 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,3 +1,4 @@
+import { random } from "lodash";
import { action, observable } from "mobx";
import { DateField } from "../../fields/DateField";
import { Doc, DocListCast } from "../../fields/Doc";
@@ -14,6 +15,7 @@ import { DragManager } from "../util/DragManager";
import { GroupManager } from "../util/GroupManager";
import { SelectionManager } from "../util/SelectionManager";
import { SharingManager } from "../util/SharingManager";
+import { SnappingManager } from "../util/SnappingManager";
import { undoBatch, UndoManager } from "../util/UndoManager";
import { CollectionDockingView } from "./collections/CollectionDockingView";
import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
@@ -22,8 +24,8 @@ import { DocumentDecorations } from "./DocumentDecorations";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { MainView } from "./MainView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
+import { CollectionStackedTimeline } from "./collections/CollectionStackedTimeline";
import { AnchorMenu } from "./pdf/AnchorMenu";
-import { SnappingManager } from "../util/SnappingManager";
import { SearchBox } from "./search/SearchBox";
import { random } from "lodash";
import { DocumentView } from "./nodes/DocumentView";
@@ -123,8 +125,8 @@ export class KeyManager {
DragManager.AbortDrag();
} else if (CollectionDockingView.Instance.HasFullScreen) {
CollectionDockingView.Instance.CloseFullScreen();
- } else if (AudioBox.SelectingRegion) {
- AudioBox.SelectingRegion = undefined;
+ } else if (CollectionStackedTimeline.SelectingRegion) {
+ CollectionStackedTimeline.SelectingRegion = undefined;
doDeselect = false;
} else {
doDeselect = !ContextMenu.Instance.closeMenu();
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 0ab2d1ecf..8ef69802b 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -163,13 +163,13 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100);
FormattedTextBox.SelectOnLoad = target[Id];
return target;
- }
+ };
const anchorCreator = () => {
const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color
annoDoc.isLinkButton = true; // prevents link button from showing up --- maybe not a good thing?
this.props.addDocument(annoDoc);
return annoDoc;
- }
+ };
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, anchorCreator, targetCreator), e.pageX, e.pageY, {
dragComplete: e => {
if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 2fb543224..c6be7cae1 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -68,7 +68,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable openContexts: boolean = true;
@observable openAppearance: boolean = true;
@observable openTransform: boolean = true;
- @observable openFilters: boolean = true;
+ @observable openFilters: boolean = false;
// @observable selectedUser: string = "";
// @observable addButtonPressed: boolean = false;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 101ecb9ad..59222e985 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -113,9 +113,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps |
case DocumentType.FILTER: docColor = docColor || (darkScheme() ? "#2d2d2d" : "rgba(105, 105, 105, 0.432)"); break;
case DocumentType.INK: docColor = doc?.isInkMask ? "rgba(0,0,0,0.7)" : undefined; break;
case DocumentType.SLIDER: break;
- case DocumentType.LABEL: docColor = docColor || (doc?.audioStart !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break;
+ case DocumentType.LABEL: docColor = docColor || (doc.audioStart !== undefined || doc?.anchorStartTime !== undefined || doc?._timecodeToShow !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break;
case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break;
case DocumentType.LINK: return "transparent";
+ case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break;
case DocumentType.COL:
if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break;
docColor = docColor ? docColor :
@@ -145,7 +146,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?.audioStart !== undefined) return "black 2px 2px 1px";
+ if (doc?.anchorStartTime !== 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
new file mode 100644
index 000000000..1bb5bc720
--- /dev/null
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -0,0 +1,373 @@
+.audiobox-container,
+.audiobox-container-interactive {
+ width: 100%;
+ height: 100%;
+ position: inherit;
+ display: flex;
+ position: relative;
+ cursor: default;
+
+ .audiobox-inner {
+ width:100%;
+ height: 100%;
+ }
+
+ .audiobox-buttons {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ height: 100%;
+
+ .audiobox-dictation {
+ position: relative;
+ width: 30px;
+ height: 100%;
+ align-items: center;
+ display: inherit;
+ background: dimgray;
+ left: 0px;
+ }
+
+ .audiobox-dictation:hover {
+ color: white;
+ cursor: pointer;
+ }
+ }
+
+ .audiobox-handle {
+ width: 20px;
+ height: 100%;
+ display: inline-block;
+ }
+
+ .audiobox-control,
+ .audiobox-control-interactive {
+ top: 0;
+ max-height: 32px;
+ width: 100%;
+ display: inline-block;
+ pointer-events: none;
+ }
+
+ .audiobox-control-interactive {
+ pointer-events: all;
+ }
+
+ .audiobox-record {
+ pointer-events: all;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ pointer-events: none;
+ }
+
+ .audiobox-record-interactive {
+ pointer-events: all;
+ width: 100%;
+ height: 100%;
+ position: relative;
+
+
+ }
+
+ .recording {
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ background-color: red;
+
+ .time {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ font-size: 20;
+ text-align: center;
+ top: 5;
+ }
+
+ .buttons {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 5px;
+ }
+
+ .buttons:hover {
+ background-color: crimson;
+ }
+ }
+
+ .audiobox-controls {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ padding-left: 2px;
+ background: black;
+
+ .audiobox-dictation {
+ position: absolute;
+ width: 30px;
+ height: 100%;
+ align-items: center;
+ display: inherit;
+ background: dimgray;
+ left: 0px;
+ }
+
+ .audiobox-player {
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+
+ .audiobox-playhead {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ margin-right: 2px;
+ height: 25px;
+ padding: 2px;
+ border-radius: 50%;
+ background-color: black;
+ color: white;
+ }
+
+ .audiobox-playhead:hover {
+ // background-color: black;
+ // border-radius: 5px;
+ background-color: grey;
+ color: lightgrey;
+ }
+
+ .audiobox-dictation {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 2px;
+ align-items: center;
+ display: inherit;
+ background: dimgray;
+ }
+
+ .audiobox-timeline {
+ position: absolute;
+ width: 100%;
+ border: gray solid 1px;
+ border-radius: 3px;
+ z-index: 1000;
+ overflow: hidden;
+
+ .audiobox-container {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 0px;
+ background: lightblue;
+ border-radius: 5px;
+ // box-shadow: black 2px 2px 1px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: darkblue;
+ border-width: 1px;
+ }
+
+ .audiobox-current {
+ width: 1px;
+ height: 100%;
+ background-color: red;
+ position: absolute;
+ top: 0px;
+ }
+
+ .waveform {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: -1000;
+ bottom: 0;
+ pointer-events: none;
+ div {
+ height: 100% !important;
+ width: 100% !important;
+ }
+ canvas {
+ height: 100% !important;
+ width: 100% !important;
+ }
+ }
+
+ .audiobox-linker,
+ .audiobox-linker-mini {
+ position: absolute;
+ width: 15px;
+ min-height: 10px;
+ height: 15px;
+ margin-left: -2.55px;
+ background: gray;
+ border-radius: 100%;
+ opacity: 0.9;
+ box-shadow: black 2px 2px 1px;
+
+ .linkAnchorBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left: unset !important;
+ top: unset !important;
+ }
+ }
+
+ .audiobox-linker-mini {
+ width: 8px;
+ min-height: 8px;
+ height: 8px;
+ box-shadow: black 1px 1px 1px;
+ margin-left: -1;
+ margin-top: -2;
+
+ .linkAnchorBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left: unset !important;
+ top: unset !important;
+ }
+ }
+
+ .audiobox-linker:hover,
+ .audiobox-linker-mini:hover {
+ opacity: 1;
+ }
+
+ .audiobox-marker-container,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ top: 2.5%;
+ background: gray;
+ border-radius: 50%;
+ box-shadow: black 2px 2px 1px;
+ overflow: visible;
+ cursor: pointer;
+
+ .audiobox-marker {
+ position: relative;
+ height: 100%;
+ // height: calc(100% - 15px);
+ width: 100%;
+ //margin-top: 15px;
+ }
+
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+ }
+
+ .audiobox-marker-timeline,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
+ height: 90%;
+ top: 2.5%;
+ border-radius: 5px;
+
+ .left-resizer {
+ background: dimgrey;
+ }
+ .resizer {
+ background: dimgrey;
+ }
+
+ .audiobox-marker {
+ position: relative;
+ height: calc(100% - 15px);
+ margin-top: 15px;
+ }
+
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+
+ .resizer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ pointer-events: all;
+ cursor: ew-resize;
+ height: 100%;
+ width: 10px;
+ z-index: 100;
+ }
+
+ .click {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ z-index: 100;
+ }
+
+ .left-resizer {
+ position: absolute;
+ left: 0;
+ top : 0;
+ pointer-events: all;
+ cursor: ew-resize;
+ height: 100%;
+ width: 10px;
+ z-index: 100;
+ }
+ }
+
+ .audiobox-marker-timeline:hover,
+ .audiobox-marker-minicontainer:hover {
+ opacity: 0.8;
+ }
+
+ .audiobox-marker-minicontainer {
+ width: 5px;
+ border-radius: 1px;
+
+ .audiobox-marker {
+ position: relative;
+ height: calc(100% - 8px);
+ margin-top: 8px;
+ }
+ }
+ }
+ }
+ }
+}
+
+@media only screen and (max-device-width: 480px) {
+ .audiobox-dictation {
+ font-size: 5em;
+ display: flex;
+ width: 100;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .audiobox-container .audiobox-record,
+ .audiobox-container-interactive .audiobox-record {
+ font-size: 3em;
+ }
+
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
+ .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
+ .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead {
+ width: 70px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
new file mode 100644
index 000000000..1775250fa
--- /dev/null
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -0,0 +1,324 @@
+import React = require("react");
+import { action, computed, IReactionDisposer, observable } from "mobx";
+import { observer } from "mobx-react";
+import { computedFn } from "mobx-utils";
+import { Doc, Opt } from "../../../fields/Doc";
+import { Id } from "../../../fields/FieldSymbols";
+import { List } from "../../../fields/List";
+import { listSpec, makeInterface } from "../../../fields/Schema";
+import { ComputedField, ScriptField } from "../../../fields/ScriptField";
+import { Cast, NumCast } from "../../../fields/Types";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { Scripting } from "../../util/Scripting";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionSubView } from "../collections/CollectionSubView";
+import { DocumentView } from "../nodes/DocumentView";
+import { LabelBox } from "../nodes/LabelBox";
+import "./CollectionStackedTimeline.scss";
+
+type PanZoomDocument = makeInterface<[]>;
+const PanZoomDocument = makeInterface();
+export type CollectionStackedTimelineProps = {
+ duration: number;
+ Play: () => void;
+ Pause: () => void;
+ playLink: (linkDoc: Doc) => void;
+ playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
+ playing: () => boolean;
+ setTime: (time: number) => void;
+ isChildActive: () => boolean;
+};
+
+@observer
+export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) {
+ static RangeScript: ScriptField;
+ static LabelScript: ScriptField;
+ static RangePlayScript: ScriptField;
+ static LabelPlayScript: ScriptField;
+
+ _disposers: { [name: string]: IReactionDisposer } = {};
+ _doubleTime: NodeJS.Timeout | undefined; // bcz: Hack! this must be called _doubleTime since setupMoveDragEvents will use that field name
+ _ele: HTMLAudioElement | null = null;
+ _start: number = 0;
+ _left: boolean = false;
+ _dragging = false;
+ _play: any = null;
+ _audioRef = React.createRef<HTMLDivElement>();
+ _timeline: Opt<HTMLDivElement>;
+ _markerStart: number = 0;
+ _currAnchor: Opt<Doc>;
+
+ @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
+ @observable _markerEnd: number = 0;
+ @observable _position: number = 0;
+ @computed get anchorDocs() { return this.childDocs; }
+ @computed get currentTime() { return NumCast(this.props.Document._currentTimecode); }
+
+ constructor(props: any) {
+ super(props);
+ // onClick play scripts
+ CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
+ CollectionStackedTimeline.LabelScript = CollectionStackedTimeline.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!;
+ CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
+ CollectionStackedTimeline.LabelPlayScript = CollectionStackedTimeline.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!;
+ }
+
+ // for creating key anchors with key events
+ @action
+ keyEvents = (e: KeyboardEvent) => {
+ if (e.target instanceof HTMLInputElement) return;
+ if (!this.props.playing()) return; // can't create if video is not playing
+ switch (e.key) {
+ case "x": // currently set to x, but can be a different key
+ const currTime = this.currentTime;
+ if (this._start) {
+ this._markerStart = currTime;
+ // this._start = false;
+ // this._visible = true;
+ } else {
+ this.createAnchor(this._markerStart, currTime);
+ // this._start = true;
+ // this._visible = false;
+ }
+ }
+ }
+
+ anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart)));
+ anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal)));
+
+ getLinkData(l: Doc) {
+ let la1 = l.anchor1 as Doc;
+ let la2 = l.anchor2 as Doc;
+ const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ }
+ return { la1, la2, linkTime };
+ }
+
+ // ref for timeline
+ timelineRef = (timeline: HTMLDivElement) => {
+ this._timeline = timeline;
+ }
+
+ // updates the anchor with the new time
+ @action
+ changeAnchor = (anchor: Opt<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 wasPlaying = this.props.playing();
+ if (wasPlaying) this.props.Pause();
+ else if (!this._doubleTime) {
+ this._doubleTime = setTimeout(() => {
+ this._doubleTime = undefined;
+ this.props.setTime((e.clientX - rect.x) / rect.width * this.props.duration);
+ }, 300);
+ }
+ this._markerStart = this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+ CollectionStackedTimeline.SelectingRegion = this;
+ setupMoveUpEvents(this, e,
+ action(e => {
+ this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+ return false;
+ }),
+ action((e, movement) => {
+ this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+ if (this._markerEnd < this._markerStart) {
+ const tmp = this._markerStart;
+ this._markerStart = this._markerEnd;
+ this._markerEnd = tmp;
+ }
+ CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd);
+ CollectionStackedTimeline.SelectingRegion = undefined;
+ }),
+ (e, doubleTap) => {
+ this.props.select(false);
+ e.shiftKey && this.createAnchor(this.currentTime);
+ !wasPlaying && doubleTap && this.props.Play();
+ }
+ , this.props.isSelected(true) || this.props.isChildActive());
+ }
+ }
+
+ @action
+ createAnchor(anchorStartTime?: number, anchorEndTime?: number) {
+ if (anchorStartTime === undefined) return this.props.Document;
+ const anchor = Docs.Create.LabelDocument({
+ title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any,
+ useLinkSmallAnchor: true,
+ hideLinkButton: true,
+ anchorStartTime,
+ anchorEndTime,
+ annotationOn: this.props.Document
+ });
+ if (Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), null) !== undefined) {
+ Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []).push(anchor);
+ } else {
+ this.dataDoc[this.props.fieldKey] = new List<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));
+ return { select: true };
+ }
+
+ // play back the audio from time
+ @action
+ clickAnchor = (anchorDoc: Doc) => {
+ if (this.props.Document.autoPlay) return this.playOnClick(anchorDoc);
+ this.props.setTime(this.anchorStart(anchorDoc));
+ return { select: true };
+ }
+
+ toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.props.duration, screen_delta / width * this.props.duration));
+ // starting the drag event for anchor resizing
+ onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => {
+ this._currAnchor = m;
+ this._left = left;
+ this._timeline?.setPointerCapture(e.pointerId);
+ setupMoveUpEvents(this, e,
+ (e) => {
+ const rect = (e.target as any).getBoundingClientRect();
+ this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width));
+ return false;
+ },
+ (e) => {
+ const rect = (e.target as any).getBoundingClientRect();
+ this.props.setTime(this.toTimeline(e.clientX - rect.x, rect.width));
+ this._timeline?.releasePointerCapture(e.pointerId);
+ },
+ emptyFunction);
+ }
+
+ rangeClickScript = () => CollectionStackedTimeline.RangeScript;
+ labelClickScript = () => CollectionStackedTimeline.LabelScript;
+ rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
+ labelPlayScript = () => CollectionStackedTimeline.LabelPlayScript;
+
+ // makes sure no anchors overlaps each other by setting the correct position and width
+ getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => {
+ const timelineContentWidth = this.props.PanelWidth();
+ const x1 = this.anchorStart(m);
+ const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.props.duration);
+ let max = 0;
+ const overlappedLevels = new Set(placed.map(p => {
+ const y1 = p.anchorStartTime;
+ const y2 = p.anchorEndTime;
+ if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) ||
+ (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) {
+ max = Math.max(max, p.level);
+ return p.level;
+ }
+ }));
+ let level = max + 1;
+ for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
+
+ placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
+ return level;
+ }
+
+ renderInner = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+ const anchor = observable({ view: undefined as any });
+ return {
+ anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ 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}
+ ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)}
+ parentActive={(out) => this.props.isSelected(out) || this.props.isChildActive()}
+ whenActiveChanged={this.props.whenActiveChanged}
+ onClick={script}
+ onDoubleClick={this.props.Document.autoPlay ? undefined : doublescript}
+ ignoreAutoHeight={false}
+ bringToFront={emptyFunction}
+ scriptContext={this} />
+ };
+ });
+ renderAnchor = computedFn(function (this: CollectionStackedTimeline, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
+ const inner = this.renderInner(mark, script, doublescript, x, y, width, height);
+ return <>
+ {inner.view}
+ {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) :
+ <>
+ <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)} />
+ </>}
+ </>;
+ });
+
+ render() {
+ const timelineContentWidth = this.props.PanelWidth();
+ const timelineContentHeight = this.props.PanelHeight();
+ const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = [];
+ const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor }));
+ const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
+ return <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);
+ }
+ }}>
+ {drawAnchors.map(d => {
+ const m = d.anchor;
+ const start = this.anchorStart(m);
+ const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.props.duration);
+ const left = start / this.props.duration * timelineContentWidth;
+ const top = d.level / maxLevel * timelineContentHeight;
+ const timespan = end - start;
+ return this.props.Document.hideAnchors ? (null) :
+ <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,
+ left,
+ top,
+ timelineContentWidth * timespan / this.props.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>;
+ }
+}
+Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 83c639871..03d8606d7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -59,7 +59,8 @@ export enum CollectionViewType {
//Staff = "staff",
Map = "map",
Grid = "grid",
- Pile = "pileup"
+ Pile = "pileup",
+ StackedTimeline = "stacked timeline"
}
export interface CollectionViewProps extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -158,14 +159,14 @@ export class CollectionView extends Touchable<CollectionViewProps> {
const pushpin = Docs.Create.FontIconDocument({
title: "pushpin", label: "",
icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
- _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
+ _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null)
});
pushpin.isPushpin = true;
Doc.GetProto(pushpin).annotationOn = doc.annotationOn;
Doc.SetInPlace(doc, "annotationOn", undefined, true);
Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", "");
- doc.displayTimecode = undefined;
+ doc._timecodeToShow = undefined;
}
doc._stayInCollection = undefined;
doc.context = this.props.Document;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5ca069fb9..c66734556 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -182,12 +182,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const presArray: Doc[] = PresBox.Instance?.sortArray();
const size: number = PresBox.Instance?._selectedArray.size;
const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
+ const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
Doc.AddDocToList(curPres, "data", pinDoc, presSelected);
- if (!pinProps?.audioRange && (pinDoc.type === DocumentType.AUDIO || pinDoc.type === DocumentType.VID)) {
+ if (!pinProps?.audioRange && duration !== undefined) {
pinDoc.mediaStart = "manual";
pinDoc.mediaStop = "manual";
pinDoc.presStartTime = 0;
- pinDoc.presEndTime = pinDoc.type === DocumentType.AUDIO ? doc.duration : NumCast(doc["data-duration"]);
+ pinDoc.presEndTime = duration;
}
//save position
if (pinProps?.setPosition || pinDoc.isInkMask) {
@@ -358,7 +359,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
active = () => this._isActive;
ScreenToLocalTransform = () => {
- const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0]?.firstChild as HTMLElement);
+ const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY);
}
PanelWidth = () => this._panelWidth;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2bc716928..6619205af 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,7 +54,7 @@ export const panZoomSchema = createSchema({
_panX: "number",
_panY: "number",
_currentTimecode: "number",
- displayTimecode: "number",
+ _timecodeToShow: "number",
_currentFrame: "number",
arrangeInit: ScriptField,
_useClusters: "boolean",
@@ -137,11 +137,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
@computed get cachedCenteringShiftX(): number {
const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
- return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections
+ return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / this.parentScaling / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
- return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections
+ return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / this.parentScaling / scaling;// shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
@@ -196,7 +196,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true));
}
- public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+ public isCurrent(doc: Doc) {
+ const dispTime = NumCast(doc._timecodeToShow, -1);
+ const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5);
+ const curTime = NumCast(this.Document._currentTimecode, -1);
+ return dispTime === -1 || ((curTime - dispTime) >= -1e-4 && curTime <= endTime);
+ }
public getActiveDocuments = () => {
return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 9ef37ecc2..0edbfe7a5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -426,7 +426,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
delete d.x;
delete d.y;
delete d.activeFrame;
- delete d.displayTimecode; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
d.x = dx - this.Bounds.left - this.Bounds.width / 2;
d.y = dy - this.Bounds.top - this.Bounds.height / 2;
return d;
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 93cdbbb09..4a3bbf8d8 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -159,9 +159,8 @@
}
.audiobox-timeline {
- position: relative;
+ position: absolute;
width: 100%;
- background: white;
border: gray solid 1px;
border-radius: 3px;
z-index: 1000;
@@ -342,7 +341,7 @@
.audiobox-marker {
position: relative;
- height: calc(100% - 8px);
+ height: 100%;
margin-top: 8px;
}
}
@@ -351,14 +350,14 @@
.current-time {
position: absolute;
font-size: 8;
- top: calc(100% - 8px);
+ top: 100%;
left: 30px;
color: white;
}
.total-time {
position: absolute;
- top: calc(100% - 8px);
+ top: 100%;
font-size: 8;
right: 2px;
color: white;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index c3c876b75..c8bec74fb 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -3,33 +3,28 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { computedFn } from "mobx-utils";
import Waveform from "react-audio-waveform";
import { DateField } from "../../../fields/DateField";
import { Doc, DocListCast, Opt } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { List } from "../../../fields/List";
import { createSchema, listSpec, makeInterface } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
+import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { emptyFunction, formatTime, numberRange, Utils } from "../../../Utils";
+import { DocUtils } from "../../documents/Documents";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Scripting } from "../../util/Scripting";
-import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
+import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import "./AudioBox.scss";
-import { DocumentView } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
import { LinkDocPreview } from "./LinkDocPreview";
-
declare class MediaRecorder {
// whatever MediaRecorder has
constructor(e: any);
@@ -47,13 +42,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
static playheadWidth = 30; // width of playhead
static heightPercent = 80; // height of timeline in percent of height of audioBox.
static Instance: AudioBox;
- static RangeScript: ScriptField;
- static LabelScript: ScriptField;
- static RangePlayScript: ScriptField;
- static LabelPlayScript: ScriptField;
_disposers: { [name: string]: IReactionDisposer } = {};
_ele: HTMLAudioElement | null = null;
+ _audioRef = React.createRef<HTMLDivElement>();
+ _stackedTimeline = React.createRef<CollectionStackedTimeline>();
_recorder: any;
_recordStart = 0;
_pauseStart = 0;
@@ -61,16 +54,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
_pausedTime = 0;
_stream: MediaStream | undefined;
_start: number = 0;
- _hold: boolean = false;
- _left: boolean = false;
- _dragging = false;
_play: any = null;
- _audioRef = React.createRef<HTMLDivElement>();
- _timeline: Opt<HTMLDivElement>;
- _markerStart: number = 0;
- _currMarker: any;
- @observable static SelectingRegion: AudioBox | undefined = undefined;
@observable static _scrubTime = 0;
@observable _markerEnd: number = 0;
@observable _position: number = 0;
@@ -80,26 +65,25 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
set audioState(value) { this.dataDoc.audioState = value; }
public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; });
@computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
- @computed get audioDuration() { return NumCast(this.dataDoc.duration); }
- @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey]); }
+ @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); }
+ @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); }
@computed get links() { return DocListCast(this.dataDoc.links); }
@computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time
+ @computed get heightPercent() { return AudioBox.heightPercent; }
constructor(props: Readonly<FieldViewProps>) {
super(props);
AudioBox.Instance = this;
- // onClick play scripts
- AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!;
- AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!;
+ if (this.duration === undefined) {
+ runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration);
+ }
}
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime = NumCast(la2.audioStart, NumCast(la1.audioStart));
+ const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime));
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
@@ -107,16 +91,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
return { la1, la2, linkTime };
}
+ 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;
+ }
+
componentWillUnmount() {
Object.values(this._disposers).forEach(disposer => disposer?.());
const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
}
- getAnchor = () => {
- return this.createMarker(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined));
- }
-
@action
componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
@@ -125,13 +109,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime));
- this._disposers.audioStart = reaction(
- () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._audioStart, "number", null) : undefined,
- audioStart => audioStart !== undefined && setTimeout(() => {
- this._audioRef.current && this.playFrom(audioStart);
+ this._disposers.triggerAudio = reaction(
+ () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined,
+ start => start !== undefined && setTimeout(() => {
+ this._audioRef.current && this.playFrom(start);
setTimeout(() => {
- this.Document._currentTimecode = audioStart;
- this.Document._audioStart = undefined;
+ this.Document._currentTimecode = start;
+ this.Document._triggerAudio = undefined;
}, 10);
}, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
@@ -140,32 +124,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
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._audioRef.current && this.Pause();
setTimeout(() => this.Document._audioStop = undefined, 10);
}, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
);
}
- playLink = (doc: Doc) => {
- this.links.filter(l => l.anchor1 === doc || l.anchor2 === doc).forEach(l => {
- const { la1, la2 } = this.getLinkData(l);
- const startTime = NumCast(la1.audioStart, NumCast(la2.audioStart, null));
- const endTime = NumCast(la1.audioEnd, NumCast(la2.audioEnd, null));
- if (startTime !== undefined) {
- this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime));
- }
- });
- doc.annotationOn === this.rootDoc && this.playFrom(NumCast(doc.audioStart), Cast(doc.audioEnd, "number", null));
- }
-
// for updating the timecode
timecodeChanged = () => {
const htmlEle = this._ele;
if (this.audioState !== "recording" && htmlEle) {
- htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
- this.links.map(l => {
- const { la1, linkTime } = this.getLinkData(l);
+ htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration);
+ this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => {
if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) {
Doc.linkFollowHighlight(la1);
}
@@ -175,7 +146,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
// pause play back
- pause = action(() => {
+ Pause = action(() => {
this._ele!.pause();
this.audioState = "paused";
});
@@ -187,23 +158,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// play back the audio from time
@action
- playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
- DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false);
- this.playFrom(seekTimeInSeconds, endTime);
- }
-
- // play back the audio from time
- @action
- clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
- if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime);
- else {
- DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false);
- this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds);
- }
- }
- // play back the audio from time
- @action
- playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
+ playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
clearTimeout(this._play);
if (Number.isNaN(this._ele?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
@@ -212,17 +167,17 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
if (seekTimeInSeconds > -1) {
setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
} else {
- this.pause();
+ this.Pause();
}
} else if (seekTimeInSeconds <= this._ele.duration) {
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
runInAction(() => this.audioState = "playing");
- if (endTime !== this.audioDuration) {
- this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
+ if (endTime !== this.duration) {
+ this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
}
} else {
- this.pause();
+ this.Pause();
}
}
}
@@ -261,9 +216,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// context menu
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
+ funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" });
funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" });
+ funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" });
ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
@@ -271,7 +226,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
stopRecording = action(() => {
this._recorder.stop();
this._recorder = undefined;
- this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
@@ -287,9 +242,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
// for play button
- onPlay = (e: any) => {
+ Play = (e?: any) => {
this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
- e.stopPropagation();
+ e?.stopPropagation?.();
}
// creates a text document for dictation
@@ -306,15 +261,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// ref for updating time
setRef = (e: HTMLAudioElement | null) => {
e?.addEventListener("timeupdate", this.timecodeChanged);
- e?.addEventListener("ended", this.pause);
+ e?.addEventListener("ended", this.Pause);
this._ele = e;
}
- // ref for timeline
- timelineRef = (timeline: HTMLDivElement) => {
- this._timeline = timeline;
- }
-
// returns the path of the audio file
@computed get path() {
const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
@@ -324,8 +274,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// returns the html audio element
@computed get audio() {
- const interactive = this.active() ? "-interactive" : "";
- return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
+ return <audio ref={this.setRef} className={`audiobox-control${this.active() ? "-interactive" : ""}`}>
<source src={this.path} type="audio/mpeg" />
Not supported.
</audio>;
@@ -349,118 +298,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
e.stopPropagation();
}
- // starting the drag event for marker 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.active()) {
- const wasPaused = this.audioState === "paused";
- this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration;
- wasPaused && this.pause();
-
- const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration;
- this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x);
- AudioBox.SelectingRegion = this;
- setupMoveUpEvents(this, e,
- action(e => {
- this._markerEnd = toTimeline(e.clientX - rect.x);
- if (this._markerEnd < this._markerStart) {
- const tmp = this._markerStart;
- this._markerStart = this._markerEnd;
- this._markerEnd = tmp;
- }
- return false;
- }),
- action((e, movement) => {
- AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x));
- AudioBox.SelectingRegion = undefined;
- }),
- e => {
- this.props.select(false);
- e.shiftKey && this.createMarker(this._ele!.currentTime);
- }
- , this.props.isSelected(true) || this._isChildActive);
- }
- }
-
- @action
- createMarker(audioStart?: number, audioEnd?: number) {
- if (audioStart === undefined) return this.rootDoc;
- const marker = Docs.Create.LabelDocument({
- title: ComputedField.MakeFunction(`"#" + formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any,
- useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false,
- isLabel: audioEnd === undefined,
- _autoHeight: true, annotationOn: this.props.Document
- });
- if (this.dataDoc[this.annotationKey]) {
- this.dataDoc[this.annotationKey].push(marker);
- } else {
- this.dataDoc[this.annotationKey] = new List<Doc>([marker]);
- }
- return marker;
- }
-
- // starting the drag event for marker resizing
- onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => {
- this._currMarker = m;
- this._left = left;
- this._timeline?.setPointerCapture(e.pointerId);
- const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.audioDuration;
- setupMoveUpEvents(this, e,
- (e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width));
- return false;
- },
- (e) => {
- const rect = (e.target as any).getBoundingClientRect();
- this._ele!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width);
- this._timeline?.releasePointerCapture(e.pointerId);
- },
- emptyFunction);
- }
-
- // updates the marker with the new time
- @action
- changeMarker = (m: any, time: any) => {
- this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker =>
- this._left ? marker.audioStart = time : marker.audioEnd = time);
- }
-
- // checks if the two markers are the same with start and end time
- isSame = (m1: any, m2: any) => {
- return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd;
- }
-
- // makes sure no markers overlaps each other by setting the correct position and width
- getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => {
- const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth;
- const x1 = m.audioStart;
- const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.audioDuration : m.audioEnd;
- let max = 0;
- const overlappedLevels = new Set(placed.map(p => {
- const y1 = p.audioStart;
- const y2 = p.audioEnd;
- if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) ||
- (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) {
- max = Math.max(max, p.level);
- return p.level;
- }
- }));
- let level = max + 1;
- for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
-
- placed.push({ audioStart: x1, audioEnd: x2, level });
- return level;
- }
-
- @computed get selectionContainer() {
- return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{
- left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`,
- width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%"
- }} />;
- }
-
// returns the audio waveform
@computed get waveform() {
const audioBuckets = Cast(this.dataDoc.audioBuckets, listSpec("number"), []);
@@ -469,8 +306,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
color={"darkblue"}
height={this._waveHeight}
barWidth={0.1}
- pos={this.audioDuration}
- duration={this.audioDuration}
+ pos={this.duration}
+ duration={this.duration}
peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined}
progressColor={"blue"} />;
}
@@ -490,51 +327,64 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
);
}
- rangeClickScript = () => AudioBox.RangeScript;
- labelClickScript = () => AudioBox.LabelScript;
- rangePlayScript = () => AudioBox.RangePlayScript;
- labelPlayScript = () => AudioBox.LabelPlayScript;
- renderInner = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
- const marker = observable({ view: undefined as any });
- return {
- marker, view: <DocumentView key="view" {...this.props} ref={action((r: DocumentView | null) => marker.view = r)}
- Document={mark}
- PanelWidth={() => width}
- PanelHeight={() => height}
- focus={() => this.playLink(mark)}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)}
- parentActive={(out) => this.props.isSelected(out) || this._isChildActive}
- whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
- onClick={script}
- onDoubleClick={this.layoutDoc.playOnClick ? undefined : doublescript}
- ignoreAutoHeight={false}
- bringToFront={emptyFunction}
- scriptContext={this} />
- };
- });
- renderMarker = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) {
- const inner = this.renderInner(mark, script, doublescript, x, y, width, height);
- return <>
- {inner.view}
- {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) :
- <>
- <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)} />
- </>}
- </>;
- });
+ playing = () => this.audioState === "playing";
+ playLink = (link: Doc) => {
+ if (link.annotationOn === this.rootDoc) {
+ if (this.layoutDoc.playOnSelect) this.playFrom(this._stackedTimeline.current?.anchorStart(link) || 0, this._stackedTimeline.current?.anchorEnd(link));
+ else this._ele!.currentTime = this.layoutDoc._currentTimecode = (this._stackedTimeline.current?.anchorStart(link) || 0);
+ }
+ else {
+ this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
+ const { la1, la2 } = this.getLinkData(l);
+ const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null));
+ const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null));
+ if (startTime !== undefined) {
+ if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+ else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
+ }
+ });
+ }
+ }
+
+ @computed get renderTimeline() {
+ return <CollectionStackedTimeline ref={this._stackedTimeline}
+ Document={this.props.Document}
+ fieldKey={this.annotationKey}
+ renderDepth={this.props.renderDepth + 1}
+ parentActive={this.props.parentActive}
+ 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}
+ playing={this.playing}
+ select={this.props.select}
+ isSelected={this.props.isSelected}
+ whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(0, -(100 - this.heightPercent) / 200 * this.props.PanelHeight())}
+ isChildActive={() => this._isChildActive}
+ 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)
+ />;
+ }
render() {
const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : "";
- const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth;
- const timelineContentHeight = (this.props.PanelHeight() * AudioBox.heightPercent / 100) * AudioBox.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
- const overlaps: { audioStart: number, audioEnd: number, level: number }[] = [];
- const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m }));
- const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 1;
return <div className="audiobox-container"
onContextMenu={this.specificContextMenu}
onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
@@ -562,43 +412,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
<div className="audiobox-controls" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }} >
<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.onPlay}> <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: `${AudioBox.heightPercent}%` }} ref={this.timelineRef}
- onClick={e => { e.stopPropagation(); e.preventDefault(); }}
- onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}>
+ <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="waveform">
{this.waveform}
</div>
- {drawMarkers.map((d, i) => {
- const m = d.marker;
- const left = NumCast(m.audioStart) / this.audioDuration * timelineContentWidth;
- const top = d.level / maxLevel * timelineContentHeight;
- const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.audioDuration : NumCast(m.audioEnd) - NumCast(m.audioStart);
- return this.layoutDoc.hideMarkers ? (null) :
- <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={i}
- style={{ left, top, width: `${timespan / this.audioDuration * 100}%`, height: `${1 / maxLevel * 100}%` }}
- onClick={e => { this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} >
- {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript,
- left + AudioBox.playheadWidth,
- (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top,
- timelineContentWidth * timespan / this.audioDuration,
- timelineContentHeight / maxLevel)}
- </div>;
- })}
- {this.selectionContainer}
- <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} />
- {this.audio}
</div>
+ {this.renderTimeline}
+ {this.audio}
<div className="current-time">
{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
</div>
<div className="total-time">
- {formatTime(Math.round(this.audioDuration))}
+ {formatTime(Math.round(this.duration))}
</div>
</div>
</div>
}
</div>;
}
-}
-Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c5c47b463..6dc7a822c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -385,9 +385,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
scriptContext: this.props.scriptContext,
thisContainer: this.props.ContainingCollectionDoc,
documentView: this.props.DocumentView,
+ clientX: e.clientX,
+ clientY: e.clientY,
shiftKey: e.shiftKey
}, console.log);
- undoBatch(func)();
+ UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click");
} else if (!Doc.IsSystem(this.props.Document)) {
if (this.props.Document.type !== DocumentType.LABEL) {
UndoManager.RunInBatch(() => {
@@ -401,19 +403,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
} else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
const shiftKey = e.shiftKey;
+ const clientX = e.clientX;
+ const clientY = e.clientY;
const func = () => this.onClickHandler.script.run({
this: this.layoutDoc,
self: this.rootDoc,
scriptContext: this.props.scriptContext,
thisContainer: this.props.ContainingCollectionDoc,
documentView: this.props.DocumentView,
+ clientX: clientX,
+ clientY: clientY,
shiftKey
}, console.log);
const clickFunc = () => {
if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) &&
!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) &&
!this.onClickHandler.script.originalScript.includes("selectMainMenu")) {
- UndoManager.RunInBatch(func, "on click");
+ UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on click");
} else func();
};
if (this.onDoubleClickHandler) {
@@ -544,7 +550,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch @action
drop = async (e: Event, de: DragManager.DropEvent) => {
- if (this.props.LayoutTemplateString) return;
+ if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
if (this.props.Document === CurrentUserUtils.ActiveDashboard) {
alert((e.target as any)?.closest?.("*.lm_content") ?
"You can't perform this move most likely because you don't have permission to modify the destination." :
@@ -954,7 +960,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
PanelWidth = () => this.panelWidth;
PanelHeight = () => this.panelHeight;
ContentScale = () => this.nativeScaling;
- screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
+ screenToLocalTransform = () => {
+ return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
+ }
componentDidMount() {
!BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this);
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 2db610e43..63455e3d1 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -103,7 +103,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
* Responds to clicking the check box in the flyout menu
*/
facetClick = (facetHeader: string) => {
- const targetDoc = CollectionDockingView.Instance.props.Document;
+ const targetDoc = SelectionManager.Views()[0].Document; // CollectionDockingView.Instance.props.Document;
const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader);
if (found !== -1) {
(this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1);
@@ -208,15 +208,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
render() {
const facetCollection = this.props.Document;
- const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}>
- {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}>
- <input className="filterBox-flyout-facet-check" type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} />
- <span className="checkmark" />
- {facet}
- </label>)}
- </div>;
+ // const flyout = <div className="filterBox-flyout" style={{ width: `100%` }} onWheel={e => e.stopPropagation()}>
+ // {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}>
+ // <input className="filterBox-flyout-facet-check" type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} />
+ // <span className="checkmark" />
+ // {facet}
+ // </label>)}
+ // </div>;
- const attributes = this.activeAttributes;
+ // const attributes = this.activeAttributes;
// const options = this._allFacets.filter(facet => !attributes.some(attribute => attribute.title === facet)).map(facet => ({ value: facet, label: facet }));
const options = this._allFacets.map(facet => ({ value: facet, label: facet }));
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 3448a4abd..87d5b07a2 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -67,7 +67,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
- const label = StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title));
+ const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
return (
<div className="labelBox-outerDiv"
onClick={action(() => this.clicked = !this.clicked)}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index aef776563..62e497e18 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -232,14 +232,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime);
if (targetDoc.type === DocumentType.AUDIO) {
if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- targetDoc._audioStart = NumCast(activeItem.presStartTime);
+ targetDoc._triggerAudio = NumCast(activeItem.presStartTime);
this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc];
} else if (targetDoc.type === DocumentType.VID) {
if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- targetDoc._videoStop = true;
+ targetDoc._triggerVideoStop = true;
setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10);
- setTimeout(() => targetDoc._videoStart = true, 20);
- this._mediaTimer = [setTimeout(() => targetDoc._videoStop = true, (duration * 1000) + 20), targetDoc];
+ setTimeout(() => targetDoc._triggerVideo = true, 20);
+ this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc];
}
}
@@ -249,7 +249,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
targetDoc._audioStop = true;
} else if (targetDoc.type === DocumentType.VID) {
if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- targetDoc._videoStop = true;
+ targetDoc._triggerVideoStop = true;
}
}
@@ -714,9 +714,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
if (audio) {
audio.mediaStart = "manual";
audio.mediaStop = "manual";
- audio.presStartTime = NumCast(doc.audioStart);
- audio.presEndTime = NumCast(doc.audioEnd);
- audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart);
+ audio.presStartTime = NumCast(doc.anchorStartTime);
+ audio.presEndTime = NumCast(doc.anchorEndTime);
+ audio.presDuration = NumCast(doc.anchorEndTime) - NumCast(doc.anchorStartTime);
TabDocView.PinDoc(audio, { audioRange: true });
setTimeout(() => this.removeDocument(doc), 0);
return false;
@@ -1477,6 +1477,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
@computed get mediaOptionsDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
+ const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10);
const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + ". " + this.childDocs[mediaStopDocInd - 1].title : "";
if (activeItem && targetDoc) {
@@ -1521,7 +1522,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>
<div className="multiThumb-slider">
- <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presEndTime)}
+ <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presEndTime)}
style={{ gridColumn: 1, gridRow: 1 }}
className={`toolbar-slider ${"end"}`}
id="toolbar-slider"
@@ -1545,7 +1546,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
e.stopPropagation();
activeItem.presEndTime = Number(e.target.value);
}} />
- <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presStartTime)}
+ <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presStartTime)}
style={{ gridColumn: 1, gridRow: 1 }}
className={`toolbar-slider ${"start"}`}
id="toolbar-slider"
@@ -1573,7 +1574,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
<div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}>
<div className="slider-text">0 s</div>
<div className="slider-text"></div>
- <div className="slider-text">{activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} s</div>
+ <div className="slider-text">{duration / 10} s</div>
</div>
</div>
<div className="ribbon-final-box">
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 07e8e0951..19f605278 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -10,6 +10,139 @@
.inkingCanvas-paths-markers {
opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
}
+
+ .audiobox-timeline {
+ position: absolute;
+ width: 100%;
+ background: beige;
+ border: gray solid 1px;
+ border-radius: 3px;
+ z-index: 1000;
+ overflow: hidden;
+ bottom: 0;
+
+ .audiobox-current {
+ width: 1px;
+ height: 100%;
+ background-color: red;
+ position: absolute;
+ top: 0px;
+ }
+
+ .audiobox-container {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 0px;
+ background: lightblue;
+ border-radius: 5px;
+ // box-shadow: black 2px 2px 1px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: darkblue;
+ border-width: 1px;
+ }
+
+ .audiobox-marker-timeline,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ top: 2.5%;
+ border-radius: 50%;
+ box-shadow: black 2px 2px 1px;
+ overflow: visible;
+ cursor: pointer;
+
+ .left-resizer {
+ background: dimgrey;
+ }
+ .resizer {
+ background: dimgrey;
+ }
+ .audiobox-marker {
+ position: relative;
+ height: 100%;
+ // height: calc(100% - 15px);
+ width: 100%;
+ //margin-top: 15px;
+ }
+
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+ }
+
+ .audiobox-marker-timeline,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
+ height: 90%;
+ top: 2.5%;
+ border-radius: 5px;
+ box-shadow: black 2px 2px 1px;
+
+ .audiobox-marker {
+ position: relative;
+ height: calc(100% - 15px);
+ margin-top: 15px;
+ }
+
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+
+ .resizer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ pointer-events: all;
+ cursor: ew-resize;
+ height: 100%;
+ width: 10px;
+ z-index: 100;
+ }
+
+ .click {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ z-index: 100;
+ }
+
+ .left-resizer {
+ position: absolute;
+ left: 0;
+ top: 0;
+ cursor: ew-resize;
+ height: 100%;
+ width: 10px;
+ z-index: 100;
+ }
+
+ // .contentFittingDocumentView-previewDoc {
+ // width: 100% !important;
+ // transform: none !important;
+ // }
+ }
+
+ .audiobox-marker-container1:hover,
+ .audiobox-marker-minicontainer:hover {
+ opacity: 0.8;
+ }
+
+ .audiobox-marker-minicontainer {
+ width: 5px;
+ border-radius: 1px;
+
+ .audiobox-marker {
+ position: relative;
+ height: calc(100% - 8px);
+ margin-top: 8px;
+ }
+ }
+ }
}
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
@@ -49,6 +182,19 @@
transform-origin: left top;
pointer-events:all;
}
+
+.timeline-button {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ z-index: 1010;
+ bottom: 35px;
+ right: 235px;
+ color: white;
+ background: dimgrey;
+ width: 20px;
+ height: 20px;
+}
.videoBox-play {
width: 25px;
height: 20px;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 6354c677e..bfac7dc1c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,31 +3,30 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
-import { Doc } from "../../../fields/Doc";
+import { 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 { Cast, StrCast, NumCast } from "../../../fields/Types";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { VideoField } from "../../../fields/URLField";
-import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
+import { Networking } from "../../Network";
+import { SelectionManager } from "../../util/SelectionManager";
+import { SnappingManager } from "../../util/SnappingManager";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
+import { MarqueeAnnotator } from "../MarqueeAnnotator";
+import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
-import "./VideoBox.scss";
-import { documentSchema } from "../../../fields/documentSchemas";
-import { Networking } from "../../Network";
-import { SnappingManager } from "../../util/SnappingManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { LinkDocPreview } from "./LinkDocPreview";
import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
-import { Transform } from "../../util/Transform";
-import { StyleProp } from "../StyleProvider";
-import { Dictionary } from "typescript-collections";
-import { MarqueeAnnotator } from "../MarqueeAnnotator";
-import { AnchorMenu } from "../pdf/AnchorMenu";
+import { LinkDocPreview } from "./LinkDocPreview";
+import "./VideoBox.scss";
const path = require('path');
export const timeSchema = createSchema({
@@ -38,26 +37,50 @@ const VideoDocument = makeInterface(documentSchema, timeSchema);
@observer
export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
static _youtubeIframeCounter: number = 0;
+ static Instance: VideoBox;
+ static heightPercent = 60; // height of timeline in percent of height of videoBox.
private _disposers: { [name: string]: IReactionDisposer } = {};
private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
- private _isResetClick = 0;
+ private _stackedTimeline = React.createRef<CollectionStackedTimeline>();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+ private _playRegionTimer: any = null;
+ private _playRegionDuration = 0;
+ @observable static _showControls: boolean;
@observable _marqueeing: number[] | undefined;
+ @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
+ @observable _screenCapture = false;
+ @observable _visible: boolean = false;
@observable _forceCreateYouTubeIFrame = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable _playing = false;
- @observable static _showControls: boolean;
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+ @computed get links() { return DocListCast(this.dataDoc.links); }
+ @computed get heightPercent() { return this.layoutDoc._timelineShow ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 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])); }
- public get player(): HTMLVideoElement | null {
- return this._videoRef;
+ public get player(): HTMLVideoElement | null { return this._videoRef; }
+
+ constructor(props: Readonly<FieldViewProps>) {
+ super(props);
+ 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;
+ }
+
+ choosePath(url: string) {
+ return url.indexOf(window.location.origin) === -1 ? Utils.CorsProxy(url) : url;
}
videoLoad = () => {
@@ -68,7 +91,13 @@ 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();
@@ -114,13 +143,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
}
- choosePath(url: string) {
- if (url.indexOf(window.location.origin) === -1) {
- return Utils.CorsProxy(url);
- }
- return url;
- }
-
@action public Snapshot() {
const width = (this.layoutDoc._width || 0);
const height = (this.layoutDoc._height || 0);
@@ -190,6 +212,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
componentDidMount() {
+ this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+
this._disposers.selection = reaction(() => this.props.isSelected(),
selected => {
if (!selected) {
@@ -198,30 +222,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
},
{ fireImmediately: true });
- this._disposers.videoStart = reaction(
- () => this.Document._videoStart,
- (videoStart) => {
- if (videoStart !== undefined) {
- if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
- const delay = this.player ? 0 : 250; // wait for mainCont and try again to play
- setTimeout(() => this.player && this.Play(), delay);
- setTimeout(() => { this.Document._videoStart = undefined; }, 10 + delay);
- }
- }
- },
+ this._disposers.triggerVideo = reaction(
+ () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined,
+ time => time !== undefined && setTimeout(() => {
+ this.player && this.Play();
+ setTimeout(() => this.Document._triggerVideo = undefined, 10);
+ }, this.player ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
);
- this._disposers.videoStop = reaction(
- () => this.Document._videoStop,
- (videoStop) => {
- if (videoStop !== undefined) {
- if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
- const delay = this.player ? 0 : 250; // wait for mainCont and try again to play
- setTimeout(() => this.player && this.Pause(), delay);
- setTimeout(() => { this.Document._videoStop = undefined; }, 10 + delay);
- }
- }
- },
+ this._disposers.triggerStop = reaction(
+ () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? NumCast(this.Document._triggerVideoStop, null) : undefined,
+ stop => stop !== undefined && setTimeout(() => {
+ this.player && this.Pause();
+ setTimeout(() => this.Document._triggerVideoStop = undefined, 10);
+ }, this.player ? 0 : 250), // wait for mainCont and try again to play
{ fireImmediately: true }
);
if (this.youtubeVideoId) {
@@ -238,7 +252,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
componentWillUnmount() {
this.Pause();
- Object.values(this._disposers).forEach(disposer => disposer?.());
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true);
}
@action
@@ -270,7 +285,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
console.log("VideoBox :" + e);
}
}
- @observable _screenCapture = false;
+
specificContextMenu = (e: React.MouseEvent): void => {
const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
if (field) {
@@ -285,6 +300,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
}), icon: "expand-arrows-alt"
});
+ subitems.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
+ subitems.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" });
ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
}
}
@@ -294,17 +311,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
- <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
- style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
- onCanPlay={this.videoLoad}
- controls={VideoBox._showControls}
- onPlay={() => this.Play()}
- onSeeked={this.updateTimecode}
- onPause={() => this.Pause()}
- onClick={e => e.preventDefault()}>
- <source src={field.url.href} type="video/mp4" />
- Not supported.
- </video>;
+ <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}>
+ <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+ <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
+ onCanPlay={this.videoLoad}
+ controls={VideoBox._showControls}
+ onPlay={() => this.Play()}
+ onSeeked={this.updateTimecode}
+ onPause={() => this.Pause()}
+ onClick={e => e.preventDefault()}>
+ <source src={field.url.href} type="video/mp4" />
+ Not supported.
+ </video>
+ </div>
+ </div>;
}
@computed get youtubeVideoId() {
@@ -356,18 +377,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
private get uIButtons() {
const curTime = (this.layoutDoc._currentTimecode || 0);
return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} >
- <span>{"" + Math.round(curTime)}</span>
+ <span>{"" + formatTime(curTime)}</span>
<span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
</div>,
<div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} >
<FontAwesomeIcon icon="camera" size="lg" />
</div>,
+ <div className="timeline-button" key="timeline-button" onPointerDown={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>,
VideoBox._showControls ? (null) : [
+ // <div className="control-background">
<div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} >
<FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
</div>,
<div className="videoBox-full" key="full" onPointerDown={this.onFullDown} >
F
+ {/* </div> */}
</div>
]]);
}
@@ -387,24 +418,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
onResetDown = (e: React.PointerEvent) => {
- this.Pause();
- e.stopPropagation();
- this._isResetClick = 0;
- document.addEventListener("pointermove", this.onResetMove, true);
- document.addEventListener("pointerup", this.onResetUp, true);
- }
-
- onResetMove = (e: PointerEvent) => {
- this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
- this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
- e.stopImmediatePropagation();
- }
-
- @action
- onResetUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onResetMove, true);
- document.removeEventListener("pointerup", this.onResetUp, true);
- this._isResetClick < 10 && (this.layoutDoc._currentTimecode = 0);
+ 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);
}
@computed get youtubeContent() {
@@ -419,59 +438,147 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
@action.bound
- addDocumentWithTimestamp(doc: Doc | Doc[]): boolean {
+ addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
const curTime = NumCast(this.layoutDoc._currentTimecode);
- docs.forEach(doc => doc.displayTimecode = curTime);
+ docs.forEach(doc => doc._timecodeToShow = curTime);
return this.addDocument(doc);
}
- screenToLocalTransform = () => this.props.ScreenToLocalTransform();
+ // play back the video from time
+ @action
+ playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
+ clearTimeout(this._playRegionTimer);
+ this._playRegionDuration = endTime - seekTimeInSeconds;
+ if (Number.isNaN(this.player?.duration)) {
+ setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+ } else if (this.player) {
+ if (seekTimeInSeconds < 0) {
+ if (seekTimeInSeconds > -1) {
+ setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+ } else {
+ this.Pause();
+ }
+ } else if (seekTimeInSeconds <= this.player.duration) {
+ this.player.currentTime = seekTimeInSeconds;
+ this.player.play();
+ runInAction(() => this._playing = true);
+ if (endTime !== this.duration) {
+ this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
+ }
+ } else {
+ this.Pause();
+ }
+ }
+ }
+
+ playLink = (doc: Doc) => {
+ const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow));
+ const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null));
+ if (startTime !== undefined) {
+ if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+ else this.Seek(startTime);
+ }
+ }
+
+ // returns the timeline
+ @computed get renderTimeline() {
+ return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}>
+ <CollectionStackedTimeline ref={this._stackedTimeline}
+ Document={this.props.Document}
+ fieldKey={this.annotationKey}
+ renderDepth={this.props.renderDepth + 1}
+ parentActive={this.props.parentActive}
+ 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))}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())}
+ isChildActive={() => this._isChildActive}
+ Play={this.Play}
+ Pause={this.Pause}
+ active={this.active}
+ playLink={this.playLink}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100}
+ />
+ </div>;
+ }
+
contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
@computed get annotationLayer() {
- return <div className="imageBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer} />;
+ return <div className="imageBox-annotationLayer" style={{ height: `${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];
- })
+ });
finishMarquee = action(() => {
this._marqueeing = undefined;
this.props.select(true);
- })
+ });
+
+ 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;
+ screenToLocalTransform = () => {
+ const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
+ return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
+ }
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
- const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad;
+ const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
style={{
pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
borderRadius
}} >
- <div className="videoBox-viewer" onPointerDown={this.marqueeDown}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- forceScaling={true}
- fieldKey={this.annotationKey}
- isAnnotationOverlay={true}
- select={emptyFunction}
- active={this.annotationsActive}
- scaling={returnOne}
- ScreenToLocalTransform={this.screenToLocalTransform}
- whenActiveChanged={this.whenActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocumentWithTimestamp}
- CollectionView={undefined}
- renderDepth={this.props.renderDepth + 1}>
- {this.contentFunc}
- </CollectionFreeFormView>
+ <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
+ <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ fieldKey={this.annotationKey}
+ isAnnotationOverlay={true}
+ forceScaling={true}
+ select={emptyFunction}
+ active={this.annotationsActive}
+ scaling={returnOne}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocWithTimecode}
+ CollectionView={undefined}
+ renderDepth={this.props.renderDepth + 1}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ {this.uIButtons}
+ {this.annotationLayer}
+ {this.renderTimeline}
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+ <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />}
</div>
- {this.uIButtons}
- {this.annotationLayer}
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />}
</div >);
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 69f797880..37f268823 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -194,7 +194,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
this._iframe?.removeEventListener('wheel', this.iframeWheel);
}
- onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); }
+ onUrlDragover = (e: React.DragEvent) => { e.preventDefault(); };
@undoBatch
@action
@@ -280,8 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
}
_ignore = 0;
- onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; }
- onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; }
+ onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; };
+ onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; };
onPostPointer = (e: React.PointerEvent) => {
if (this._ignore !== e.timeStamp) e.stopPropagation();
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ac5ea66ff..e06a324d2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -230,7 +230,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100);
FormattedTextBox.SelectOnLoad = target[Id];
return target;
- }
+ };
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, {
dragComplete: e => {
@@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
DocListCast(this.dataDoc.links).map((l, i) => {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- this._linkTime = NumCast(la1.audioStart, NumCast(la2.audioStart));
+ this._linkTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime));
audioState = la2.audioState;
if (Doc.AreProtosEqual(la2, this.dataDoc)) {
la1 = l.anchor2 as Doc;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 992194e2b..dc630af74 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -604,7 +604,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
e.stopPropagation();
self.TextView.endUndoTypingBatch();
UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush");
- }
+ };
let label = "Stored marks: ";
if (this.brushMarks && this.brushMarks.size > 0) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e1ab1d3d3..9aacf5375 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1062,6 +1062,7 @@ export namespace Doc {
// all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
export function setDocFilter(target: Opt<Doc>, key: string, value: any, modifiers?: "remove" | "match" | "check" | "x" | undefined) {
+ console.log("m8");
const container = target ?? CollectionDockingView.Instance.props.Document;
const docFilters = Cast(container._docFilters, listSpec("string"), []);
runInAction(() => {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 67df56015..d52f3a928 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -19,10 +19,10 @@ export const documentSchema = createSchema({
lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide)
activeFrame: "number", // the active frame of a frame based animated document
_currentTimecode: "number", // current play back time of a temporal document (video / audio)
- displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+ _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
isLabel: "boolean", // whether the document is a label or not (video / audio)
- audioStart: "number", // the time frame where the audio should begin playing
- audioEnd: "number", // the time frame where the audio should stop playing
+ anchorStartTime: "number", // the time code where a document activates (eg in Audio or video timelines)
+ anchorEndTime: "number", // the time code where a document deactivates
markers: listSpec(Doc), // list of markers for audio / video
x: "number", // x coordinate when in a freeform view
y: "number", // y coordinate when in a freeform view