diff options
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.scss | 84 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 86 |
3 files changed, 109 insertions, 63 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 9c9b19122..f0011d65f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -190,7 +190,7 @@ export class MainView extends React.Component { fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll, fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines, - fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt, fa.faSearchPlus, fa.faVolumeUp, fa.faVolumeDown, fa.faSquareRootAlt]); + fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt, fa.faSearchPlus, fa.faVolumeUp, fa.faVolumeDown, fa.faSquareRootAlt, fa.faVolumeMute]); this.initAuthenticationRouters(); } diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index d4cddd65e..c7108d290 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -80,55 +80,51 @@ // pointer-events: all; // } +.videoBox-ui-wrapper { + width: 0; + height: 0; +} + .videoBox-ui { position: absolute; flex-direction: row; align-items: center; justify-content: center; display: flex; - width: 100%; - visibility: none; - opacity: 0; background-color: $dark-gray; color: white; border-radius: 100px; - transform-origin: bottom left; - left: 0; - bottom: 0; - - transition: top 0.5s, width 0.5s, opacity 0.2s, visibility 0s; - height: 24px; - padding: 0 20px; + z-index: 2001; + min-width: 300px; + + transition: top 0.5s, width 0.5s, opacity 0.2s, visibility 0.2s; + height: 50px; + padding: 0 10px 0 7px; .timecode-controls { display: flex; flex-direction: row; align-items: center; justify-content: center; - margin: 0 5px; + margin: 0 2px; flex-grow: 2; - font-size: 12px; - - .timecode { - margin: 0 5px; - } + font-size: 14px; .timeline-slider { - margin: 0 10px 0 10px; + margin: 5px; flex-grow: 2; } } - .toolbar-slider.volume, - .toolbar-slider.zoom { - width: 100px; + .toolbar-slider.volume, .toolbar-slider.zoom { + width: 50px; } .videobox-button { - margin: 5px; + margin: 2px; cursor: pointer; - width: 24px; - height: 24px; + width: 30px; + height: 30px; border-radius: 50%; background: $dark-gray; display: flex; @@ -140,8 +136,8 @@ } svg { - width: 18px; - height: 18px; + width: 17px; + height: 17px; } } } @@ -163,28 +159,17 @@ } } -.videoBox:hover { - .videoBox-ui { - visibility: visible; - opacity: 1; - z-index: 10000; - } -} - -.videoBox-content-fullScreen, -.videoBox-content-fullScreen-interactive { +.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive { display: flex; justify-content: center; - align-items: center; - - &:hover { - .videoBox-ui { - opacity: 0; - } - } + align-items: flex-end; - .videoBox-ui:hover { - opacity: 1; + .videoBox-ui { + left: 50%; + top: 90%; + transform: translate(-50%, -50%); + width: 80%; + transition: top 0s, width 0s, opacity 0.3s, visibility 0.3s; } } @@ -195,7 +180,6 @@ video::-webkit-media-controls { input[type="range"] { -webkit-appearance: none; background: none; - margin: 10px; } input[type="range"]:focus { @@ -204,19 +188,19 @@ input[type="range"]:focus { input[type="range"]::-webkit-slider-runnable-track { width: 100%; - height: 18px; + height: 10px; cursor: pointer; box-shadow: 0; background: $light-gray; - border-radius: 18px; + border-radius: 10px; } input[type="range"]::-webkit-slider-thumb { box-shadow: 0; border: 0; - height: 20px; - width: 20px; - border-radius: 20px; + height: 12px; + width: 12px; + border-radius: 10px; background: $medium-blue; cursor: pointer; -webkit-appearance: none; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 0017e5f5e..5fe1ad5ed 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from "mobx-react"; import { basename } from "path"; import * as rp from 'request-promise'; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { AudioField, ImageField, VideoField } from "../../../fields/URLField"; @@ -84,6 +84,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _playRegionTimer: any = null; // timeout for playback + private _controlsFadeTimer: any = null; // timeout for controls fade + + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search + @observable _stackedTimeline: any; // CollectionStackedTimeline ref @observable static _nativeControls: boolean; // default html controls @observable _marqueeing: number[] | undefined; // coords for marquee selection @@ -97,6 +101,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; + @observable _controlsTransform?: { X: number, Y: number }; + @observable _controlsVisible: boolean = true; @computed get links() { return DocListCast(this.dataDoc.links); } @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height @@ -214,7 +220,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp else { this._fullScreen = true; this.player && this._contentRef && this._contentRef.requestFullscreen(); - } try { this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); @@ -223,6 +228,35 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } + // fades out controls in fullscreen after mouse stops moving + @action controlsFade = (e: PointerEvent) => { + e.stopPropagation(); + this._controlsVisible = true; + clearTimeout(this._controlsFadeTimer); + this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000); + } + + + // drag controls around window in fulls screen + @action controlsDrag = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const eleStyle = getComputedStyle(e.target as Element); + this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) }; + + setupMoveUpEvents(e.target, + e, + action((e, down, delta) => { + if (this._controlsTransform) { + this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth)); + this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight)); + } + return false; + }), + emptyFunction, + emptyFunction) + } + // creates and links snapshot photo of current video frame @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { @@ -344,7 +378,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setContentRef = (cref: HTMLDivElement | null) => { this._contentRef = cref; if (cref) { - cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref)); + cref.onfullscreenchange = action((e) => { + this._fullScreen = (document.fullscreenElement === cref); + if (this._fullScreen) { + document.addEventListener('pointermove', this.controlsFade); + this._controlsVisible = true; + this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000) + console.log("added"); + } + else { + document.removeEventListener('pointermove', this.controlsFade); + console.log("removed"); + } + }); } } @@ -385,16 +431,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div key="loading">Loading</div> : - <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}> + <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}> <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}> - {this.uIButtons} + {this._fullScreen && <div className="videoBox-ui" onPointerDown={this.controlsDrag} + style={{ left: this._controlsTransform && this._controlsTransform.X, top: this._controlsTransform && this._controlsTransform.Y, visibility: this._controlsVisible ? 'visible' : 'hidden', opacity: this._controlsVisible ? 1 : 0 }}> + {this.UIButtons} + </div>} <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}} onCanPlay={this.videoLoad} controls={VideoBox._nativeControls} onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} - onClick={e => e.preventDefault()}> + onClick={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> Not supported. </video> @@ -697,12 +746,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // renders video controls - @computed get uIButtons() { + componentUI = (boundsLeft: number, boundsTop: number) => { + const bounds = this.props.docViewPath().lastElement().getBounds(); + const left = bounds?.left || 0; + const right = bounds?.right || 0; + const top = bounds?.top || 0; + const height = (bounds?.bottom || 0) - top; + const yPos = height * this.heightPercent / 100 - 60 + top; + const width = Math.max(right - left, 300); + const overflow = (width - right + left) / 2; + return this._fullScreen ? null : <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> + <div className="videoBox-ui" style={{ left: left - overflow, top: yPos, width: width }}> + {this.UIButtons} + </div> + </div> + } + + @computed get UIButtons() { const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); - const scaling = (this.props.scaling?.() || 1); - return <div className="videoBox-ui" style={{ - transform: `scale(${1 / scaling})`, width: `${100 * scaling}%`, bottom: 20 / scaling - }}> + return <> <div className="videobox-button" title={this._playing ? "play" : "pause"} onPointerDown={this.onPlayDown}> @@ -770,7 +832,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }} /> </>} - </div>; + </> } // renders CollectionStackedTimeline |