From e47643e882af47546bb3f9c5560d24c4429a4cf3 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Wed, 2 Dec 2020 19:47:59 +0100 Subject: video modal mostly working --- animism-align/frontend/app/types.js | 1 + .../app/views/viewer/modals/modals.video.css | 43 +++++ .../app/views/viewer/modals/modals.video.js | 177 ++++++++------------- .../frontend/app/views/viewer/nav/eflux.chrome.js | 37 +++-- .../frontend/app/views/viewer/nav/eflux.css | 1 + .../frontend/app/views/viewer/nav/nav.parent.js | 4 +- .../frontend/app/views/viewer/nav/viewer.icons.js | 10 +- .../components.fullscreen/fullscreen.video.js | 1 + .../player/components.inline/inline.video.js | 10 +- .../player/components.media/video.scrubber.js | 13 +- .../app/views/viewer/player/player.container.js | 2 + .../frontend/app/views/viewer/viewer.actions.js | 12 ++ .../frontend/app/views/viewer/viewer.container.js | 4 + .../frontend/app/views/viewer/viewer.reducer.js | 43 ++++- 14 files changed, 230 insertions(+), 128 deletions(-) create mode 100644 animism-align/frontend/app/views/viewer/modals/modals.video.css (limited to 'animism-align/frontend') diff --git a/animism-align/frontend/app/types.js b/animism-align/frontend/app/types.js index bbc76b2..e1015d2 100644 --- a/animism-align/frontend/app/types.js +++ b/animism-align/frontend/app/types.js @@ -31,6 +31,7 @@ export const viewer = with_type('viewer', [ 'set_current_section', 'reached_end_of_section', 'set_nav_style', 'set_media_title', 'set_value', 'open_vitrine_modal', 'close_vitrine_modal', 'set_vitrine_index', + 'open_video_modal', 'close_video_modal', 'reset_video_modal', 'open_growl', 'close_growl', 'open_footnote', ]) 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
+ } const style = { backgroundColor: color.backgroundColor, color: color.textColor, - transitionDuration, } const playerStyle = { opacity: ready ? 1.0 : 0.0 } - // // console.log(volume, playerVolume) return (
-
- +
+ +
+ +
- -
) } } 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 (
@@ -92,7 +114,7 @@ class EfluxChrome extends Component { onMouseEnter={() => this.handleMouseEnter('play')} onMouseLeave={() => this.handleMouseLeave('play')} > - +
- {'Close fullscreen player'} + {videoModalOpen ? 'Close video' : 'Close fullscreen player'}
this.handleMouseEnter('close')} onMouseLeave={() => this.handleMouseLeave('close')} - onClick={() => { - actions.viewer.toggleFullscreenVisible(false) - actions.audio.pause() - this.handleMouseLeave('close') - }} + onClick={this.handleClose} > {EfluxClose}
@@ -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 (
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 = ( ) -export const PlayButton = ({ playing }) => { +export const PlayButton = ({ playing, onClick }) => { return (
{ 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} /> 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) + } + }} >
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 (
- +
({ - 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 {
+
) } 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, -- cgit v1.2.3-70-g09d2