diff options
37 files changed, 701 insertions, 601 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 8fd906dc7..b32cbd3d0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,4 +1,4 @@ -import { runInAction, action } from "mobx"; +import { action, runInAction } from "mobx"; import { basename, extname } from "path"; import { DateField } from "../../fields/DateField"; import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; @@ -42,19 +42,19 @@ import { ImageBox } from "../views/nodes/ImageBox"; import { KeyValueBox } from "../views/nodes/KeyValueBox"; import { LabelBox } from "../views/nodes/LabelBox"; import { LinkBox } from "../views/nodes/LinkBox"; +import { LinkDescriptionPopup } from "../views/nodes/LinkDescriptionPopup"; import { PDFBox } from "../views/nodes/PDFBox"; import { PresBox } from "../views/nodes/PresBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; import { ScriptingBox } from "../views/nodes/ScriptingBox"; import { SliderBox } from "../views/nodes/SliderBox"; +import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; import { SearchBox } from "../views/search/SearchBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; -import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -import { LinkDescriptionPopup } from "../views/nodes/LinkDescriptionPopup"; const path = require('path'); const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @@ -113,6 +113,7 @@ export interface DocumentOptions { page?: number; description?: string; // added for links _viewScale?: number; + _overflow?: string; forceActive?: boolean; layout?: string | Doc; // default layout string for a document contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents @@ -609,7 +610,7 @@ export namespace Docs { proto.links = ComputedField.MakeFunction("links(self)"); viewDoc.author = Doc.CurrentUserEmail; - viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc); + viewDoc.type !== DocumentType.LINK && viewDoc.type !== DocumentType.LABEL && DocUtils.MakeLinkToActiveAudio(viewDoc); viewDoc["acl-Public"] = dataDoc["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; viewDoc["acl-Override"] = dataDoc["acl-Override"] = "None"; @@ -1007,10 +1008,10 @@ export namespace DocUtils { }); } - export let ActiveRecordings: Doc[] = []; + export let ActiveRecordings: AudioBox[] = []; export function MakeLinkToActiveAudio(doc: Doc) { - DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline")); + DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d.getAnchor() || d.props.Document }, "audio link", "audio timeline")); } export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { @@ -1250,14 +1251,13 @@ export namespace DocUtils { }); }); if (x !== undefined && y !== undefined) { - const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100 }); + const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _overflow: "visible" }); newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55; newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55; newCollection._width = newCollection._height = 110; //newCollection.borderRounding = "40px"; newCollection._jitterRotation = 10; newCollection._backgroundColor = "gray"; - newCollection._overflow = "visible"; return newCollection; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 00ee0f4b9..382b225a4 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -795,10 +795,11 @@ export class CurrentUserUtils { treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); - (doc.myFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); } + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([]); scriptContext._docFilters = scriptContext._docRangeFilters = undefined;`, { scriptContext: Doc.name }); + (doc.myFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.myFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); + } static setupUserDoc(doc: Doc) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index d24348746..52ccfda74 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -529,15 +529,16 @@ export namespace DragManager { endDrag(); }; const upHandler = (e: PointerEvent) => { - dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); - options?.dragComplete?.(new DragCompleteEvent(false, dragData)); + const complete = new DragCompleteEvent(false, dragData); + dispatchDrag(eles, e, complete, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); + options?.dragComplete?.(complete); endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, + function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, complete: DragCompleteEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { const removed = dragEles.map(dragEle => { const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height, o: dragEle.style.overflow }; @@ -558,7 +559,6 @@ export namespace DragManager { }); const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); if (target) { - const complete = new DragCompleteEvent(false, dragData); target.dispatchEvent( new CustomEvent<DropEvent>("dashPreDrop", { bubbles: true, diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 96f5d78fd..a2bd0aad9 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -186,20 +186,21 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV @computed get pinButton() { const targetDoc = this.view0?.props.Document; - const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div></>}> + return !targetDoc ? (null) : <Tooltip title={ + <div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div>}> <div className="documentButtonBar-linker" style={{ color: "white" }} - onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, false)))}> + onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, { setPosition: e.shiftKey ? true : undefined })))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> - </div></Tooltip>; + </div> + </Tooltip>; } @undoBatch @action pinWithView = (targetDoc: Doc) => { if (targetDoc) { - TabDocView.PinDoc(targetDoc, false); + TabDocView.PinDoc(targetDoc); const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; const scrollable: boolean = (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking); const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 372dc5456..e1b9fe8aa 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -62,7 +62,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b @computed get Bounds(): { x: number, y: number, b: number, r: number } { - return SelectionManager.Views().map(dv => dv.getBounds()).reduce((bounds, rect) => + const boudns = SelectionManager.Views().map(dv => dv.getBounds()).reduce((bounds, rect) => !rect ? bounds : { x: Math.min(rect.left, bounds.x), @@ -71,6 +71,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b b: Math.max(rect.bottom, bounds.b) }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + return boudns; } titleBlur = action((commit: boolean) => { @@ -275,7 +276,6 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b // doc._width = (right - left) * element.props.ScreenToLocalTransform().Scale; doc._height = (bottom - top); doc._width = (right - left); - } index++; } @@ -423,7 +423,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b const width = (doc._width || 0); let height = (doc._height || (nheight / nwidth * width)); height = !height || isNaN(height) ? 20 : height; - const scale = docView.props.ScreenToLocalTransform().Scale * docView.ContentScale(); + const scale = docView.props.ScreenToLocalTransform().Scale; if (nwidth && nheight) { if (nwidth / nheight !== width / height && !dragBottom) { height = nheight / nwidth * width; @@ -559,7 +559,8 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b const canDelete = SelectionManager.Views().some(docView => { const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; const docAcl = GetEffectiveAcl(docView.props.Document); - return !docView.props.Document._stayInCollection && (collectionAcl === AclAdmin || collectionAcl === AclEdit || docAcl === AclAdmin); + return (!docView.props.Document._stayInCollection || docView.props.Document.isInkMask) && + (collectionAcl === AclAdmin || collectionAcl === AclEdit || docAcl === AclAdmin); }); const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection); const closeIcon = !canDelete ? (null) : ( @@ -568,7 +569,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b <FontAwesomeIcon className="documentdecorations-times" icon={"times"} size="lg" /> </div></Tooltip>); - const openIcon = !canOpen ? (null) : <Tooltip title={<div className="dash-tooltip">Open in Tab (ctrl: as alias, shift: in new collection)</div>} placement="top"><div className="documentDecorations-openInTab" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} onPointerDown={this.onMaximizeDown}> + const openIcon = !canOpen ? (null) : <Tooltip key="open" title={<div className="dash-tooltip">Open in Tab (ctrl: as alias, shift: in new collection)</div>} placement="top"><div className="documentDecorations-openInTab" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} onPointerDown={this.onMaximizeDown}> {SelectionManager.Views().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} </div> </Tooltip>; @@ -612,31 +613,35 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b }}> {closeIcon} {bounds.r - bounds.x < 100 ? null : titleArea} - {SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : - <Tooltip title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top"> - <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> - <FontAwesomeIcon icon={seldoc.finalLayoutKey.includes("icon") ? "window-restore" : "window-minimize"} className="documentView-minimizedIcon" /> - </div> - </Tooltip>} - {openIcon} - <div className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-centerCont"></div> - <div className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - <div className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> - {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - <Tooltip title={<div className="dash-tooltip">tap to select containing document</div>} placement="top"> - <div className="documentDecorations-levelSelector" - onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon className="documentdecorations-times" icon={"arrow-alt-circle-up"} size="lg" /> - </div></Tooltip>} - <div className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} + {(seldoc.rootDoc.annotationOn as Doc)?.type === DocumentType.AUDIO ? (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"> + <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> + <FontAwesomeIcon icon={seldoc.finalLayoutKey.includes("icon") ? "window-restore" : "window-minimize"} className="documentView-minimizedIcon" /> + </div> + </Tooltip>} + {openIcon} + <div key="tl" className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="t" className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="tr" className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="l" className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="c" className="documentDecorations-centerCont"></div> + <div key="r" className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="bl" className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div key="br" className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + + {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : + <Tooltip key="level" title={<div className="dash-tooltip">tap to select containing document</div>} placement="top"> + <div className="documentDecorations-levelSelector" + onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> + <FontAwesomeIcon className="documentdecorations-times" icon={"arrow-alt-circle-up"} size="lg" /> + </div></Tooltip>} + </> + } + <div key="rot" className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}>{useRotation && "⟲"}</div> - </div > {seldoc?.Document.type === DocumentType.FONTICON ? (null) : <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> <DocumentButtonBar views={SelectionManager.Views} /> diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index c38842c4f..8a07c5321 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -27,6 +27,7 @@ import { SnappingManager } from "../util/SnappingManager"; import { SearchBox } from "./search/SearchBox"; import { random } from "lodash"; import { DocumentView } from "./nodes/DocumentView"; +import { AudioBox } from "./nodes/AudioBox"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -121,6 +122,9 @@ export class KeyManager { DragManager.AbortDrag(); } else if (CollectionDockingView.Instance.HasFullScreen) { CollectionDockingView.Instance.CloseFullScreen(); + } else if (AudioBox.SelectingRegion) { + AudioBox.SelectingRegion = undefined; + doDeselect = false; } else { doDeselect = !ContextMenu.Instance.closeMenu(); } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 2dac44bf8..ce7f3d8d9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -14,6 +14,7 @@ export class InkStrokeProperties { private _lastFill = "#D0021B"; private _lastLine = "#D0021B"; private _lastDash = "2"; + private _inkDocs: { x: number, y: number, width: number, height: number }[] = []; @observable _lock = false; @observable _controlBtn = false; @@ -205,45 +206,42 @@ export class InkStrokeProperties { if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { const ink = Cast(doc.data, InkField)?.inkData; if (ink) { - const newPoints: { X: number, Y: number }[] = []; const order = controlNum % 4; for (var i = 0; i < ink.length; i++) { - if (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) - || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) { - newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); - } - else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } + newPoints.push( + (controlNum === i || + (order === 0 && i === controlNum + 1) || + (order === 0 && controlNum !== 0 && i === controlNum - 2) || + (order === 0 && controlNum !== 0 && i === controlNum - 1) || + (order === 3 && i === controlNum - 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) || + ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) + ) ? + { X: ink[i].X - xDiff, Y: ink[i].Y - yDiff } : + { X: ink[i].X, Y: ink[i].Y }); } const oldx = doc.x; const oldy = doc.y; - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); + const oldxs = ink.map(p => p.X); + const oldys = ink.map(p => p.Y); + const oldleft = Math.min(...oldxs); + const oldtop = Math.min(...oldys); Doc.GetProto(doc).data = new InkField(newPoints); - const xs2 = newPoints.map(p => p.X); - const ys2 = newPoints.map(p => p.Y); - const left2 = Math.min(...xs2); - const top2 = Math.min(...ys2); - const right2 = Math.max(...xs2); - const bottom2 = Math.max(...ys2); - doc._height = (bottom2 - top2); - doc._width = (right2 - left2); - //if points move out of bounds + const newxs = newPoints.map(p => p.X); + const newys = newPoints.map(p => p.Y); + const newleft = Math.min(...newxs); + const newtop = Math.min(...newys); + const newright = Math.max(...newxs); + const newbottom = Math.max(...newys); - doc.x = oldx - (left - left2); - doc.y = oldy - (top - top2); + //if points move out of bounds + doc._height = (newbottom - newtop) * inkView.props.ScreenToLocalTransform().Scale; + doc._width = (newright - newleft) * inkView.props.ScreenToLocalTransform().Scale; + doc.x = oldx - (oldleft - newleft) * inkView.props.ScreenToLocalTransform().Scale; + doc.y = oldy - (oldtop - newtop) * inkView.props.ScreenToLocalTransform().Scale; } } } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 8ed6fbd85..8df7f7eef 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -35,7 +35,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } private makeMask = () => { - this.props.Document._backgroundColor = "rgba(0,0,0,0.7)"; this.props.Document.mixBlendMode = "hard-light"; this.props.Document.color = "#9b9b9bff"; //this.props.Document._stayInCollection = true; @@ -48,6 +47,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume @action onControlDown = (e: React.PointerEvent, i: number): void => { //TODO:renew points before controlling + InkStrokeProperties.Instance?.control(0.001, 0.001, 1); setupMoveUpEvents(this, e, this.onControlMove, this.onControlup, (e) => { }); this._controlUndo = UndoManager.StartBatch("DocDecs set radius"); this._prevX = e.clientX; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 33bd7e77e..d6a455a22 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -369,7 +369,6 @@ height: 55px; top: 50%; left: -10px; - border: 1px solid black; border-radius: 8px; position: relative; z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 5eb8429f0..c1fafe3e6 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -28,7 +28,7 @@ import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; -import { UndoManager } from '../util/UndoManager'; +import { undoBatch, UndoManager } from '../util/UndoManager'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; @@ -46,7 +46,8 @@ import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; +import { FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; @@ -58,7 +59,7 @@ import { PDFMenu } from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; -import { DefaultStyleProvider } from './StyleProvider'; +import { DefaultStyleProvider, StyleProp } from './StyleProvider'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -295,6 +296,33 @@ export class MainView extends React.Component { doc.dockingConfig ? CurrentUserUtils.openDashboard(Doc.UserDoc(), doc) : CollectionDockingView.AddSplit(doc, "right"); } + + /** + * add lock and hide button decorations for the "Dashboards" flyout TreeView + */ + DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + const toggleField = undoBatch(action((e: React.MouseEvent, doc: Doc, field: string) => { + e.stopPropagation(); + doc[field] = doc[field] ? undefined : true; + })); + switch (property.split(":")[0]) { + case StyleProp.Decorations: + return !doc || property.includes(":afterHeader") || // bcz: Todo: afterHeader should be generalized into a renderPath that is a list of the documents rendered so far which would mimic much of CSS property selectors + DocListCast((Doc.UserDoc().myDashboards as Doc).data).some(dash => dash === doc || + DocListCast(dash.data).some(tabset => tabset === doc)) ? (null) : + <> + <div className={`styleProvider-treeView-hide${doc.hidden ? "-active" : ""}`} onClick={e => toggleField(e, doc, "hidden")}> + <FontAwesomeIcon icon={doc.hidden ? "eye-slash" : "eye"} size="sm" /> + </div> + <div className={`styleProvider-treeView-lock${doc.lockedPosition ? "-active" : ""}`} onClick={e => toggleField(e, doc, "lockedPosition")}> + <FontAwesomeIcon icon={doc.lockedPosition ? "lock" : "unlock"} size="sm" /> + </div> + </>; + } + return DefaultStyleProvider(doc, props, property); + } + + @computed get flyout() { return !this._flyoutWidth ? <div className={`mainView-libraryFlyout-out`}> {this.docButtons} @@ -313,8 +341,9 @@ export class MainView extends React.Component { PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} renderDepth={0} + scriptContext={CollectionDockingView.Instance.props.Document} focus={emptyFunction} - styleProvider={DefaultStyleProvider} + styleProvider={this._sidebarContent.proto === Doc.UserDoc().myDashboards ? this.DashboardStyleProvider : DefaultStyleProvider} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -386,8 +415,8 @@ export class MainView extends React.Component { {this.menuPanel} <div className={`mainView-innerContent${this.darkScheme ? "-dark" : ""}`}> {this.flyout} - < div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined, }} onPointerDown={this.onFlyoutPointerDown} > - <FontAwesomeIcon icon="chevron-left" color={this.darkScheme ? "white" : "black"} size="sm" /> + <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined, }} onPointerDown={this.onFlyoutPointerDown} > + <FontAwesomeIcon icon="chevron-left" color={this.darkScheme ? "white" : "black"} style={{ opacity: "50%" }} size="sm" /> </div> {this.dockingContent} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 312cfc73e..1d822439a 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; import { runInAction } from 'mobx'; -import { Doc, Opt, StrListCast } from "../../fields/Doc"; +import { Doc, Opt, StrListCast, LayoutSym } from "../../fields/Doc"; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; @@ -105,11 +105,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case DocumentType.PRES: docColor = docColor || (darkScheme() ? "#3e3e3e" : "white"); break; case DocumentType.FONTICON: docColor = undefined; break; case DocumentType.RTF: docColor = docColor || (darkScheme() ? "#2d2d2d" : "#f1efeb"); break; - case DocumentType.LABEL: - case DocumentType.INK: docColor = undefined; break; + 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.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.LINK: return "transparent"; case DocumentType.COL: + if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; docColor = docColor ? docColor : doc?._isGroup ? "#00000004" : // very faint highlight to show bounds of group (Doc.IsSystem(doc) ? (darkScheme() ? "rgb(62,62,62)" : "lightgrey") : // system docs (seen in treeView) get a grayish background @@ -135,6 +138,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | return StrCast(doc?.boxShadow, isBackground() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined : // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) `${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"; 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/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 85fcf6384..5670d45f5 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -389,7 +389,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp const isPinned = targetDoc && Doc.isDocPinned(targetDoc); return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> <button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderLeft: "1px solid gray" }} - onClick={e => TabDocView.PinDoc(targetDoc, isPinned)}> + onClick={e => TabDocView.PinDoc(targetDoc, { unpin: isPinned })}> <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> </button> </Tooltip>; @@ -399,7 +399,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @action pinWithView = (targetDoc: Opt<Doc>) => { if (targetDoc) { - TabDocView.PinDoc(targetDoc, false); + TabDocView.PinDoc(targetDoc); const presArray: Doc[] = PresBox.Instance?.sortArray(); const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 344a6c103..564939270 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -213,7 +213,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll TraceMobx(); if (!(this.doc instanceof Doc)) return (null); const background = this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); - const paddingX = `${NumCast(this.doc._xPadding, 10)}px`; + const paddingX = `${NumCast(this.doc._xPadding, 15)}px`; const paddingTop = `${NumCast(this.doc._yPadding, 20)}px`; const pointerEvents = !this.props.active() && !SnappingManager.GetIsDragging() && !this._isChildActive ? "none" : undefined; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 0d03936dc..5ca069fb9 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -24,7 +24,7 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocumentView, DocAfterFocusFunc, DocumentViewProps } from "../nodes/DocumentView"; import { FieldViewProps } from '../nodes/FieldView'; -import { PresBox, PresMovement } from '../nodes/PresBox'; +import { PresBox, PresMovement, PinProps } from '../nodes/PresBox'; import { DefaultLayerProvider, DefaultStyleProvider, StyleLayers, StyleProp } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; @@ -166,8 +166,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { * Adds a document to the presentation view **/ @action - public static async PinDoc(doc: Doc, unpin = false, audioRange?: boolean) { - if (unpin) console.log('TODO: Remove UNPIN from this location'); + public static async PinDoc(doc: Doc, pinProps?: PinProps) { + if (pinProps?.unpin) console.log('TODO: Remove UNPIN from this location'); //add this new doc to props.Document const curPres = CurrentUserUtils.ActivePresentation; if (curPres) { @@ -183,12 +183,22 @@ export class TabDocView extends React.Component<TabDocViewProps> { const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; Doc.AddDocToList(curPres, "data", pinDoc, presSelected); - if (!audioRange && (pinDoc.type === DocumentType.AUDIO || pinDoc.type === DocumentType.VID)) { + if (!pinProps?.audioRange && (pinDoc.type === DocumentType.AUDIO || pinDoc.type === DocumentType.VID)) { pinDoc.mediaStart = "manual"; pinDoc.mediaStop = "manual"; pinDoc.presStartTime = 0; pinDoc.presEndTime = pinDoc.type === DocumentType.AUDIO ? doc.duration : NumCast(doc["data-duration"]); } + //save position + if (pinProps?.setPosition || pinDoc.isInkMask) { + pinDoc.setPosition = true; + pinDoc.y = doc.y; + pinDoc.x = doc.x; + pinDoc.presHideAfter = true; + pinDoc.presHideBefore = true; + pinDoc.title = doc.title + " (move)"; + pinDoc.presMovement = PresMovement.None; + } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; const dview = CollectionDockingView.Instance.props.Document; const fieldKey = CollectionDockingView.Instance.props.fieldKey; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 7a654c7cf..067675038 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -98,10 +98,21 @@ width: unset; } - >svg { - display: none; + .right-buttons-container { + display: flex; + align-items: center; + margin-left: 0.25rem; + opacity: 0.75; + + >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide, .styleProvider-treeView-lock-active, .styleProvider-treeView-hide-active { + margin-left: 0.25rem; + margin-right: 0.25rem; + } + + >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + display: none; + } } - } .treeView-header:hover { @@ -109,10 +120,6 @@ display: inherit; } - >svg { - display: inherit; - } - .treeView-openRight { display: inline-block; height: 17px; @@ -125,6 +132,12 @@ margin-left: 3px; } } + + .right-buttons-container { + >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + display: inherit; + } + } } .treeView-header-above { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 93d3be1fc..c4934cf45 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -31,6 +31,7 @@ import { CollectionTreeView } from './CollectionTreeView'; import { CollectionView, CollectionViewType } from './CollectionView'; import "./TreeView.scss"; import React = require("react"); +import { SliderBox } from '../nodes/SliderBox'; export interface TreeViewProps { document: Doc; @@ -378,7 +379,8 @@ export class TreeView extends React.Component<TreeViewProps> { TraceMobx(); const expandKey = this.treeViewExpandedView; if (["links", "annotations", this.fieldKey].includes(expandKey)) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); + const key = expandKey === "annotations" ? this.fieldKey + "-annotations" : expandKey; + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't), // then the modification would be done here @@ -390,7 +392,7 @@ export class TreeView extends React.Component<TreeViewProps> { docs.sort((a, b) => NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1); docs.forEach((d, i) => d.zIndex = i); } - const added = Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); + const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); added && (doc.context = this.doc.context); return added; }; @@ -466,6 +468,7 @@ export class TreeView extends React.Component<TreeViewProps> { } </div>; } + @computed get showTitleEditorControl() { return ["*", this._uniqueId, this.props.treeView._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); } @computed get headerElements() { return (Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode) || this.props.treeViewHideHeaderFields() ? (null) : @@ -567,6 +570,7 @@ export class TreeView extends React.Component<TreeViewProps> { parentActive={returnTrue} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} + cantBrush={this.props.treeView.props.cantBrush} dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -584,7 +588,10 @@ export class TreeView extends React.Component<TreeViewProps> { }} > {view} </div > - {this.headerElements} + <div className={"right-buttons-container"}> + {this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containingCollection) ? ":afterHeader" : ""))} {/* hide and lock buttons */} + {this.headerElements} + </div> </>; } @@ -612,15 +619,17 @@ export class TreeView extends React.Component<TreeViewProps> { } renderEmbeddedDocument = (asText: boolean) => { - const panelWidth = asText || StrCast(Doc.LayoutField(this.layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.expandPanelWidth; - const panelHeight = asText ? this.rtfOutlineHeight : StrCast(Doc.LayoutField(this.layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.expandPanelHeight; + const layout = StrCast(Doc.LayoutField(this.layoutDoc)); + const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name); + const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth; + const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight; return <DocumentView key={this.doc[Id]} ref={action((r: DocumentView | null) => this._dref = r)} Document={this.doc} DataDoc={undefined} PanelWidth={panelWidth} PanelHeight={panelHeight} - NativeWidth={!asText && this.layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} - NativeHeight={!asText && this.layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} + NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} + NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} fitContentsToDoc={true} hideTitle={asText} LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined} @@ -642,6 +651,7 @@ export class TreeView extends React.Component<TreeViewProps> { whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} + cantBrush={this.props.treeView.props.cantBrush} bringToFront={returnFalse} />; } diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index f16d13a4e..93cdbbb09 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -4,7 +4,6 @@ height: 100%; position: inherit; display: flex; - pointer-events: all; position: relative; cursor: default; @@ -125,7 +124,6 @@ margin-top: auto; margin-bottom: auto; width: 100%; - height: 80%; position: relative; padding-right: 5px; display: flex; @@ -135,7 +133,6 @@ margin-top: auto; margin-bottom: auto; margin-right: 2px; - width: 30px; height: 25px; padding: 2px; border-radius: 50%; @@ -163,7 +160,6 @@ .audiobox-timeline { position: relative; - height: 80%; width: 100%; background: white; border: gray solid 1px; @@ -280,16 +276,20 @@ } } - .audiobox-marker-container1, + .audiobox-marker-timeline, .audiobox-marker-minicontainer { position: absolute; width: 10px; height: 90%; top: 2.5%; - background: gray; border-radius: 5px; - box-shadow: black 2px 2px 1px; - opacity: 0.3; + + .left-resizer { + background: dimgrey; + } + .resizer { + background: dimgrey; + } .audiobox-marker { position: relative; @@ -303,10 +303,12 @@ .resizer { position: absolute; + top: 0; right: 0; + pointer-events: all; cursor: ew-resize; height: 100%; - width: 2px; + width: 10px; z-index: 100; } @@ -320,14 +322,16 @@ .left-resizer { position: absolute; left: 0; + top : 0; + pointer-events: all; cursor: ew-resize; height: 100%; - width: 2px; + width: 10px; z-index: 100; } } - .audiobox-marker-container1:hover, + .audiobox-marker-timeline:hover, .audiobox-marker-minicontainer:hover { opacity: 0.8; } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 72d504590..77777ff76 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -3,33 +3,32 @@ 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 { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, DateCast, NumCast } from "../../../fields/Types"; +import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, 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 { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; -import { StyleProp } from "../StyleProvider"; -import "./AudioBox.scss"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; +import { DocumentView } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; -import { LinkAnchorBox } from "./LinkAnchorBox"; import { LinkDocPreview } from "./LinkDocPreview"; +import "./AudioBox.scss"; declare class MediaRecorder { // whatever MediaRecorder has @@ -45,15 +44,15 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); } public static Enabled = false; public static NUMBER_OF_BUCKETS = 100; - + 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; - // _linkPlayDisposer: IReactionDisposer | undefined; - // _reactionDisposer: IReactionDisposer | undefined; - // _scrubbingDisposer: IReactionDisposer | undefined; - private _disposers: { [name: string]: IReactionDisposer } = {}; + _disposers: { [name: string]: IReactionDisposer } = {}; _ele: HTMLAudioElement | null = null; _recorder: any; _recordStart = 0; @@ -64,145 +63,100 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD _start: number = 0; _hold: boolean = false; _left: boolean = false; - _first: boolean = false; + _dragging = false; _play: any = null; - - _count: Array<any> = []; _audioRef = React.createRef<HTMLDivElement>(); _timeline: Opt<HTMLDivElement>; - _duration = 0; _markerStart: number = 0; - private _currMarker: any; + _currMarker: any; - @observable _visible: boolean = false; + @observable static SelectingRegion: AudioBox | undefined = undefined; + @observable static _scrubTime = 0; @observable _markerEnd: number = 0; @observable _position: number = 0; @observable _waveHeight: Opt<number> = this.layoutDoc._height; - @observable private _paused: boolean = false; - @observable private static _scrubTime = 0; + @observable _paused: boolean = false; @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); } 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); } - async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); } + @computed get markerDocs() { 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 constructor(props: Readonly<FieldViewProps>) { super(props); AudioBox.Instance = this; // onClick play scripts - AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!; - AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!; + 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" })!; } getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2_timecode); + const linkTime = NumCast(la2.audioStart, NumCast(la1.audioStart)); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1_timecode); } return { la1, la2, linkTime }; } componentWillUnmount() { - this._disposers.reaction?.(); - this._disposers.linkPlay?.(); - this._disposers.scrubbing?.(); - this._disposers.audioStart?.(); + 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() { - if (!this.dataDoc.markerAmount) { - this.dataDoc.markerAmount = 0; - } + 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.audioState = this.path ? "paused" : undefined; - this._disposers.linkPlay = reaction(() => this.layoutDoc.scrollToLinkID, - scrollLinkId => { - if (scrollLinkId) { - DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => { - const { linkTime } = this.getLinkData(l); - setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250); - }); - Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); - } - }, { fireImmediately: true }); - - // for play when link is selected - this._disposers.reaction = reaction(() => SelectionManager.Views(), - selected => { - const sel = selected.length ? selected[0].props.Document : undefined; - let link; - sel && DocListCast(this.dataDoc.links).forEach(l => { - if (l.anchor1 === sel || l.anchor2 === sel && !sel.audioStart) { - link = this.playLink(sel); - } - }); - // for links created during recording - if (!link) { - this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime()); - this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause(); - } - }); + this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); this._disposers.audioStart = reaction( - () => this.Document._audioStart, - (audioStart) => { - if (audioStart !== undefined) { - if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - const delay = this._audioRef.current ? 0 : 250; // wait for mainCont and try again to play - const startTime: number = NumCast(this.Document._audioStart); - setTimeout(() => this._audioRef.current && this.playFrom(startTime), delay); - setTimeout(() => { this.Document._currentTimecode = startTime; this.Document._audioStart = undefined; }, 10 + delay); - } - } - }, + () => !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); + setTimeout(() => { + this.Document._currentTimecode = audioStart; + this.Document._audioStart = undefined; + }, 10); + }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); this._disposers.audioStop = reaction( - () => this.Document._audioStop, - (audioStop) => { - if (audioStop !== undefined) { - if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - const delay = this._audioRef.current ? 0 : 250; // wait for mainCont and try again to play - setTimeout(() => this._audioRef.current && this.pause(), delay); - setTimeout(() => { this.Document._audioStop = undefined; }, 10 + delay); - } - } - }, + () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined, + audioStop => audioStop !== undefined && setTimeout(() => { + 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) => { - let link = false; - !Doc.AreProtosEqual(doc, this.props.Document) && DocListCast(this.props.Document.links).forEach(l => { - if (l.anchor1 === doc || l.anchor2 === doc) { - const { la1, la2, linkTime } = this.getLinkData(l); - let startTime = linkTime; - if (la2.audioStart) startTime = NumCast(la2.audioStart); - if (la1.audioStart) startTime = NumCast(la1.audioStart); - - let endTime; - if (la1.audioEnd) endTime = NumCast(la1.audioEnd); - if (la2.audioEnd) endTime = NumCast(la2.audioEnd); - - if (startTime) { - link = true; - this.layoutDoc.playOnSelect && this.recordingStart && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); - } + 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)); } }); - - this.layoutDoc.playOnSelect && this.recordingStart && Doc.AreProtosEqual(doc, this.props.Document) && this.pause(); - return link; + doc.annotationOn === this.rootDoc && this.playFrom(NumCast(doc.audioStart), Cast(doc.audioEnd, "number", null)); } // for updating the timecode @@ -210,7 +164,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const htmlEle = this._ele; if (this.audioState !== "recording" && htmlEle) { htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); - DocListCast(this.dataDoc.links).map(l => { + this.links.map(l => { const { la1, linkTime } = this.getLinkData(l); if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); @@ -226,16 +180,31 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this.audioState = "paused"; }); - // play audio for documents created during recording + // play audio for documents created during recording playFromTime = (absoluteTime: number) => { this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000); } // 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) => { clearTimeout(this._play); - this._duration = endTime - seekTimeInSeconds; if (Number.isNaN(this._ele?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); } else if (this._ele && AudioBox.Enabled) { @@ -250,7 +219,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._ele.play(); runInAction(() => this.audioState = "playing"); if (endTime !== this.audioDuration) { - this._play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } } else { this.pause(); @@ -275,7 +244,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this._recorder = new MediaRecorder(this._stream); this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); - DocUtils.ActiveRecordings.push(this.props.Document); + DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); if (!(result instanceof Error)) { @@ -294,20 +263,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const funcs: ContextMenuProps[] = []; 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.hideLabels ? "Don't hide" : "Hide") + " label markers", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } - // stops the recording + // stops the recording stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; this.audioState = "paused"; this._stream?.getAudioTracks()[0].stop(); - const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document); + const ind = DocUtils.ActiveRecordings.indexOf(this); ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); }); @@ -371,7 +339,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._paused = true; this._recorder.pause(); e.stopPropagation(); - } // continue the recording @@ -381,52 +348,58 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._paused = false; this._recorder.resume(); e.stopPropagation(); - - } - - // return the total time paused to update the correct time - @computed get pauseTime() { - return this._pauseEnd - this._pauseStart; } // starting the drag event for marker resizing @action onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = (e.target as any).getBoundingClientRect(); - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; - this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); - setupMoveUpEvents(this, e, - action((e: PointerEvent) => { - this._visible = true; - this._markerEnd = toTimeline(e.clientX - rect.x); - if (this._markerEnd < this._markerStart) { - const tmp = this._markerStart; - this._markerStart = this._markerEnd; - this._markerEnd = tmp; + 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); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); + if (this._markerEnd < this._markerStart) { + const tmp = this._markerStart; + this._markerStart = this._markerEnd; + this._markerEnd = tmp; + } + AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + AudioBox.SelectingRegion = undefined; + }), + e => { + this.props.select(false); + e.shiftKey && this.createMarker(this._ele!.currentTime); } - return false; - }), - action((e: PointerEvent, movement: number[]) => { - (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); - this._visible = false; - }), - (e: PointerEvent) => e.shiftKey && this.createMarker(this._ele!.currentTime) - ); + , this.props.isSelected(true) || this._isChildActive); + } } @action - createMarker(audioStart: number, audioEnd?: number) { + 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, isLabel: audioEnd === undefined, + 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 }); - marker.data = ""; // clears out the label's text so that only its border will display 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 @@ -436,12 +409,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._timeline?.setPointerCapture(e.pointerId); const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.audioDuration; setupMoveUpEvents(this, e, - (e: PointerEvent) => { + (e) => { const rect = (e.target as any).getBoundingClientRect(); this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); return false; }, - (e: PointerEvent) => { + (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); @@ -452,7 +425,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // updates the marker with the new time @action changeMarker = (m: any, time: any) => { - DocListCast(this.dataDoc[this.annotationKey]).filter(marker => this.isSame(marker, m)).forEach(marker => + this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => this._left ? marker.audioStart = time : marker.audioEnd = time); } @@ -461,48 +434,31 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; } - // instantiates a new array of size 500 for marker layout - markers = () => { - const increment = this.audioDuration / 500; - this._count = []; - for (let i = 0; i < 500; i++) { - this._count.push([increment * i, 0]); - } - } - // makes sure no markers overlaps each other by setting the correct position and width - isOverlap = (m: any) => { - if (this._first) { - this._first = false; - this.markers(); - } + 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; - - for (let i = 0; i < 500; i++) { - if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { - this._count[i][1]++; - - if (this._count[i][1] > max) { - max = this._count[i][1]; - } + 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; } - } - - for (let i = 0; i < 500; i++) { - if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { - this._count[i][1] = max; - } - } + })); + let level = max + 1; + for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - if (this.dataDoc.markerAmount < max) { - this.dataDoc.markerAmount = max; - } - return max - 1; + placed.push({ audioStart: x1, audioEnd: x2, level }); + return level; } @computed get selectionContainer() { - return <div className="audiobox-container" style={{ - left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`, + return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.audioDuration * 100}%`, width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%" }} />; } @@ -515,7 +471,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD color={"darkblue"} height={this._waveHeight} barWidth={0.1} - // pos={this.layoutDoc._currentTimecode} need to correctly resize parent to make this work (not very necessary for function) pos={this.audioDuration} duration={this.audioDuration} peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined} @@ -537,30 +492,55 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD ); } - rangeScript = () => AudioBox.RangeScript; - labelScript = () => AudioBox.LabelScript; - static audioStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => { - if (property === StyleProp.BackgroundColor) return "transparent"; - if (property === StyleProp.PointerEvents) return "none"; - } - render() { - const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : ""; - this._first = true; // for indicating the first marker that is rendered - const markerDoc = (mark: Doc, script: undefined | (() => ScriptField)) => { - return <DocumentView {...this.props} + 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} - pointerEvents={"all"} + PanelWidth={() => width} + PanelHeight={() => height} + focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} ContainingCollectionDoc={this.props.Document} removeDocument={this.removeDocument} - parentActive={returnTrue} - onClick={this.layoutDoc.playOnClick ? script : undefined} + 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} />; + scriptContext={this} /> }; - return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} style={{ pointerEvents: !interactive ? "none" : undefined }}> + }); + 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)} /> + </>} + </>; + }); + + 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) + 2; + return <div className="audiobox-container" + onContextMenu={this.specificContextMenu} + onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} + style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }}> {!this.path ? <div className="audiobox-buttons"> <div className="audiobox-dictation" onClick={this.onFile}> @@ -569,10 +549,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD {this.audioState === "recording" || this.audioState === "paused" ? <div className="recording" onClick={e => e.stopPropagation()}> <div className="buttons" onClick={this.recordClick}> - <FontAwesomeIcon style={{ width: "100%" }} icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}> - <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> </div> @@ -581,74 +561,33 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD RECORD </button>} </div> : - <div className="audiobox-controls" > - <div className="audiobox-dictation"></div> - <div className="audiobox-player" > - <div className="audiobox-playhead" 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" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => { - if (e.button === 0 && !e.ctrlKey) { - const rect = (e.target as any).getBoundingClientRect(); - - if (e.target !== this._audioRef.current) { - const wasPaused = this.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; - wasPaused && this.pause(); - } - - this.onPointerDownTimeline(e); - } - }}> + <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="waveform"> {this.waveform} </div> - {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => - (!m.isLabel) ? - (this.layoutDoc.hideMarkers) ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container1`} key={i} - title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`} - style={{ - left: `${NumCast(m.audioStart) / this.audioDuration * 100}%`, - top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%`, - width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / this.audioDuration * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%` - }} - onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} > - <div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div> - {markerDoc(m, this.rangeScript)} - <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div> - </div> - : - (this.layoutDoc.hideLabels) ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={i} - style={{ left: `${NumCast(m.audioStart) / this.audioDuration * 100}%` }}> - {markerDoc(m, this.labelScript)} - </div> - )} - {DocListCast(this.dataDoc.links).map((l, i) => { - const { la1, la2, linkTime } = this.getLinkData(l); - let startTime = linkTime; - if (la2.audioStart && !la2.audioEnd) { - startTime = NumCast(la2.audioStart); - } - - return !linkTime ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={l[Id]} style={{ left: `${startTime / this.audioDuration * 100}%` }} onClick={e => e.stopPropagation()}> - <DocumentView {...this.props} - Document={l} - rootSelected={returnFalse} - ContainingCollectionDoc={this.props.Document} - parentActive={returnTrue} - bringToFront={emptyFunction} - styleProvider={AudioBox.audioStyleProvider} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} - /> - <div key={i} className={`audiobox-marker`} onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(startTime); e.stopPropagation(); e.preventDefault(); } }} /> + {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._visible ? this.selectionContainer : null} - + {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> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index bfb6e8fc8..09d89170c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -169,6 +169,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return <div className={"collectionFreeFormDocumentView-container"} style={{ outline: this.Highlight ? "orange solid 2px" : "", + width: this.panelWidth(), + height: this.panelHeight(), transform: this.transform, transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), zIndex: this.ZInd, diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index e7a94580a..2cac2d0b8 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -160,6 +160,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.AnnotationUri = undefined; //!this.props.StartLink } else if (startLink !== endLink) { + endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink; + startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink; const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag", undefined, undefined, true); // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved if (endLinkView) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e99b0a155..6371ae5fb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -29,6 +29,7 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; +import { InkingStroke } from "../InkingStroke"; import { InkStrokeProperties } from '../InkStrokeProperties'; import { StyleLayers, StyleProp } from "../StyleProvider"; import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; @@ -51,6 +52,7 @@ export interface DocumentViewSharedProps { fitContentsToDoc?: boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; + setContentView?: (view: { getAnchor: () => Doc }) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; PanelWidth: () => number; PanelHeight: () => number; @@ -74,6 +76,7 @@ export interface DocumentViewSharedProps { dropAction?: dropActionType; dontRegisterView?: boolean; ignoreAutoHeight?: boolean; + cantBrush?: boolean; // whether the document doesn't show brush highlighting pointerEvents?: string; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document } @@ -376,14 +379,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const func = () => this.onDoubleClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, + scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView, shiftKey: e.shiftKey }, console.log); undoBatch(func)(); } else if (!Doc.IsSystem(this.props.Document)) { if (this.props.Document.type === DocumentType.INK) { InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); - } else { + } else if (this.props.Document.type !== DocumentType.LABEL) { UndoManager.RunInBatch(() => { const fullScreenDoc = Cast(this.props.Document._fullScreenView, Doc, null) || this.props.Document; this.props.addDocTab(fullScreenDoc, "add"); @@ -411,7 +416,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } else func(); }; if (this.onDoubleClickHandler) { - this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 500); + this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); } else clickFunc(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); @@ -548,7 +553,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const linkSource = de.complete.annoDragData ? de.complete.annoDragData.annotationDocument : de.complete.linkDragData ? de.complete.linkDragData.linkSourceDocument : undefined; if (linkSource && linkSource !== this.props.Document) { e.stopPropagation(); - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: this.props.Document }, "link", undefined, undefined, undefined, [de.x, de.y]); + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkSource }, { doc: this._componentView?.getAnchor() || this.rootDoc }, "link", undefined, undefined, undefined, [de.x, de.y]); } } @@ -592,9 +597,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); this.props.contextMenuItems?.().forEach(item => - item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); const appearance = cm.findByDescription("UI Controls..."); @@ -688,10 +693,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - makeLink = () => this.props.DocumentView._link; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined. + makeLink = () => this.props.DocumentView.LinkBeingCreated; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined. + setContentView = (view: { getAnchor: () => Doc }) => this._componentView = view; @observable contentsActive: () => boolean = returnFalse; @action setContentsActive = (setActive: () => boolean) => this.contentsActive = setActive; - + _componentView: { getAnchor: () => Doc } | undefined; @computed get contents() { TraceMobx(); return <div className="documentView-contentsView" @@ -700,6 +706,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView key={1} {...this.props} + setContentView={this.setContentView} scaling={this.contentScaling} PanelHeight={this.panelHeight} contentsActive={this.setContentsActive} @@ -715,15 +722,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; } - // used to decide whether a link anchor view should be created or not. - // if it's a temporal link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here. - // would be good to generalize this some way. - isNonTemporalLink = (linkDoc: Doc) => { - const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc; - const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1_timecode : linkDoc.anchor2_timecode; - return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true; - } - @undoBatch hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true) anchorPanelWidth = () => this.props.PanelWidth() || 1; @@ -738,7 +736,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink(d)); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden); return filtered.map((d, i) => <div className="documentView-anchorCont" key={i + 1}> <DocumentView {...this.props} @@ -820,7 +818,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"])[highlightIndex]; const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex]; - let highlighting = highlightIndex && ![DocumentType.FONTICON, DocumentType.INK].includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; + let highlighting = !this.props.cantBrush && highlightIndex && ![DocumentType.FONTICON, DocumentType.INK].includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way const boxShadow = highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : @@ -964,9 +962,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { {!this.props.Document || !this.props.PanelWidth() ? (null) : ( <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef} style={{ + position: this.props.Document.isInkMask ? "absolute" : undefined, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: Math.abs(this.Xshift) > 0.001 ? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth(), - height: Math.abs(this.YShift) > 0.001 ? this.props.Document._fitWidth ? `${this.panelHeight}px` : `${100 * this.nativeHeight / this.nativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), + width: this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) > 0.001 ? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth(), + height: this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.YShift) > 0.001 ? this.props.Document._fitWidth ? `${this.panelHeight}px` : `${100 * this.nativeHeight / this.nativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), }}> <DocumentViewInternal {...this.props} {...internalProps} ref={action((r: DocumentViewInternal | null) => this.docView = r)} /> </div>)} diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 730ae8f10..badacbc5c 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -102,7 +102,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc if (docRangeFilters) { let index: number; while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { - docRangeFilters.splice(index, 1); + docRangeFilters.splice(index, 3); } } } else { @@ -122,13 +122,13 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc }); let newFacet: Opt<Doc>; if (facetHeader === "text" || facetValues.rtFields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); + newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, system: true, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet._textBoxPadding = 4; const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", treeViewOpen: true }); + newFacet = Docs.Create.SliderDocument({ title: facetHeader, _overflow: "visible", _height: 40, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", treeViewOpen: true }); const newFacetField = Doc.LayoutFieldKey(newFacet); const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox @@ -146,13 +146,13 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc newFacet.title = facetHeader; newFacet.treeViewOpen = true; newFacet.type = DocumentType.COL; - const capturedVariables = { layoutDoc: targetDoc, _stayInCollection: true, _hideContextMenu: true, dataDoc: (targetDoc.data as any)[0][DataSym] }; + const capturedVariables = { layoutDoc: targetDoc, system: true, _stayInCollection: true, _hideContextMenu: true, dataDoc: (targetDoc.data as any)[0][DataSym] }; newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, "${facetHeader}")`, {}, capturedVariables); } newFacet && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); } } - filterBackground = () => "rgba(105, 105, 105, 0.432)"; + @computed get scriptField() { const scriptText = "setDocFilter(this?.target, heading, this.title, checked)"; const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); @@ -184,6 +184,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc DataDoc={Doc.GetProto(facetCollection)} fieldKey={`${this.props.fieldKey}`} CollectionView={undefined} + cantBrush={true} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} @@ -208,7 +209,8 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc treeViewHideHeaderFields={true} onCheckedClick={this.scriptField} dontRegisterView={true} - styleProvider={this.filterBackground} + styleProvider={this.props.styleProvider} + scriptContext={this.props.scriptContext} moveDocument={returnFalse} removeDocument={returnFalse} addDocument={returnFalse} /> diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 393ba07e6..b0e7f4ce5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -413,7 +413,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, borderRadius }} > - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} CollectionView={undefined} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index bc2090a33..3448a4abd 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -67,6 +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)); return ( <div className="labelBox-outerDiv" onClick={action(() => this.clicked = !this.clicked)} @@ -88,7 +89,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument paddingBottom: NumCast(this.layoutDoc._yPadding), whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - {StrCast(this.rootDoc[this.fieldKey], StrCast(this.rootDoc.title))} + {label.startsWith("#") ? (null) : label} </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index e347d1304..d86dfd7b1 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -77,6 +77,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch LinkManager.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); + e.stopPropagation(); } } else { this._timeout && clearTimeout(this._timeout); diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 42462000b..aef776563 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -66,6 +66,12 @@ export enum PresColor { SlideBackground = "#d5dce2", } +export class PinProps { + audioRange?: boolean; + unpin?: boolean; + setPosition?: boolean; +} + type PresBoxSchema = makeInterface<[typeof documentSchema]>; const PresBoxDocument = makeInterface(documentSchema); @@ -423,8 +429,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection); // documents open in new tab instead of on right } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. @@ -433,6 +439,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.navigateToView(targetDoc, activeItem); } // TODO: Add progressivize for navigating web (storing websites for given frames) + } /** @@ -536,6 +543,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.next(); } } + await timer(duration); this.next(); // then the created Promise can be awaited if (i === this.childDocs.length - 1) { setTimeout(() => { @@ -709,7 +717,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> audio.presStartTime = NumCast(doc.audioStart); audio.presEndTime = NumCast(doc.audioEnd); audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart); - TabDocView.PinDoc(audio, false, true); + TabDocView.PinDoc(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; } @@ -766,11 +774,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Regular click @action - selectElement = (doc: Doc) => { + selectElement = async (doc: Doc) => { const context = Cast(doc.context, Doc, null); this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0); else this.updateCurrentPresentation(context); + + if (this.activeItem.setPosition && + this.activeItem.y !== undefined && + this.activeItem.x !== undefined && + this.targetDoc.x !== undefined && + this.targetDoc.y !== undefined) { + const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms)); + const time = 10; + const ydiff = NumCast(this.activeItem.y) - NumCast(this.targetDoc.y); + const xdiff = NumCast(this.activeItem.x) - NumCast(this.targetDoc.x); + + for (let i = 0; i < time; i++) { + this.targetDoc.x = NumCast(this.targetDoc.x) + xdiff / time; + this.targetDoc.y = NumCast(this.targetDoc.y) + ydiff / time; + await timer(0.1); + } + } } //Command click @@ -1711,7 +1736,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const presData = Cast(this.rootDoc.data, listSpec(Doc)); if (data && presData) { data.push(doc); - TabDocView.PinDoc(doc, false); + TabDocView.PinDoc(doc); this.gotoDocument(this.childDocs.length, this.activeItem); } else { this.props.addDocTab(doc, "add:right"); diff --git a/src/client/views/nodes/SliderBox-components.tsx b/src/client/views/nodes/SliderBox-components.tsx index 874a1108f..e19f28f08 100644 --- a/src/client/views/nodes/SliderBox-components.tsx +++ b/src/client/views/nodes/SliderBox-components.tsx @@ -11,7 +11,6 @@ const railStyle: React.CSSProperties = { position: "absolute", width: "100%", height: 40, - top: -13, borderRadius: 7, cursor: "pointer", opacity: 0.3, @@ -231,7 +230,7 @@ export function Tick({ tick, count, format = defaultFormat }: TickProps) { <div style={{ position: "absolute", - marginTop: 17, + marginTop: 20, width: 1, height: 5, backgroundColor: "rgb(200,200,200)", diff --git a/src/client/views/nodes/SliderBox.scss b/src/client/views/nodes/SliderBox.scss index 78015bd70..4206a368d 100644 --- a/src/client/views/nodes/SliderBox.scss +++ b/src/client/views/nodes/SliderBox.scss @@ -1,7 +1,19 @@ .sliderBox-outerDiv { - width: 100%; + width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box height: 100%; border-radius: inherit; display: flex; flex-direction: column; + position: relative; + .slider-tracks { + top: 7px; + position: relative; + } + .slider-ticks { + position: relative; + } + .slider-handles { + top: 7px; + position: relative; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 2aa71b2bd..bfe07c22b 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -41,7 +41,10 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume onChange = (values: readonly number[]) => runInAction(() => { this.dataDoc[this.minThumbKey] = values[0]; this.dataDoc[this.maxThumbKey] = values[1]; - Cast(this.layoutDoc.onThumbChanged, ScriptField, null)?.script.run({ self: this.rootDoc, range: values, this: this.layoutDoc }); + Cast(this.layoutDoc.onThumbChanged, ScriptField, null)?.script.run({ + self: this.rootDoc, + scriptContext: this.props.scriptContext, range: values, this: this.layoutDoc + }); }) render() { @@ -50,10 +53,12 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume return domain[1] <= domain[0] ? (null) : ( <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> - <div className="sliderBox-mainButton" onContextMenu={this.specificContextMenu} style={{ - background: StrCast(this.layoutDoc.backgroundColor), color: StrCast(this.layoutDoc.color, "black"), - fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) - }} > + <div className="sliderBox-mainButton" + onContextMenu={this.specificContextMenu} style={{ + background: StrCast(this.layoutDoc.backgroundColor), + color: StrCast(this.layoutDoc.color, "black"), + fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) + }} > <Slider mode={2} step={1} @@ -101,7 +106,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume </Tracks> <Ticks count={5}> {({ ticks }) => ( - <div className="slider-tracks"> + <div className="slider-ticks"> {ticks.map((tick) => ( <Tick key={tick.id} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 9cdd6465f..76edda847 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -15,12 +15,13 @@ position: absolute; height: 20%; width: 100%; - bottom: -60px; + bottom: 0px; background: white; border: gray solid 1px; border-radius: 3px; z-index: 1000; overflow: hidden; + left: 0px; .audiobox-current { width: 1px; @@ -70,7 +71,7 @@ } } - .audiobox-marker-container1, + .audiobox-marker-timeline, .audiobox-marker-minicontainer { position: absolute; width: 10px; @@ -115,6 +116,11 @@ width: 2px; z-index: 100; } + + // .contentFittingDocumentView-previewDoc { + // width: 100% !important; + // transform: none !important; + // } } .audiobox-marker-container1:hover, @@ -173,14 +179,14 @@ pointer-events:all; } -.timeline-button { - position: absolute; - bottom: 15px; - right: 17%; - color: lightgrey; - width: 20px; +// .timeline-button { +// position: absolute; +// bottom: 35px; +// right: 235px; +// color: lightgrey; +// width: 20px; -} +// } .videoBox-play { width: 25px; height: 20px; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 060953e7b..67e8d74b3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -28,6 +28,8 @@ import { LinkDocPreview } from "./LinkDocPreview"; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { Transform } from "../../util/Transform"; import { StyleProp } from "../StyleProvider"; +import { computedFn } from "mobx-utils"; +import { DocumentManager } from "../../util/DocumentManager"; const path = require('path'); export const timeSchema = createSchema({ @@ -42,6 +44,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD static Instance: VideoBox; static RangeScript: ScriptField; static LabelScript: ScriptField; + static RangePlayScript: ScriptField; + static LabelPlayScript: ScriptField; + static heightPercent = 20; // height of timeline in percent of height of videoBox. private _reactionDisposer?: IReactionDisposer; private _youtubeReactionDisposer?: IReactionDisposer; // private _reactionDisposer?: IReactionDisposer; @@ -57,11 +62,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD _audioRef = React.createRef<HTMLDivElement>(); _markerStart: number = 0; _left: boolean = false; - _first: boolean = false; _count: Array<any> = []; _duration = 0; _start: boolean = true; private _currMarker: any; + @observable static SelectingRegion: VideoBox | undefined = undefined; @observable _visible: boolean = false; @observable _markerEnd: number = 0; @observable _forceCreateYouTubeIFrame = false; @@ -70,6 +75,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @observable _playing = false; @observable static _showControls: boolean; @computed get videoDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } + @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } public get player(): HTMLVideoElement | null { @@ -81,8 +87,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance = this; // onClick play scripts - VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!; - VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!; + VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; } videoLoad = () => { @@ -253,10 +261,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.layoutDoc._height = (this.layoutDoc._width || 0) / youtubeaspect; } } - - if (!this.dataDoc.markerAmount) { - this.dataDoc.markerAmount = 0; - } } componentWillUnmount() { @@ -317,23 +321,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } } + // returns the video and timeline @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; + const h = this.layoutDoc._showTimeline ? `${100 - VideoBox.heightPercent}%` : "100%"; return !field ? <div>Loading</div> : - <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: h, 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> + {this.uIButtons} + </div> + {this.renderTimeline} </div>; } @@ -392,7 +402,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, - <div className="timeline-button" key="timeline-button" onPointerDown={this.toggleTimeline}> + <div className="timeline-button" key="timeline-button" onPointerDown={this.toggleTimeline} style={{ + position: "absolute", + bottom: "41px", + right: this.layoutDoc._showTimeline ? "235px" : "155px", + color: "lightgrey", + width: "20px" + }}> <FontAwesomeIcon icon={this.layoutDoc._showTimeline ? "eye-slash" : "eye"} style={{ width: "100%" }} /> </div>, VideoBox._showControls ? (null) : [ @@ -462,7 +478,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return this.addDocument(doc); } - // play back the audio from time + // play back the video from time @action playFrom = (seekTimeInSeconds: number, endTime: number = this.videoDuration) => { clearTimeout(this._play); @@ -497,29 +513,39 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._timeline = timeline; } - // starting the drag event for marker resizing + // starting the drag event creating a range marker @action onPointerDownTimeline = (e: React.PointerEvent): void => { - const rect = (e.target as any).getBoundingClientRect(); - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.videoDuration; - this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); - setupMoveUpEvents(this, e, - action((e: PointerEvent) => { - this._visible = true; - this._markerEnd = toTimeline(e.clientX - rect.x); - if (this._markerEnd < this._markerStart) { - const tmp = this._markerStart; - this._markerStart = this._markerEnd; - this._markerEnd = tmp; + const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); + if (rect && e.target !== this._audioRef.current && this.active()) { + const wasPaused = !this._playing; + this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; + wasPaused && this.Pause(); + + const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.videoDuration; + this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); + VideoBox.SelectingRegion = this; + setupMoveUpEvents(this, e, + action(e => { + this._markerEnd = toTimeline(e.clientX - rect.x); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); + if (this._markerEnd < this._markerStart) { + const tmp = this._markerStart; + this._markerStart = this._markerEnd; + this._markerEnd = tmp; + } + VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + VideoBox.SelectingRegion = undefined; + }), + e => { + this.props.select(false); + e.shiftKey && this.createMarker(this.player!.currentTime); } - return false; - }), - action((e: PointerEvent, movement: number[]) => { - (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); - this._visible = false; - }), - (e: PointerEvent) => e.shiftKey && this.createMarker(this.player!.currentTime) - ); + , this.props.isSelected(true) || this._isChildActive); + } } @action @@ -537,6 +563,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } } + // play back the video from time + @action + playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); + this.playFrom(seekTimeInSeconds, endTime); + } + + // play back the video from time + @action + clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); + else { + DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); + this.player && (this.player.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + } + } + // starting the drag event for marker resizing onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { this._currMarker = m; @@ -557,6 +600,98 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD emptyFunction); } + // 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(); + const x1 = m.audioStart; + const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.videoDuration : 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; + } + + // renders the markers as a document + renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + 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} + 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: VideoBox, 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)} /> + </>} + </>; + }); + + // returns the timeline + @computed get renderTimeline() { + const rect = this._timeline?.getBoundingClientRect(); + const timelineContentWidth = this.props.PanelWidth(); + //const timelineContentWidth = this.layoutDoc._showTimeline ? this.props.PanelWidth() * 1.25 : this.props.PanelWidth(); + //const timelineContentWidth = rect ? rect.width : this.props.PanelWidth(); + const timelineContentHeight = (this.props.PanelHeight() * VideoBox.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) + 2; + return !this.layoutDoc._showTimeline ? (null) : + <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ height: `${VideoBox.heightPercent}%` }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> + {drawMarkers.map((d, i) => { + const m = d.marker; + const left = NumCast(m.audioStart) / this.videoDuration; + const l = `${NumCast(m.audioStart) / this.videoDuration * 100}%`; + const top = d.level / maxLevel * timelineContentHeight; + const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.videoDuration : 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: l, top, width: `${timespan / this.videoDuration * 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, + top, + timelineContentWidth * timespan / this.videoDuration, + 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.videoDuration * 100}%`, pointerEvents: "none" }} /> + </div>; + } + // updates the marker with the new time @action changeMarker = (m: any, time: any) => { @@ -569,48 +704,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; } - // instantiates a new array of size 500 for marker layout - markers = () => { - const increment = this.videoDuration / 500; - this._count = []; - for (let i = 0; i < 500; i++) { - this._count.push([increment * i, 0]); - } - } - - // makes sure no markers overlaps each other by setting the correct position and width - isOverlap = (m: any) => { - if (this._first) { - this._first = false; - this.markers(); - } - let max = 0; - - for (let i = 0; i < 500; i++) { - if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { - this._count[i][1]++; - - if (this._count[i][1] > max) { - max = this._count[i][1]; - } - } - } - - for (let i = 0; i < 500; i++) { - if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) { - this._count[i][1] = max; - } - } - - if (this.dataDoc.markerAmount < max) { - this.dataDoc.markerAmount = max; - } - return max - 1; - } - + // returns the blue container when dragging @computed get selectionContainer() { - return <div className="audiobox-container" style={{ - left: `${NumCast(this._markerStart) / this.videoDuration * 100}%`, + return VideoBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.videoDuration * 100}%`, width: `${Math.abs(this._markerStart - this._markerEnd) / this.videoDuration * 100}%`, height: "100%", top: "0%" }} />; } @@ -619,49 +716,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance.keyEvents(e); } + // for creating key markers with key events @action keyEvents = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement) return; if (!this._playing) return; // can't create if video is not playing switch (e.key) { - case "x": + case "x": // currently set to x, but can be a different key const currTime = this.player!.currentTime; if (this._start) { this._markerStart = this.player!.currentTime; this._start = false; - console.log("begin"); this._visible = true; } else { this.createMarker(this._markerStart, currTime); this._start = true; - console.log("end"); this._visible = false; } } } - rangeScript = () => VideoBox.RangeScript; - labelScript = () => VideoBox.LabelScript; + rangeClickScript = () => VideoBox.RangeScript; + labelClickScript = () => VideoBox.LabelScript; + rangePlayScript = () => VideoBox.RangePlayScript; + labelPlayScript = () => VideoBox.LabelPlayScript; screenToLocalTransform = () => this.props.ScreenToLocalTransform(); contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; render() { - const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : ""; - this._first = true; // for indicating the first marker that is rendered - const markerDoc = (mark: Doc, script: undefined | (() => ScriptField)) => { - return <DocumentView {...this.props} - Document={mark} - pointerEvents={"all"} - rootSelected={returnFalse} - LayoutTemplate={undefined} - ContainingCollectionDoc={this.props.Document} - removeDocument={this.removeDocument} - parentActive={returnTrue} - onClick={this.layoutDoc.playOnClick ? script : undefined} - ignoreAutoHeight={false} - bringToFront={emptyFunction} - scriptContext={this} />; - }; 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; return (<div className="videoBox" onContextMenu={this.specificContextMenu} @@ -672,7 +754,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD borderRadius }} > <div className="videoBox-viewer" > - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} forceScaling={true} fieldKey={this.annotationKey} isAnnotationOverlay={true} @@ -689,47 +771,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.contentFunc} </CollectionFreeFormView> </div> - {this.uIButtons} - {!this.layoutDoc._showTimeline ? (null) : - <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => { - if (e.button === 0 && !e.ctrlKey) { - const rect = (e.target as any).getBoundingClientRect(); - - if (e.target !== this._audioRef.current) { - const wasPaused = !this._playing; - this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; - wasPaused && this.Pause(); - } - this.onPointerDownTimeline(e); - } - }}> - {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => - (!m.isLabel) ? - (this.layoutDoc.hideMarkers) ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container1`} key={i} - title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`} - style={{ - left: `${NumCast(m.audioStart) / this.videoDuration * 100}%`, - top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%`, - width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / this.videoDuration * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%` - }} - onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} > - <div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div> - {markerDoc(m, this.rangeScript)} - <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div> - </div> - : - (this.layoutDoc.hideLabels) ? (null) : - <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={i} - style={{ left: `${NumCast(m.audioStart) / this.videoDuration * 100}%` }}> - {markerDoc(m, this.labelScript)} - </div> - )} - {this._visible ? this.selectionContainer : null} - <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.videoDuration * 100}%`, pointerEvents: "none" }} /> - </div>} - </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 8e824a447..7fb14b0cc 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -689,7 +689,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum height: NumCast(this.scrollHeight, 50), pointerEvents: inactiveLayer ? "none" : undefined }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} CollectionView={undefined} fieldKey={this.annotationKey} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2b9910dfb..c129d0204 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -320,12 +320,11 @@ 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(l.anchor2_timecode); + this._linkTime = NumCast(la1.audioStart, NumCast(la2.audioStart)); audioState = la2.audioState; if (Doc.AreProtosEqual(la2, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; - this._linkTime = NumCast(l.anchor1_timecode); audioState = la1.audioState; } }); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 02f7ada96..8700a5739 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -713,7 +713,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? "hard-light" : undefined, transform: `scale(${this._zoomed})` }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} isAnnotationOverlay={true} fieldKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 13d4a6d5a..76f9b66eb 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -30,42 +30,29 @@ .searchBox-lozenge-user, .searchBox-lozenge-dashboard, .searchBox-lozenge { - background-color: #313131; - border-radius: 5px; height: 18px; padding: 4px; - box-shadow: lightgrey 0.15em 0.15em 0.1em; - margin: 2px; - margin-bottom: 4px; - border-top: dimgrey 1px solid; - border-left: dimgrey 1px solid; + margin-right: 5px; display: flex; + align-items: center; + border: grey 1px solid; .searchBox-logoff, .searchBox-dashboards { border-radius: 3px; background: olivedrab; color: white; - position: relative; display: none; - margin-left: 3px; - padding-left: 2px; - padding-right: 2px; - padding-bottom: 11px; - cursor: default; + margin-left: 5px; + padding: 1px 2px 1px 2px; + cursor: pointer; } .searchBox-logoff { background: red; } .searchBox-dashSelect{ - background-color: black; - color: white; - font-size: 9; - margin-right: 6; - border-radius: 5px; - position: relative; - height: 15px; - transform: translate(0,-3px); + border: none; + background-color: transparent; &:hover { cursor: pointer; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index d3fee85a7..d10afdcf9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -466,7 +466,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } @computed get scopeButtons() { - return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px", border: "1px solid gray", borderRadius: "0.3em", borderBottom: !this.open ? "1px solid" : "none", }}> + return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px"}}> <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> <div style={{ display: "contents" }}> <div className="radio" style={{ margin: 0 }}> diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index c4a6433c8..c7e2d02cb 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -512,7 +512,7 @@ export class MobileInterface extends React.Component { return <div className="docButton" title={Doc.isDocPinned(this._activeDoc) ? "Unpin from presentation" : "Pin to presentation"} style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} - onClick={e => TabDocView.PinDoc(this._activeDoc, isPinned)}> + onClick={e => TabDocView.PinDoc(this._activeDoc, { unpin: isPinned })}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div>; } else return (null); |