diff --git a/src/renderer/component/overlay/index.js b/src/renderer/component/overlay/index.js new file mode 100644 index 000000000..b35797aca --- /dev/null +++ b/src/renderer/component/overlay/index.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { selectShowOverlay } from 'redux/selectors/media'; +import Overlay from './view'; + +const select = state => ({ + showOverlay: selectShowOverlay(state), +}); + +export default connect( + select, + null +)(Overlay); diff --git a/src/renderer/component/overlay/view.jsx b/src/renderer/component/overlay/view.jsx new file mode 100644 index 000000000..477c0f24f --- /dev/null +++ b/src/renderer/component/overlay/view.jsx @@ -0,0 +1,15 @@ +// @flow +import React from 'react'; + +type Props = { + children: ?React.node, +}; + +class Overlay extends React.PureComponent { + render() { + const { children } = this.props; + return
{children}
; + } +} + +export default Overlay; diff --git a/src/renderer/component/video/index.js b/src/renderer/component/video/index.js index 066b7c951..153870f13 100644 --- a/src/renderer/component/video/index.js +++ b/src/renderer/component/video/index.js @@ -2,8 +2,8 @@ import { connect } from 'react-redux'; import * as settings from 'constants/settings'; import { doChangeVolume } from 'redux/actions/app'; import { selectVolume } from 'redux/selectors/app'; -import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; -import { doPlay, doPause, savePosition } from 'redux/actions/media'; +import { doPlayUri, doSetPlayingUri, doLoadVideo } from 'redux/actions/content'; +import { doPlay, doPause, savePosition, doHideOverlay, doShowOverlay } from 'redux/actions/media'; import { makeSelectMetadataForUri, makeSelectContentTypeForUri, @@ -15,7 +15,11 @@ import { selectSearchBarFocused, } from 'lbry-redux'; import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings'; -import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media'; +import { + selectMediaPaused, + makeSelectMediaPositionForUri, + selectShowOverlay, +} from 'redux/selectors/media'; import { selectPlayingUri } from 'redux/selectors/content'; import Video from './view'; @@ -34,14 +38,20 @@ const select = (state, props) => ({ mediaPosition: makeSelectMediaPositionForUri(props.uri)(state), autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state), searchBarFocused: selectSearchBarFocused(state), + showOverlay: selectShowOverlay(state), + hiddenControls: props.hiddenControls, + fromOverlay: props.fromOverlay, }); const perform = dispatch => ({ play: uri => dispatch(doPlayUri(uri)), + load: uri => dispatch(doLoadVideo(uri)), cancelPlay: () => dispatch(doSetPlayingUri(null)), changeVolume: volume => dispatch(doChangeVolume(volume)), doPlay: () => dispatch(doPlay()), doPause: () => dispatch(doPause()), + doShowOverlay: () => dispatch(doShowOverlay()), + doHideOverlay: () => dispatch(doHideOverlay()), savePosition: (claimId, position) => dispatch(savePosition(claimId, position)), }); diff --git a/src/renderer/component/video/internal/player.jsx b/src/renderer/component/video/internal/player.jsx index 5ec6fc965..129ab8023 100644 --- a/src/renderer/component/video/internal/player.jsx +++ b/src/renderer/component/video/internal/player.jsx @@ -22,14 +22,9 @@ class VideoPlayer extends React.PureComponent { this.toggleFullScreenVideo = this.toggleFullScreen.bind(this); } - componentWillReceiveProps(nextProps) { - const el = this.refs.media.children[0]; - if (!this.props.paused && nextProps.paused && !el.paused) el.pause(); - } - componentDidMount() { const container = this.media; - const { contentType, changeVolume, volume, position, claim } = this.props; + const { contentType, changeVolume, volume, position, claim, hiddenControls } = this.props; const loadedMetadata = () => { this.setState({ hasMetadata: true, startedPlaying: true }); @@ -48,6 +43,12 @@ class VideoPlayer extends React.PureComponent { } }; + // Hide overlay video when the video ends only if its overlayed + const ended = () => { + this.props.doPause(); + this.props.savePosition(claim.claim_id, 0); + }; + // use renderAudio override for mp3 if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) { this.renderAudio(container, null, false); @@ -55,7 +56,7 @@ class VideoPlayer extends React.PureComponent { player.append( this.file(), container, - { autoplay: true, controls: true }, + { autoplay: true, controls: !hiddenControls }, renderMediaCallback.bind(this) ); } @@ -79,16 +80,21 @@ class VideoPlayer extends React.PureComponent { }); mediaElement.volume = volume; mediaElement.addEventListener('dblclick', this.toggleFullScreenVideo); + mediaElement.addEventListener('ended', ended); } } componentWillReceiveProps(next) { const el = this.media.children[0]; - if (!this.props.paused && next.paused && !el.paused) el.pause(); + if (!this.props.paused && next.paused && !el.paused) { + el.pause(); + } else if (this.props.paused && !next.paused && el.paused) { + el.play(); + } } componentDidUpdate() { - const { contentType, downloadCompleted } = this.props; + const { contentType, downloadCompleted, hiddenControls } = this.props; const { startedPlaying } = this.state; if (this.playableType() && !startedPlaying && downloadCompleted) { @@ -99,7 +105,7 @@ class VideoPlayer extends React.PureComponent { } else { player.render(this.file(), container, { autoplay: true, - controls: true, + controls: !hiddenControls, }); } } @@ -111,7 +117,7 @@ class VideoPlayer extends React.PureComponent { if (mediaElement) { mediaElement.removeEventListener('click', this.togglePlayListener); } - this.props.doPause(); + // this.props.doPause(); } toggleFullScreen(event) { diff --git a/src/renderer/component/video/view.jsx b/src/renderer/component/video/view.jsx index b2448ba5f..8e8eaee88 100644 --- a/src/renderer/component/video/view.jsx +++ b/src/renderer/component/video/view.jsx @@ -6,11 +6,11 @@ import type { Claim } from 'types/claim'; import VideoPlayer from './internal/player'; import VideoPlayButton from './internal/play-button'; import LoadingScreen from './internal/loading-screen'; +import ReactDOM from 'react-dom'; const SPACE_BAR_KEYCODE = 32; type Props = { - cancelPlay: () => void, fileInfo: { outpoint: string, file_name: string, @@ -34,12 +34,19 @@ type Props = { doPlay: () => void, doPause: () => void, savePosition: (string, number) => void, + doShowOverlay: () => void, + doHideOverlay: () => void, mediaPaused: boolean, mediaPosition: ?number, className: ?string, obscureNsfw: boolean, play: string => void, searchBarFocused: boolean, + showOverlay: boolean, + hiddenControls: boolean, + fromOverlay: boolean, + overlayed: boolean, + fromOverlay: boolean, }; class Video extends React.PureComponent { @@ -53,6 +60,11 @@ class Video extends React.PureComponent { componentDidMount() { this.handleAutoplay(this.props); window.addEventListener('keydown', this.handleKeyDown); + + const { showOverlay, doHideOverlay, uri, playingUri } = this.props; + if (showOverlay && uri === playingUri) { + doHideOverlay(); + } } componentWillReceiveProps(nextProps: Props) { @@ -64,13 +76,53 @@ class Video extends React.PureComponent { ) { this.handleAutoplay(nextProps); } + if (nextProps.fromOverlay) { + this.moveVideoFromOverlayToNormal(); + this.destroyVideoOnOverlay(); + this.props.doHideOverlay(); + } } componentWillUnmount() { - this.props.cancelPlay(); + const { overlayed, doShowOverlay, mediaPaused } = this.props; + if (!overlayed && !mediaPaused) { + doShowOverlay(); + this.moveVideoToOverlay(); + } window.removeEventListener('keydown', this.handleKeyDown); } + moveVideoToOverlay() { + const topContainer = document.getElementById('video__overlay_id_top_container'); + const container = document.getElementById('video__overlay_id'); + const videoContainer = this.mediaContainer.media ? this.mediaContainer.media : document.getElementById('insert_video'); + const video = videoContainer.getElementsByTagName('video')[0]; + if (video) { + topContainer.classList.remove('hiddenContainer'); + container.appendChild(video); + video.controls = false; + video.play(); + } + } + + moveVideoFromOverlayToNormal() { + const videoContainer = document.getElementById('video__overlay_id'); + if (!videoContainer) return; + const video = videoContainer.getElementsByTagName('video')[0]; + if (!video) return; + const filePageVideoContainer = document.getElementById('insert_video'); + filePageVideoContainer.appendChild(video); + video.controls = true; + video.play(); + } + + destroyVideoOnOverlay() { + const topContainer = document.getElementById('video__overlay_id_top_container'); + const videoContainer = document.getElementById('video__overlay_id'); + topContainer.classList.add('hiddenContainer'); + videoContainer.innerHTML = ''; + } + handleKeyDown(event: SyntheticKeyboardEvent<*>) { const { searchBarFocused } = this.props; if (!searchBarFocused && event.keyCode === SPACE_BAR_KEYCODE) { @@ -100,8 +152,16 @@ class Video extends React.PureComponent { } playContent() { - const { play, uri } = this.props; - play(uri); + const { play, uri, playingUri, doHideOverlay } = this.props; + if (playingUri) { + if (playingUri === uri) { + this.moveVideoFromOverlayToNormal(); + } + this.destroyVideoOnOverlay(); + doHideOverlay(); + } else { + play(uri); + } } render() { @@ -123,6 +183,10 @@ class Video extends React.PureComponent { mediaPosition, className, obscureNsfw, + hiddenControls, + doHideOverlay, + showOverlay, + fromOverlay, } = this.props; const isPlaying = playingUri === uri; @@ -147,6 +211,7 @@ class Video extends React.PureComponent { const layoverStyle = !shouldObscureNsfw && poster ? { backgroundImage: `url("${poster}")` } : {}; + const commingFromOverlay = playingUri === uri; return (
{isPlaying && ( @@ -155,7 +220,7 @@ class Video extends React.PureComponent {
- ) : ( + ) : (commingFromOverlay && fromOverlay ?
this.mediaContainer = mediaContainer} /> : { uri={uri} paused={mediaPaused} position={mediaPosition} + hiddenControls={hiddenControls} + doHideOverlay={doHideOverlay} + ref={mediaContainer => this.mediaContainer = mediaContainer } /> )}
diff --git a/src/renderer/component/videoOverlay/index.js b/src/renderer/component/videoOverlay/index.js new file mode 100644 index 000000000..3e2c47d81 --- /dev/null +++ b/src/renderer/component/videoOverlay/index.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux'; +import { selectPlayingUri } from 'redux/selectors/content'; +import { doSetPlayingUri } from 'redux/actions/content'; +import { doNavigate } from 'redux/actions/navigation'; +import { doPlay, doPause, doHideOverlay } from 'redux/actions/media'; +import { selectMediaPaused, selectShowOverlay } from 'redux/selectors/media'; +import VideoOverlay from './view'; + +const select = state => ({ + playingUri: selectPlayingUri(state), + mediaPaused: selectMediaPaused(state), + showOverlay: selectShowOverlay(state), +}); + +const perform = dispatch => ({ + navigate: (path, params) => dispatch(doNavigate(path, params)), + doCancelPlay: () => dispatch(doSetPlayingUri(null)), + doHideOverlay: () => dispatch(doHideOverlay()), + doPlay: () => dispatch(doPlay()), + doPause: () => dispatch(doPause()), +}); + +export default connect( + select, + perform +)(VideoOverlay); diff --git a/src/renderer/component/videoOverlay/view.jsx b/src/renderer/component/videoOverlay/view.jsx new file mode 100644 index 000000000..13c6237db --- /dev/null +++ b/src/renderer/component/videoOverlay/view.jsx @@ -0,0 +1,92 @@ +// @flow +import React from 'react'; +import Video from 'component/video'; +import Overlay from 'component/overlay'; +import VideoOverlayHeader from 'component/videoOverlayHeader'; +import Button from 'component/button'; +import * as icons from 'constants/icons'; + +type Props = { + doCancelPlay: () => void, + doHideOverlay: () => void, + navigate: (string, ?{}) => void, + doPlay: () => void, + doPause: () => void, + playingUri: ?string, + mediaPaused: boolean, + showOverlay: boolean, +}; + +class VideoOverlay extends React.Component { + constructor() { + super(); + + (this: any).closeVideo = this.closeVideo.bind(this); + (this: any).returnToMedia = this.returnToMedia.bind(this); + } + + closeVideo() { + const { doCancelPlay, doHideOverlay } = this.props; + doCancelPlay(); + doHideOverlay(); + this.destroyMediaPlayer(); + } + + returnToMedia() { + const { navigate, playingUri, doHideOverlay } = this.props; + doHideOverlay(); + this.destroyMediaPlayer(false); + navigate('/show', { uri: playingUri, fromOverlay: true }); + } + + renderPlayOrPauseButton() { + const { mediaPaused, doPause, doPlay } = this.props; + if (mediaPaused) { + return
)} + + + ); + } +} + +export default VideoOverlay; diff --git a/src/renderer/component/videoOverlayHeader/index.js b/src/renderer/component/videoOverlayHeader/index.js new file mode 100644 index 000000000..c50ab5bcb --- /dev/null +++ b/src/renderer/component/videoOverlayHeader/index.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { makeSelectTitleForUri } from 'lbry-redux'; +import VideoOverlayHeader from './view'; + +const select = (state, props) => ({ + title: makeSelectTitleForUri(props.uri)(state), + onClose: props.onClose, +}); + +export default connect(select, null)(VideoOverlayHeader); diff --git a/src/renderer/component/videoOverlayHeader/view.jsx b/src/renderer/component/videoOverlayHeader/view.jsx new file mode 100644 index 000000000..d923c9d05 --- /dev/null +++ b/src/renderer/component/videoOverlayHeader/view.jsx @@ -0,0 +1,31 @@ +// @flow +import React from 'react'; +import Button from 'component/button'; +import * as icons from 'constants/icons'; +import TruncatedText from 'component/common/truncated-text'; + +type Props = { + onClose: () => void, + title: string, +}; + +class VideoOverlayHeader extends React.Component { + render() { + const { onClose, title } = this.props; + return ( +
+

+ {title} +

+
+ ); + } +} + +export default VideoOverlayHeader; diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 4a2d31e0d..ae44685d3 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -188,6 +188,9 @@ export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; export const MEDIA_PLAY = 'MEDIA_PLAY'; export const MEDIA_PAUSE = 'MEDIA_PAUSE'; export const MEDIA_POSITION = 'MEDIA_POSITION'; +// Overlay Media +export const SHOW_OVERLAY_MEDIA = 'SHOW_OVERLAY_MEDIA'; +export const HIDE_OVERLAY_MEDIA = 'HIDE_OVERLAY_MEDIA'; // Publishing export const CLEAR_PUBLISH = 'CLEAR_PUBLISH'; diff --git a/src/renderer/constants/icons.js b/src/renderer/constants/icons.js index 845039e9f..1c8e210e3 100644 --- a/src/renderer/constants/icons.js +++ b/src/renderer/constants/icons.js @@ -25,4 +25,7 @@ export const CHECK = 'CheckCircle'; export const HEART = 'Heart'; export const UNLOCK = 'Unlock'; export const CHECK_SIMPLE = 'Check'; +export const PLAY = 'Play'; +export const MAXIMIZE = 'Maximize2'; +export const PAUSE = 'Pause'; export const GLOBE = 'Globe'; diff --git a/src/renderer/index.js b/src/renderer/index.js index 974df0384..30479b63f 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -16,6 +16,7 @@ import 'scss/all.scss'; import store from 'store'; import app from './app'; import analytics from './analytics'; +import VideoOverlay from './component/videoOverlay/'; import doLogWarningConsoleMessage from './logWarningConsoleMessage'; const { autoUpdater } = remote.require('electron-updater'); @@ -148,6 +149,7 @@ const init = () => {
+
, document.getElementById('app') diff --git a/src/renderer/page/file/index.js b/src/renderer/page/file/index.js index 564ce0aed..c0044bb77 100644 --- a/src/renderer/page/file/index.js +++ b/src/renderer/page/file/index.js @@ -34,6 +34,7 @@ const select = (state, props) => ({ isPaused: selectMediaPaused(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state), + fromOverlay: Boolean(props.fromOverlay), }); const perform = dispatch => ({ diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index b47ad5879..be04a5521 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -45,6 +45,7 @@ type Props = { prepareEdit: ({}, string) => void, setClientSetting: (string, boolean | string) => void, checkSubscription: ({ channelName: string, uri: string }) => void, + fromOverlay: boolean, subscriptions: Array, }; @@ -107,6 +108,7 @@ class FilePage extends React.Component { prepareEdit, navigate, autoplay, + fromOverlay, costInfo, } = this.props; @@ -150,7 +152,7 @@ class FilePage extends React.Component { ) : (
- {isPlayable &&