diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 20 | ||||
-rw-r--r-- | src/client/views/collections/collectionLinear/CollectionLinearView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 7 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.scss | 90 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 172 |
5 files changed, 231 insertions, 64 deletions
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 25f2e0b25..dca5089f4 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -4,7 +4,7 @@ import { computed, IReactionDisposer, observable, - reaction, + reaction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; @@ -19,35 +19,29 @@ import { formatTime, OmitKeys, returnFalse, - returnOne, - setupMoveUpEvents, - StopEvent, - returnTrue, - smoothScroll, - smoothScrollHorizontal, + returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from "../../../Utils"; import { Docs } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; import { LinkManager } from "../../util/LinkManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; +import { Colors } from "../global/globalEnums"; import { LightboxView } from "../LightboxView"; import { DocAfterFocusFunc, DocFocusFunc, DocumentView, - DocumentViewProps, + DocumentViewProps } from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; -import { Colors } from "../global/globalEnums"; -import { DocumentManager } from "../../util/DocumentManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { DragManager } from "../../util/DragManager"; -import { faBreadSlice } from "@fortawesome/free-solid-svg-icons"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 4198a7979..77eed8bfc 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -3,13 +3,14 @@ import { Tooltip } from '@material-ui/core'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, Opt, WidthSym, DataSym } from '../../../../fields/Doc'; +import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { makeInterface } from '../../../../fields/Schema'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnTrue, returnFalse, OmitKeys, Utils } from '../../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils'; import { DocUtils } from '../../../documents/Documents'; +import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { Colors, Shadows } from '../../global/globalEnums'; @@ -20,7 +21,6 @@ import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { CollectionViewType } from '../CollectionView'; -import { DocumentManager } from "../../../util/DocumentManager"; import "./CollectionLinearView.scss"; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c33a74325..b9046b61e 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -421,6 +421,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action setVolume = (volume: number) => { if (this._ele) { + if (this._muted) { + this._muted = false; + } this._volume = volume; this._ele.volume = volume; } @@ -503,10 +506,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audiobox-button" title={this._muted ? "unmute" : "mute"} onPointerDown={this.toggleMute}> - <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} /> + <FontAwesomeIcon icon={this._muted || this._volume == 0 ? "volume-mute" : "volume-up"} /> </div> <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} - className="toolbar-slider" id="volume-slider" + className="toolbar-slider volume" onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.setVolume(Number(e.target.value)) }} /> diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 8a3261c7d..35f5a7e65 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -78,10 +78,49 @@ .videoBox-ui { position: absolute; flex-direction: row; - right: 5px; - top: 5px; - display: none; - background-color: rgba($dark-gray, 0.6); + align-items: center; + justify-content: center; + display: flex; + visibility: none; + opacity: 0; + background-color: $dark-gray; + color: white; + border-radius: 100px; + left: 50%; + transform: translate(-50%, -150%); + transition: top 0.5s, opacity 0.2s, visibility 0s; + height: 5%; + width: 80%; + + .timecode-controls { + display: flex; + flex-direction: row; + margin: 0 5px; + + .timecode { + margin: 0 5px; + } + } + + .videobox-button { + margin: 5px; + cursor: pointer; + width: 30px; + height: 30px; + border-radius: 50%; + background: $dark-gray; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: $black; + } + + svg { + width: 20px; + } + } } .videoBox-time, .videoBox-snapshot, @@ -101,6 +140,47 @@ .videoBox:hover { .videoBox-ui { - display: flex; + visibility: visible; + opacity: 1; + z-index: 10000; } } + +video::-webkit-media-controls { + display: none !important; +} + +input[type="range"] { + -webkit-appearance: none; + background: none; + margin: 5px; +} + +input[type="range"]:focus { + outline: none; +} + +input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + cursor: pointer; + box-shadow: 0; + background: $light-gray; + border-radius: 3px; +} + +input[type="range"]::-webkit-slider-thumb { + box-shadow: 0; + border: 0; + height: 10px; + width: 10px; + border-radius: 10px; + background: $medium-blue; + cursor: pointer; + -webkit-appearance: none; + margin-top: -2px; +} + +.toolbar-slider.volume { + width: 50px; +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index d56363348..bd9423d74 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -59,6 +59,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; + private _contentRef: HTMLDivElement | null = null; private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; private _audioPlayer: HTMLAudioElement | null = null; @@ -77,6 +78,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _fullScreen = false; @observable _playing = false; @observable _finished: boolean = false; + @observable _volume: number = 1; + @observable _muted: boolean = false; @computed get links() { return DocListCast(this.dataDoc.links); } @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } @@ -168,8 +171,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @action public FullScreen = () => { - this._fullScreen = true; - this.player && this.player.requestFullscreen(); + if (document.fullscreenElement == this._contentRef) { + this._fullScreen = false; + this.player && this._contentRef && document.exitFullscreen(); + } + else { + this._fullScreen = true; + this.player && this._contentRef && this._contentRef.requestFullscreen(); + } try { this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); } catch (e) { @@ -269,13 +278,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (vref) { this._videoRef!.ontimeupdate = this.updateTimecode; // @ts-ignore - vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); + // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); this._disposers.reactionDisposer?.(); this._disposers.reactionDisposer = reaction(() => (this.layoutDoc._currentTimecode || 0), time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); } } + @action + setContentRef = (cref: HTMLDivElement | null) => { + this._contentRef = cref; + if (cref) { + cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement == cref)); + } + } + specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { @@ -292,10 +309,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" }); subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" }); subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" }); - subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" }); - subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" }); - subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" }); - subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" }); + // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" }); + // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" }); + // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" }); + // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" }); subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } @@ -309,7 +326,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 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={classname}> + <div className={classname} ref={this.setContentRef}> + {this.uIButtons} <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._nativeControls} @@ -489,6 +507,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp })); } + @action + setVolume = (volume: number) => { + if (this.player) { + this._volume = volume; + this.player.volume = volume; + } + } + + @action + toggleMute = (e: React.PointerEvent) => { + e.stopPropagation(); + console.log("click"); + if (this.player) { + console.log("audio exists"); + this._muted = !this._muted; + this.player.muted = this._muted; + } + } + playLink = (doc: Doc) => { const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0)); const endTime = this.timeline?.anchorEnd(doc); @@ -534,41 +571,95 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get uIButtons() { const curTime = (this.layoutDoc._currentTimecode || 0) - (this.timeline?.clipStart || 0); - const nonNativeControls = [ - <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} key="play" placement="bottom"> - <div className="videoBox-play" onPointerDown={this.onPlayDown} > - <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> - </div> - </Tooltip>, - <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" placement="bottom"> - <div className="videoBox-time" onPointerDown={this.onResetDown} > - <span>{formatTime(curTime)}</span> - <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span> - </div> - </Tooltip>, - <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} key="full" placement="bottom"> - <div className="videoBox-full" onPointerDown={this.FullScreen}> - <FontAwesomeIcon icon="expand" size="lg" /> - </div> - </Tooltip>]; - return <div className="videoBox-ui"> - {[...(VideoBox._nativeControls ? [] : nonNativeControls), - <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} key="snap" placement="bottom"> - <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} > - <FontAwesomeIcon icon="camera" size="lg" /> + // const nonNativeControls = [ + // <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} key="play" placement="bottom"> + // <div className="videoBox-play" onPointerDown={this.onPlayDown} > + // <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> + // </div> + // </Tooltip>, + // <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" placement="bottom"> + // <div className="videoBox-time" onPointerDown={this.onResetDown} > + // <span>{formatTime(curTime)}</span> + // <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span> + // </div> + // </Tooltip>, + // <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} key="full" placement="bottom"> + // <div className="videoBox-full" onPointerDown={this.FullScreen}> + // <FontAwesomeIcon icon="expand" size="lg" /> + // </div> + // </Tooltip>]; + // return <div className="videoBox-ui"> + // {[...(VideoBox._nativeControls ? [] : nonNativeControls), + // <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} key="snap" placement="bottom"> + // <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} > + // <FontAwesomeIcon icon="camera" size="lg" /> + // </div> + // </Tooltip>, + // <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} key="timeline" placement="bottom"> + // <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}> + // <FontAwesomeIcon icon="eye" size="lg" /> + // </div> + // </Tooltip>, + // <Tooltip title={<div className="dash-tooltip">{this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}</div>} key="trim" placement="bottom"> + // <div className="videoBox-timelineButton" onPointerDown={this.onClipPointerDown}> + // <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} size="lg" /> + // </div> + // </Tooltip>,]} + // </div>; + return <div className="videoBox-ui" style={{ top: `calc(100% - 10px)` }}> + <div className="videobox-button" + title={this._playing ? "play" : "pause"} + onPointerDown={this.onPlayDown}> + <FontAwesomeIcon icon={this._playing ? "pause" : "play"} /> + </div> + + {this.timeline && <div className="timecode-controls"> + <div className="timecode-current"> + {formatTime(curTime)} </div> - </Tooltip>, - <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} key="timeline" placement="bottom"> - <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}> - <FontAwesomeIcon icon="eye" size="lg" /> + + <div className="timeline-slider"> + <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime} + className="toolbar-slider time-progress" + onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.setPlayheadTime(Number(e.target.value)) }} + /> </div> - </Tooltip>, - <Tooltip title={<div className="dash-tooltip">{this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}</div>} key="trim" placement="bottom"> - <div className="videoBox-timelineButton" onPointerDown={this.onClipPointerDown}> - <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} size="lg" /> + + <div className="timecode-end"> + {formatTime(this.timeline.clipDuration)} </div> - </Tooltip>,]} - </div>; + </div>} + + <div className="videobox-button" + title={"full screen"} + onPointerDown={this.onFullDown}> + <FontAwesomeIcon icon="expand" /> + </div> + + {!this._fullScreen && <div className="videobox-button" + title={"show timeline"} + onPointerDown={this.onTimelineHdlDown}> + <FontAwesomeIcon icon="eye" /> + </div>} + + {!this._fullScreen && <div className="videobox-button" + title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"} + onPointerDown={this.onClipPointerDown}> + <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} /> + </div>} + + <div className="videobox-button" + title={this._muted ? "unmute" : "mute"} + onPointerDown={this.toggleMute}> + <FontAwesomeIcon icon={this._muted || this._volume == 0 ? "volume-mute" : "volume-up"} /> + </div> + <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} + className="toolbar-slider volume" + onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.setVolume(Number(e.target.value)) }} + /> + </div> } @computed get renderTimeline() { return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> @@ -653,7 +744,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp mainCont={this._mainCont.current} />} {this.renderTimeline} - {this.uIButtons} </div> </div >); } |