import * as React from "react"; import "./Timeline.scss"; import { CollectionSubView } from "../collections/CollectionSubView"; import { Document, listSpec } from "../../../new_fields/Schema"; import { observer } from "mobx-react"; import { Track } from "./Track"; import { observable, reaction, action, IReactionDisposer, computed, runInAction } from "mobx"; import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlayCircle, faBackward, faForward, faGripLines, faArrowUp, faArrowDown, faClock, faPauseCircle } from "@fortawesome/free-solid-svg-icons"; import { ContextMenuProps } from "../ContextMenuItem"; import { ContextMenu } from "../ContextMenu"; import { TimelineOverview } from "./TimelineOverview"; export interface FlyoutProps { x?: number; y?: number; display?: string; regiondata?: Doc; regions?: List; } @observer export class Timeline extends CollectionSubView(Document) { private readonly DEFAULT_CONTAINER_HEIGHT: number = 300; private readonly DEFAULT_TICK_SPACING: number = 50; private readonly MIN_CONTAINER_HEIGHT: number = 205; private readonly MAX_CONTAINER_HEIGHT: number = 800; private readonly DEFAULT_TICK_INCREMENT: number = 1000; @observable private _isMinimized = false; @observable private _tickSpacing = this.DEFAULT_TICK_SPACING; @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT; @observable private _scrubberbox = React.createRef(); @observable private _scrubber = React.createRef(); @observable private _trackbox = React.createRef(); @observable private _titleContainer = React.createRef(); @observable private _timelineContainer = React.createRef(); @observable private _timelineWrapper = React.createRef(); @observable private _infoContainer = React.createRef(); @observable private _currentBarX: number = 0; @observable private _windSpeed: number = 1; @observable private _isPlaying: boolean = false; //scrubber playing @observable private _isFrozen: boolean = true; //timeline freeze @observable private _totalLength: number = 0; @observable private _visibleLength: number = 0; @observable private _visibleStart: number = 0; @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT; @observable private _time = 100000; //DEFAULT @observable private _ticks: number[] = []; @observable private _playButton = faPlayCircle; @computed private get children(): List { let extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type)); if (extendedDocument) { if (this.props.Document.data_ext) { return Cast((Cast(this.props.Document.data_ext, Doc) as Doc).annotations, listSpec(Doc)) as List; } else { return new List(); } } return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List; } componentWillMount() { this.props.Document.isAnimating ? this.props.Document.isAnimating = true : this.props.Document.isAnimating = false; console.log(this._currentBarX); } componentDidMount() { if (StrCast(this.props.Document.type) === "video") { console.log("ran"); console.log(this.props.Document.duration); if (this.props.Document.duration) { this._time = Math.round(NumCast(this.props.Document.duration)) * 1000; reaction(() => { return NumCast(this.props.Document.curPage); }, curPage => { this.changeCurrentBarX(curPage * this._tickIncrement / this._tickSpacing); }); } } runInAction(() => { reaction(() => { return this._time; }, () => { this._ticks = []; for (let i = 0; i < this._time;) { this._ticks.push(i); i += this._tickIncrement; } let trackbox = this._trackbox.current!; this._totalLength = this._tickSpacing * this._ticks.length; trackbox.style.width = `${this._totalLength}`; this._scrubberbox.current!.style.width = `${this._totalLength}`; }, {fireImmediately:true}); this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; this._visibleStart = this._infoContainer.current!.scrollLeft; }); } @action changeCurrentBarX = (pixel: number) => { pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel; } //for playing @action onPlay = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (this._isPlaying) { this._isPlaying = false; this._playButton = faPlayCircle; } else { this._isPlaying = true; this._playButton = faPauseCircle; const playTimeline = () => { if (this._isPlaying){ if (this._currentBarX >= this._totalLength) { this.changeCurrentBarX(0); } else { this.changeCurrentBarX(this._currentBarX + this._windSpeed); setTimeout(playTimeline, 15); } } }; playTimeline(); } } @action windForward = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (this._windSpeed < 64) { //max speed is 32 this._windSpeed = this._windSpeed * 2; } } @action windBackward = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (this._windSpeed > 1 / 16) { // min speed is 1/8 this._windSpeed = this._windSpeed / 2; } } //for scrubber action @action onScrubberDown = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); document.addEventListener("pointermove", this.onScrubberMove); document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onScrubberMove); }); } @action onScrubberMove = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); let scrubberbox = this._scrubberbox.current!; let left = scrubberbox.getBoundingClientRect().left; let offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale; this.changeCurrentBarX(offsetX); } @action onScrubberClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); let scrubberbox = this._scrubberbox.current!; let offsetX = (e.clientX - scrubberbox.getBoundingClientRect().left) * this.props.ScreenToLocalTransform().Scale; this.changeCurrentBarX(offsetX); } @action onPanDown = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); document.addEventListener("pointermove", this.onPanMove); document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onPanMove); }); } @action onPanMove = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); let trackbox = this._trackbox.current!; let titleContainer = this._titleContainer.current!; this.movePanX(this._visibleStart - e.movementX); trackbox.scrollTop = trackbox.scrollTop - e.movementY; titleContainer.scrollTop = titleContainer.scrollTop - e.movementY; } @action movePanX = (pixel:number) => { let infoContainer = this._infoContainer.current!; infoContainer.scrollLeft = pixel; this._visibleStart = infoContainer.scrollLeft; } @action onResizeDown = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); document.addEventListener("pointermove", this.onResizeMove); document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onResizeMove); }); } @action onResizeMove = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); let offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom; if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) { this._containerHeight = this.MIN_CONTAINER_HEIGHT; } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) { this._containerHeight = this.MAX_CONTAINER_HEIGHT; } else { this._containerHeight += offset; } } @action onTimelineDown = (e: React.PointerEvent) => { e.preventDefault(); if (e.nativeEvent.which === 1 && !this._isFrozen) { document.addEventListener("pointermove", this.onTimelineMove); document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onTimelineMove); }); } } @action onTimelineMove = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); let timelineContainer = this._timelineWrapper.current!; let left = parseFloat(timelineContainer.style.left!); let top = parseFloat(timelineContainer.style.top!); timelineContainer.style.left = `${left + e.movementX}px`; timelineContainer.style.top = `${top + e.movementY}px`; } @action minimize = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); let timelineContainer = this._timelineContainer.current!; if (this._isMinimized) { this._isMinimized = false; timelineContainer.style.visibility = "visible"; } else { this._isMinimized = true; timelineContainer.style.visibility = "hidden"; } } @action toReadTime = (time: number): string => { const inSeconds = time / 1000; let min: (string | number) = Math.floor(inSeconds / 60); let sec: (string | number) = inSeconds % 60; if (Math.floor(sec / 10) === 0) { sec = "0" + sec; } return `${min}:${sec}`; } convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time") => { let time = dir === "pixel" ? pos / this._tickSpacing * this._tickIncrement : pos * this._tickSpacing / this._tickIncrement; switch (unit) { case "mili": return time; case "sec": return dir === "pixel" ? time / 1000 : time * 1000; case "min": return dir === "pixel" ? time / 60000 : time * 60000; case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000; default: return time; } } timelineContextMenu = (e: React.MouseEvent): void => { let subitems: ContextMenuProps[] = []; let timelineContainer = this._timelineWrapper.current!; subitems.push({ description: "Pin to Top", event: action(() => { if (!this._isFrozen) { timelineContainer.style.left = "0px"; timelineContainer.style.top = "0px"; timelineContainer.style.transition = "none"; } }), icon: faArrowUp }); subitems.push({ description: this._isFrozen ? "Unfreeze Timeline" : "Freeze Timeline", event: action(() => { if (this._isFrozen) { this._isFrozen = false; } else { this._isFrozen = true; } }), icon: "thumbtack" }); ContextMenu.Instance.addItem({ description: "Timeline Funcs...", subitems: subitems, icon: faClock }); } private timelineToolBox = (scale:number) => { let size = 50 * scale; //50 is default return (
); } render() { return (
{this.timelineToolBox(0.5)}
{this._ticks.map(element => { return

{this.toReadTime(element)}

; })}
{DocListCast(this.children).map(doc => )}
{DocListCast(this.children).map(doc =>

{doc.title}

)}
{BoolCast(this.props.Document.isAnimating) ?
: this.timelineToolBox(1) }
); } }