diff options
Diffstat (limited to 'animism-align/frontend/app/views')
13 files changed, 229 insertions, 128 deletions
diff --git a/animism-align/frontend/app/views/viewer/modals/modals.video.css b/animism-align/frontend/app/views/viewer/modals/modals.video.css new file mode 100644 index 0000000..090391b --- /dev/null +++ b/animism-align/frontend/app/views/viewer/modals/modals.video.css @@ -0,0 +1,43 @@ +.video-modal { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + transition: opacity 0.5s; + opacity: 0.0; + pointer-events: none; +} +.video-modal.open { + opacity: 1.0; + pointer-events: auto; +} +.video-modal .video { + width: 100%; + height: 100%; +} +.video-modal .video .videoPlayer { + pointer-events: none; + width: 100%; + height: calc(100vh) !important; + padding: 3rem 0 3rem 0; + transition: opacity 0.5s; +} +.video-modal .video .videoPlayer div { + width: 100%; + height: 100% !important; + padding: 0 !important; +} +.nav-open .video-modal .video-scrubber { + transform: translateZ(0) translateY(3rem); + opacity: 0; + pointer-events: none; +} + +.video-modal-open .nav-player { + opacity: 0; + pointer-events: none; +}
\ No newline at end of file diff --git a/animism-align/frontend/app/views/viewer/modals/modals.video.js b/animism-align/frontend/app/views/viewer/modals/modals.video.js index e94b4c4..b67fc4b 100644 --- a/animism-align/frontend/app/views/viewer/modals/modals.video.js +++ b/animism-align/frontend/app/views/viewer/modals/modals.video.js @@ -4,7 +4,7 @@ import VimeoPlayer from 'app/utils/vendor/vimeo' import actions from 'app/actions' import { timestampToSeconds } from 'app/utils' -import { VideoScrubber, VideoSubtitles } from '../components.media' +import { VideoScrubber, VideoSubtitles } from '../player/components.media' class ModalVideo extends Component { state = { @@ -28,6 +28,8 @@ class ModalVideo extends Component { volume: 1.0, volumeFadeTime: 1000, lastCue: -1, + // play state + playing: false, } constructor(props) { super(props) @@ -35,70 +37,24 @@ class ModalVideo extends Component { this.handlePause = this.handlePause.bind(this) this.handleTimeUpdate = this.handleTimeUpdate.bind(this) this.handleScrub = this.handleScrub.bind(this) + this.handleToggle = this.handleToggle.bind(this) this.handleEnd = this.handleEnd.bind(this) } - componentDidMount() { - const video_start_ts = timestampToSeconds(this.props.element.settings.video_start_ts) || 0.0 - // TODO: here you can add the current play time modulo ... - this.setState({ - seek: video_start_ts, - video_start_ts, - }) - } componentDidUpdate(prevProps) { - // if the play_ts jumped more than a second, seek - const { play_ts, element, currentSection, fadeOutDuration } = this.props - if (Math.abs(play_ts - prevProps.play_ts) > 1.0) { - // handle seek - const { duration, video_start_ts } = this.state - const seek = ((play_ts - element.start_ts) % duration) + video_start_ts - this.setState({ seek }) - } - // console.log(play_ts, currentSection.cues.length, fadeOutDuration, element.settings.unmuted, this.state.fadeOut) - // volume changes - only if unmuted and not already leaving. - if (element.settings.unmuted && !this.state.fadeOut) { - // if we just started leaving the element, fade out the audio - if (fadeOutDuration) { - // console.log("fade out audio", this.props.fadeOutDuration) - setTimeout(() => this.setState({ fadeOut: true }), 0) - } - // otherwise, check if we gotta set the volume based on the position - else if (currentSection.cues.length) { - this.checkCuePosition(play_ts, currentSection.cues) - } - } - } - checkCuePosition(play_ts, cues) { - // const { volume element.settings.unmuted ? volume : 0.0 - const { unmuted } = this.props.element.settings - let volume = unmuted ? 1.0 : 0.0 - let { volumeFadeTime, lastCue } = this.state - // console.log(cues) - cues.some((cue, i) => { - if (cue.start_ts < play_ts) { - lastCue = i - if (cue.settings.volume) { - volume = parseFloat(cue.settings.volume) - volumeFadeTime = timestampToSeconds(cue.settings.duration) * 1000 - // console.log(volume, volumeFadeTime) - return false - } - } - return true - }) - // console.log('last cue', lastCue, volume, volumeFadeTime) - if (lastCue !== this.state.lastCue) { - this.setState({ volume, volumeFadeTime, lastCue }) + if (prevProps.open && !this.props.open) { + setTimeout(() => { + actions.viewer.resetVideoModal() + }, 500) } } handlePlay() { - this.setState({ ready: true }) + this.setState({ ready: true, playing: true }) } handlePause() { - + this.setState({ playing: false }) } handleEnd() { - + this.setState({ playing: false }) } handleTimeUpdate(timing) { if (!this.state.scrubbing || ('scrubbing' in timing)) { @@ -107,80 +63,87 @@ class ModalVideo extends Component { } handleScrub(timing) { // console.log(timing) - this.setState(timing) + this.setState({ + ...timing, + seek: timing.seconds, + }) + } + handleToggle() { + console.log('handle toggle to', !this.state.playing) + this.setState({ playing: !this.state.playing }) } - render() { - const { element, media, transitionDuration, play_ts, playing, cc, playerVolume } = this.props - const { duration, seconds, ready, volume, volumeFadeTime, fadeOut, lastCue } = this.state - const { color } = element - const item = media.lookup[element.settings.media_id] + const { open, media: mediaItem, color, cc, playerVolume } = this.props + const { duration, seconds, ready, volume, volumeFadeTime, playing, fadeOut, lastCue } = this.state + if (!color || !mediaItem) { + return <div /> + } const style = { backgroundColor: color.backgroundColor, color: color.textColor, - transitionDuration, } const playerStyle = { opacity: ready ? 1.0 : 0.0 } - // // console.log(volume, playerVolume) return ( <div - className='fullscreen-element video' + className={open ? 'video-modal open' : 'video-modal'} style={style} > - <div - className='videoPlayer' - style={playerStyle} - > - <VimeoPlayer - video={item.url} - paused={!playing} - autoplay={true} - autopause={false} - muted={!element.settings.unmuted} - loop={!!element.settings.loop} - seek={this.state.seek} - responsive={true} - controls={false} - byline={false} - volume={element.settings.unmuted ? playerVolume : 0.0} - targetVolume={element.settings.unmuted ? volume * playerVolume : 0.0} - volumeFadeTime={volumeFadeTime} - fadeOut={fadeOut && this.props.fadeOutDuration} - onPlay={this.handlePlay} - onPause={this.handlePause} - onTimeUpdate={this.handleTimeUpdate} - onEnd={this.handleEnd} - lastCue={lastCue} + <div className='fullscreen-element video' onClick={this.handleToggle}> + <div + className='videoPlayer' + style={playerStyle} + > + <VimeoPlayer + video={mediaItem.url} + paused={!open || (ready && !playing)} + autoplay={true} + autopause={false} + muted={false} + loop={false} + seek={this.state.seek} + responsive={true} + controls={false} + byline={false} + volume={playerVolume} + targetVolume={playerVolume} + volumeFadeTime={volumeFadeTime} + fadeOut={0} + onPlay={this.handlePlay} + onPause={this.handlePause} + onTimeUpdate={this.handleTimeUpdate} + onEnd={this.handleEnd} + lastCue={lastCue} + /> + </div> + <VideoScrubber + play_ts={seconds} + start_ts={0} + video_start_ts={0} + playing={playing} + duration={duration} + timing={this.state} + mediaItem={mediaItem} + cc={cc} + onScrub={this.handleScrub} + onToggle={this.handleToggle} + scrubMasterAudio={false} + /> + <VideoSubtitles + cc={cc} + play_ts={seconds} + mediaItem={mediaItem} /> </div> - <VideoScrubber - play_ts={play_ts} - start_ts={element.start_ts} - video_start_ts={this.state.video_start_ts} - playing={playing} - duration={element.duration} - timing={this.state} - mediaItem={item} - cc={cc} - onScrub={this.handleScrub} - /> - <VideoSubtitles - cc={cc} - play_ts={seconds} - mediaItem={item} - /> </div> ) } } const mapStateToProps = state => ({ - viewer: state.viewer, - play_ts: state.audio.play_ts, - playing: state.audio.playing, + ...state.viewer.videoModal, playerVolume: state.audio.volume, cc: state.audio.cc, }) diff --git a/animism-align/frontend/app/views/viewer/nav/eflux.chrome.js b/animism-align/frontend/app/views/viewer/nav/eflux.chrome.js index d365c9c..00851be 100644 --- a/animism-align/frontend/app/views/viewer/nav/eflux.chrome.js +++ b/animism-align/frontend/app/views/viewer/nav/eflux.chrome.js @@ -24,12 +24,33 @@ class EfluxChrome extends Component { started: false, playMessage: PLAY_MESSAGES.not_started, } + constructor(props) { + super(props) + this.handleToggle = this.handleToggle.bind(this) + this.handleClose = this.handleClose.bind(this) + } handleMouseEnter(category) { this.setState({ [category]: true }) } handleMouseLeave(category) { this.setState({ [category]: false }) } + handleToggle() { + if (this.props.playing) { + actions.audio.pause() + } else { + actions.viewer.playFromClick() + } + } + handleClose() { + if (this.props.videoModalOpen) { + actions.viewer.closeVideoModal() + } else { + actions.viewer.toggleFullscreenVisible(false) + actions.audio.pause() + } + this.handleMouseLeave('close') + } componentDidUpdate(prevProps) { if (this.props.playing !== prevProps.playing) { if (!this.state.started) { @@ -61,7 +82,8 @@ class EfluxChrome extends Component { playing, transcriptOpen, isFullscreen, fullscreenVisible, isFullscreenSingleton, growlOpen, growlMessage, - atEndOfSection, currentSection + atEndOfSection, currentSection, + videoModalOpen } = this.props let className = "eflux-header" if (navStyle) className += ' ' + navStyle @@ -69,7 +91,7 @@ class EfluxChrome extends Component { if (transcriptOpen) className += ' transcript-open' if (isFullscreen) className += ' is-fullscreen' // console.log(isFullscreen, fullscreenVisible, isFullscreenSingleton) - const fullscreenWantsCloseButton = isFullscreen && fullscreenVisible && !isFullscreenSingleton + const fullscreenWantsCloseButton = (isFullscreen && fullscreenVisible && !isFullscreenSingleton) || videoModalOpen return ( <div className={className}> <div className="eflux-gradient" /> @@ -92,7 +114,7 @@ class EfluxChrome extends Component { onMouseEnter={() => this.handleMouseEnter('play')} onMouseLeave={() => this.handleMouseLeave('play')} > - <PlayButton playing={playing} /> + <PlayButton playing={playing} onClick={this.handleToggle} /> </span> <div className="transcript-icon" @@ -108,7 +130,7 @@ class EfluxChrome extends Component { ? "growl-tooltip tooltip-close hover" : "growl-tooltip tooltip-close" }> - {'Close fullscreen player'} + {videoModalOpen ? 'Close video' : 'Close fullscreen player'} </div> <div className={ this.state.eflux @@ -142,11 +164,7 @@ class EfluxChrome extends Component { className="fullscreen-close" onMouseEnter={() => this.handleMouseEnter('close')} onMouseLeave={() => this.handleMouseLeave('close')} - onClick={() => { - actions.viewer.toggleFullscreenVisible(false) - actions.audio.pause() - this.handleMouseLeave('close') - }} + onClick={this.handleClose} > {EfluxClose} </div> @@ -166,6 +184,7 @@ const mapStateToProps = state => ({ isFullscreen: state.viewer.isFullscreen, fullscreenVisible: state.viewer.fullscreenVisible, isFullscreenSingleton: state.viewer.isFullscreenSingleton, + videoModalOpen: state.viewer.videoModal.open, }) export default connect(mapStateToProps)(EfluxChrome) diff --git a/animism-align/frontend/app/views/viewer/nav/eflux.css b/animism-align/frontend/app/views/viewer/nav/eflux.css index 3c464ae..2670518 100644 --- a/animism-align/frontend/app/views/viewer/nav/eflux.css +++ b/animism-align/frontend/app/views/viewer/nav/eflux.css @@ -128,6 +128,7 @@ padding: 0.75rem; text-align: center; pointer-events: none; + user-select: none; } .growl-tooltip.tooltip-eflux { left: 1.8rem; diff --git a/animism-align/frontend/app/views/viewer/nav/nav.parent.js b/animism-align/frontend/app/views/viewer/nav/nav.parent.js index 33f2d58..0f829dd 100644 --- a/animism-align/frontend/app/views/viewer/nav/nav.parent.js +++ b/animism-align/frontend/app/views/viewer/nav/nav.parent.js @@ -74,7 +74,7 @@ class NavParent extends Component { e && e.stopPropagation() if (isMobile) { actions.viewer.toggleComponent('nav') - } else { + } else if (!this.props.videoModal.open) { // console.log('>> CLICK NAV') // actions.viewer.toggleComponent('nav') const percent = (e.pageX / this.navbarRef.current.offsetWidth) @@ -115,7 +115,7 @@ class NavParent extends Component { let containerClassName = "viewer-nav " + viewer.navStyle let navClassName = 'nav-row main-nav' if (this.state.hoveringNav || !started) containerClassName += ' hovering-nav' - if ((this.state.hoveringNext || (viewer.atEndOfSection && !playing)) && !viewer.nav && viewer.nextSection) containerClassName += ' hovering-next' + if ((this.state.hoveringNext || (viewer.atEndOfSection && !playing && !viewer.videoModal.open)) && !viewer.nav && viewer.nextSection) containerClassName += ' hovering-next' return ( <div ref={this.navbarRef} className={containerClassName} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}> <div className={navClassName} onClick={this.handleNavBarClick}> diff --git a/animism-align/frontend/app/views/viewer/nav/viewer.icons.js b/animism-align/frontend/app/views/viewer/nav/viewer.icons.js index 31910b7..95be8b6 100644 --- a/animism-align/frontend/app/views/viewer/nav/viewer.icons.js +++ b/animism-align/frontend/app/views/viewer/nav/viewer.icons.js @@ -67,13 +67,19 @@ export const PauseIcon = ( </svg> ) -export const PlayButton = ({ playing }) => { +export const PlayButton = ({ playing, onClick }) => { return ( <div className={playing ? 'playToggle playing' : 'playToggle paused'} onClick={e => { e && e.stopPropagation() - playing ? actions.audio.pause() : actions.viewer.playFromClick() + if (onClick) { + onClick(playing) + } else if (playing) { + actions.audio.pause() + } else { + actions.viewer.playFromClick() + } }} > {playing ? PauseIcon : PlayIcon} diff --git a/animism-align/frontend/app/views/viewer/player/components.fullscreen/fullscreen.video.js b/animism-align/frontend/app/views/viewer/player/components.fullscreen/fullscreen.video.js index adbc443..cd40746 100644 --- a/animism-align/frontend/app/views/viewer/player/components.fullscreen/fullscreen.video.js +++ b/animism-align/frontend/app/views/viewer/player/components.fullscreen/fullscreen.video.js @@ -166,6 +166,7 @@ class FullscreenVideo extends Component { mediaItem={item} cc={cc} onScrub={this.handleScrub} + scrubMasterAudio={true} /> <VideoSubtitles cc={cc} diff --git a/animism-align/frontend/app/views/viewer/player/components.inline/inline.video.js b/animism-align/frontend/app/views/viewer/player/components.inline/inline.video.js index ea73d03..00f2de4 100644 --- a/animism-align/frontend/app/views/viewer/player/components.inline/inline.video.js +++ b/animism-align/frontend/app/views/viewer/player/components.inline/inline.video.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import VimeoPlayer from 'app/utils/vendor/vimeo' +import actions from 'app/actions' import { CURTAIN_COLOR_LOOKUP } from 'app/constants' import { SpeakerIcon } from '../../nav/viewer.icons' import { MediaCitation } from '../components.media' @@ -61,7 +62,14 @@ export class MediaVideo extends Component { <div className='videoPoster' style={style} - onClick={e => onAnnotationClick(e, paragraph, annotation)} + onClick={e => { + if (annotation.settings.can_play_full_video) { + e && e.stopPropagation() + actions.viewer.openVideoModal(item, color) + } else { + onAnnotationClick(e, paragraph, annotation) + } + }} > <div className='posterImage'> <img src={posterURL(item)} /> diff --git a/animism-align/frontend/app/views/viewer/player/components.media/video.scrubber.js b/animism-align/frontend/app/views/viewer/player/components.media/video.scrubber.js index e43c7ec..10677dc 100644 --- a/animism-align/frontend/app/views/viewer/player/components.media/video.scrubber.js +++ b/animism-align/frontend/app/views/viewer/player/components.media/video.scrubber.js @@ -47,15 +47,17 @@ class VideoScrubber extends Component { this.defer = false } scrub(x, scrubbing) { - const { timing, start_ts, video_start_ts, onScrub, duration } = this.props + const { timing, start_ts, video_start_ts, onScrub, onToggle, duration, scrubMasterAudio } = this.props const bounds = this.scrubberRef.current.getBoundingClientRect() // get percent offset from the scrubber const percent = clamp((x - bounds.left) / bounds.width, 0, 1) // this offset in seconds based on the length of the fullscreen element const seconds = percent * duration // we can use this to seek the audio - actions.audio.seek(start_ts + seconds) - actions.audio.play() + if (scrubMasterAudio) { + actions.audio.seek(start_ts + seconds) + actions.audio.play() + } // apply the video start offset. // in case the video loops, modulo the length of the original video const video_seek = ((seconds + video_start_ts) % timing.duration) @@ -104,7 +106,7 @@ class VideoScrubber extends Component { clearTimeout(this.hideTimeout) } render() { - const { playing, start_ts, play_ts, volume, timing, video_start_ts, duration, cc, mediaItem } = this.props + const { playing, start_ts, play_ts, volume, timing, video_start_ts, duration, cc, mediaItem, onToggle } = this.props const { hovering, showing } = this.state // remove video start offset from timing // console.log(start_ts, play_ts) @@ -119,7 +121,7 @@ class VideoScrubber extends Component { return ( <div className={className}> <div className='start-controls'> - <PlayButton playing={playing} /> + <PlayButton playing={playing} onClick={onToggle} /> </div> <div className='scrub-bar-container' @@ -153,7 +155,6 @@ class VideoScrubber extends Component { } const mapStateToProps = state => ({ - playing: state.audio.playing, volume: state.audio.volume, cc: state.audio.cc, }) diff --git a/animism-align/frontend/app/views/viewer/player/player.container.js b/animism-align/frontend/app/views/viewer/player/player.container.js index 77de555..39a08dc 100644 --- a/animism-align/frontend/app/views/viewer/player/player.container.js +++ b/animism-align/frontend/app/views/viewer/player/player.container.js @@ -7,6 +7,7 @@ import { preloadSectionImages } from 'app/utils/image.utils' import PlayerTranscript from './player.transcript' import PlayerFullscreen from './player.fullscreen' +import VideoModal from '../modals/modals.video' class PlayerContainer extends Component { constructor(props) { @@ -110,6 +111,7 @@ class PlayerContainer extends Component { <div className='viewer-container'> <PlayerTranscript /> <PlayerFullscreen /> + <VideoModal /> </div> ) } diff --git a/animism-align/frontend/app/views/viewer/viewer.actions.js b/animism-align/frontend/app/views/viewer/viewer.actions.js index c8b43b6..39aa9f8 100644 --- a/animism-align/frontend/app/views/viewer/viewer.actions.js +++ b/animism-align/frontend/app/views/viewer/viewer.actions.js @@ -455,6 +455,18 @@ export const vitrineGo = direction => dispatch => { } } +/* video modal */ + +export const openVideoModal = (media, color) => dispatch => { + dispatch({ type: types.viewer.open_video_modal, media, color }) +} +export const closeVideoModal = () => dispatch => { + dispatch({ type: types.viewer.close_video_modal }) +} +export const resetVideoModal = () => dispatch => { + dispatch({ type: types.viewer.reset_video_modal }) +} + /* growl */ export const openGrowl = message => dispatch => { diff --git a/animism-align/frontend/app/views/viewer/viewer.container.js b/animism-align/frontend/app/views/viewer/viewer.container.js index 5bc95f0..3d18745 100644 --- a/animism-align/frontend/app/views/viewer/viewer.container.js +++ b/animism-align/frontend/app/views/viewer/viewer.container.js @@ -15,6 +15,7 @@ import './checklist/checklist.css' import './checklist/credits.css' import './modals/modals.vitrine.css' import './modals/modals.handheld.css' +import './modals/modals.video.css' import './forms/forms.css' import './player/player.container.css' import './player/player.fullscreen.css' @@ -52,6 +53,9 @@ class ViewerContainer extends Component { if (viewer.atEndOfSection) { className += ' section-end' } + if (viewer.videoModal.open) { + className += ' video-modal-open' + } if (viewer.checklist || viewer.credits) { className += ' checklist-open' } else { diff --git a/animism-align/frontend/app/views/viewer/viewer.reducer.js b/animism-align/frontend/app/views/viewer/viewer.reducer.js index 0fcbfa2..be4e7bb 100644 --- a/animism-align/frontend/app/views/viewer/viewer.reducer.js +++ b/animism-align/frontend/app/views/viewer/viewer.reducer.js @@ -45,11 +45,19 @@ const initialState = { growlMessage: GROWL.OPENING_MESSAGE, growlTranscriptOpen: false, - /* vitrine */ + /* vitrine modal */ vitrineModal: { open: false, media: null, index: null, + color: null, + }, + + /* video modal */ + videoModal: { + open: false, + media: null, + color: null, }, options: {}, @@ -112,6 +120,10 @@ export default function viewerReducer(state = initialState, action) { navStyle: action.currentSection.color, fullscreenVisible: true, atEndOfSection: false, + videoModal: { + ...state.videoModal, + open: false, + } } case types.viewer.reached_end_of_section: @@ -167,6 +179,35 @@ export default function viewerReducer(state = initialState, action) { } } + case types.viewer.open_video_modal: + return { + ...state, + videoModal: { + open: true, + media: action.media, + color: action.color, + } + } + + case types.viewer.close_video_modal: + return { + ...state, + videoModal: { + ...state.videoModal, + open: false, + } + } + + case types.viewer.reset_video_modal: + return { + ...state, + videoModal: { + open: false, + media: null, + color: null, + } + } + case types.viewer.open_footnote: return { ...state, |
