diff --git a/ui/component/fileRenderFloating/index.js b/ui/component/fileRenderFloating/index.js index 3244330a8..4f2a2f37b 100644 --- a/ui/component/fileRenderFloating/index.js +++ b/ui/component/fileRenderFloating/index.js @@ -26,6 +26,7 @@ const select = (state, props) => { streamingUrl: makeSelectStreamingUrlForUri(uri)(state), floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state), renderMode: makeSelectFileRenderModeForUri(uri)(state), + videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), }; }; diff --git a/ui/component/fileRenderFloating/view.jsx b/ui/component/fileRenderFloating/view.jsx index ae6cb7c82..69cf81d8e 100644 --- a/ui/component/fileRenderFloating/view.jsx +++ b/ui/component/fileRenderFloating/view.jsx @@ -31,6 +31,7 @@ type Props = { renderMode: string, playingUri: ?PlayingUri, primaryUri: ?string, + videoTheaterMode: boolean, }; export default function FileRenderFloating(props: Props) { @@ -45,6 +46,7 @@ export default function FileRenderFloating(props: Props) { renderMode, playingUri, primaryUri, + videoTheaterMode, } = props; const { location: { pathname }, @@ -187,7 +189,7 @@ export default function FileRenderFloating(props: Props) { window.removeEventListener('resize', handleResize); onFullscreenChange(window, 'remove', handleResize); }; - }, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying]); + }, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying, videoTheaterMode]); useEffect(() => { // @if TARGET='app' @@ -248,6 +250,7 @@ export default function FileRenderFloating(props: Props) { className={classnames('content__viewer', { 'content__viewer--floating': isFloating, 'content__viewer--inline': !isFloating, + 'content__viewer--theater-mode': !isFloating && videoTheaterMode, })} style={ !isFloating && fileViewerRect diff --git a/ui/component/fileRenderInitiator/view.jsx b/ui/component/fileRenderInitiator/view.jsx index 3bdde5f15..25c288ccb 100644 --- a/ui/component/fileRenderInitiator/view.jsx +++ b/ui/component/fileRenderInitiator/view.jsx @@ -32,6 +32,7 @@ type Props = { claim: StreamClaim, claimWasPurchased: boolean, authenticated: boolean, + videoTheaterMode: boolean, }; export default function FileRenderInitiator(props: Props) { @@ -51,6 +52,7 @@ export default function FileRenderInitiator(props: Props) { costInfo, claimWasPurchased, authenticated, + videoTheaterMode, } = props; const cost = costInfo && costInfo.cost; const isFree = hasCostInfo && cost === 0; @@ -118,6 +120,7 @@ export default function FileRenderInitiator(props: Props) { style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}} className={classnames('content__cover', { 'content__cover--disabled': disabled, + 'content__cover--theater-mode': videoTheaterMode, 'card__media--nsfw': obscurePreview, })} > diff --git a/ui/component/page/index.js b/ui/component/page/index.js index dd41e0fa9..cccdf3123 100644 --- a/ui/component/page/index.js +++ b/ui/component/page/index.js @@ -1,4 +1,10 @@ import { connect } from 'react-redux'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { SETTINGS } from 'lbry-redux'; import Page from './view'; -export default connect()(Page); +const select = (state, props) => ({ + videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), +}); + +export default connect(select)(Page); diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index 914372f91..f3187c5cf 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -26,6 +26,7 @@ type Props = { noFooter: boolean, noSideNavigation: boolean, fullWidthPage: boolean, + videoTheaterMode: boolean, backout: { backLabel?: string, backNavDefault?: string, @@ -45,7 +46,9 @@ function Page(props: Props) { noFooter = false, noSideNavigation = false, backout, + videoTheaterMode, } = props; + const { location: { pathname }, } = useHistory(); @@ -82,7 +85,12 @@ function Page(props: Props) { setSidebarOpen={setSidebarOpen} /> )} -
+
{!authPage && !noSideNavigation && ( {children} diff --git a/ui/component/recommendedContent/view.jsx b/ui/component/recommendedContent/view.jsx index 173570a36..6a15d561e 100644 --- a/ui/component/recommendedContent/view.jsx +++ b/ui/component/recommendedContent/view.jsx @@ -55,7 +55,6 @@ export default function RecommendedContent(props: Props) { title={__('Related')} body={ { const { search } = props.location; @@ -35,6 +36,7 @@ const perform = dispatch => ({ doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)), doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)), claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()), + toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()), }); export default withRouter(connect(select, perform)(VideoViewer)); diff --git a/ui/component/viewers/videoViewer/internal/theater-mode.js b/ui/component/viewers/videoViewer/internal/theater-mode.js new file mode 100644 index 000000000..10e57f2aa --- /dev/null +++ b/ui/component/viewers/videoViewer/internal/theater-mode.js @@ -0,0 +1,16 @@ +// @flow +import type { Player } from './videojs'; + +export function addTheaterModeButton(player: Player, toggleVideoTheaterMode: () => void) { + var myButton = player.controlBar.addChild('button', { + text: __('Theater mode'), + clickHandler: () => { + toggleVideoTheaterMode(); + }, + }); + + // $FlowFixMe + myButton.addClass('vjs-button--theater-mode'); + // $FlowFixMe + myButton.setAttribute('title', __('Theater mode')); +} diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 867a51da1..df5b21cb0 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -31,6 +31,9 @@ export type Player = { userActive: (?boolean) => boolean, overlay: any => void, mobileUi: any => void, + controlBar: { + addChild: (string, any) => void, + }, }; type Props = { diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index e54cbe03d..ccc9ae212 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -3,7 +3,6 @@ import React, { useEffect, useState, useContext, useCallback } from 'react'; import { stopContextMenu } from 'util/context-menu'; import type { Player } from './internal/videojs'; import VideoJs from './internal/videojs'; - import analytics from 'analytics'; import { EmbedContext } from 'page/embedWrapper/view'; import classnames from 'classnames'; @@ -13,6 +12,7 @@ import usePrevious from 'effects/use-previous'; import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded'; import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle'; import LoadingScreen from 'component/common/loading-screen'; +import { addTheaterModeButton } from './internal/theater-mode'; const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; @@ -35,6 +35,7 @@ type Props = { claimRewards: () => void, savePosition: (string, number) => void, clearPosition: string => void, + toggleVideoTheaterMode: () => void, }; /* @@ -62,6 +63,7 @@ function VideoViewer(props: Props) { savePosition, clearPosition, desktopPlayStartTime, + toggleVideoTheaterMode, } = props; const claimId = claim && claim.claim_id; const isAudio = contentType.includes('audio'); @@ -135,6 +137,8 @@ function VideoViewer(props: Props) { if (!embedded) { player.muted(muted); player.volume(volume); + + addTheaterModeButton(player, toggleVideoTheaterMode); } const shouldPlay = !embedded || autoplayIfEmbedded; diff --git a/ui/page/file/index.js b/ui/page/file/index.js index 113fda147..b3d523e5b 100644 --- a/ui/page/file/index.js +++ b/ui/page/file/index.js @@ -1,9 +1,15 @@ import { connect } from 'react-redux'; import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content'; import { withRouter } from 'react-router'; -import { doFetchFileInfo, makeSelectFileInfoForUri, makeSelectMetadataForUri, makeSelectClaimIsNsfw } from 'lbry-redux'; +import { + doFetchFileInfo, + makeSelectFileInfoForUri, + makeSelectMetadataForUri, + makeSelectClaimIsNsfw, + SETTINGS, +} from 'lbry-redux'; import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc'; -import { selectShowMatureContent } from 'redux/selectors/settings'; +import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectFileRenderModeForUri } from 'redux/selectors/content'; import { makeSelectCommentForCommentId } from 'redux/selectors/comments'; import FilePage from './view'; @@ -21,6 +27,7 @@ const select = (state, props) => { isMature: makeSelectClaimIsNsfw(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), renderMode: makeSelectFileRenderModeForUri(props.uri)(state), + videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), }; }; diff --git a/ui/page/file/view.jsx b/ui/page/file/view.jsx index f3ff86227..e5c9ebc70 100644 --- a/ui/page/file/view.jsx +++ b/ui/page/file/view.jsx @@ -24,6 +24,7 @@ type Props = { isMature: boolean, linkedComment: any, setPrimaryUri: (?string) => void, + videoTheaterMode: boolean, }; function FilePage(props: Props) { @@ -39,6 +40,7 @@ function FilePage(props: Props) { costInfo, linkedComment, setPrimaryUri, + videoTheaterMode, } = props; const cost = costInfo ? costInfo.cost : null; const hasFileInfo = fileInfo !== undefined; @@ -67,10 +69,9 @@ function FilePage(props: Props) { return (
- +
{/* playables will be rendered and injected by */} -
); } @@ -96,14 +97,14 @@ function FilePage(props: Props) { return ( - + ); } - function renderBlockedPage() { + if (obscureNsfw && isMature) { return ( @@ -111,19 +112,21 @@ function FilePage(props: Props) { ); } - if (obscureNsfw && isMature) { - return renderBlockedPage(); - } - return (
{renderFilePageLayout()} - +
+
+ {RENDER_MODES.FLOATING_MODES.includes(renderMode) && } + +
+ {videoTheaterMode && } +
- + {!videoTheaterMode && }
); } diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js index a204e0d34..740c885c7 100644 --- a/ui/redux/actions/settings.js +++ b/ui/redux/actions/settings.js @@ -414,3 +414,12 @@ export function doSetAppToTrayWhenClosed(value) { dispatch(doSetClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED, value)); }; } + +export function toggleVideoTheaterMode() { + return (dispatch, getState) => { + const state = getState(); + const videoTheaterMode = makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state); + + dispatch(doSetClientSetting(SETTINGS.VIDEO_THEATER_MODE, !videoTheaterMode)); + }; +} diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index aea778ef4..bd6dd11b5 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -46,6 +46,7 @@ const defaultState = { [SETTINGS.OS_NOTIFICATIONS_ENABLED]: true, [SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false, [SETTINGS.TILE_LAYOUT]: true, + [SETTINGS.VIDEO_THEATER_MODE]: false, [SETTINGS.DARK_MODE_TIMES]: { from: { hour: '21', min: '00', formattedTime: '21:00' }, diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index 9cce7dc07..9b278dd44 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -94,6 +94,42 @@ } } +.vjs-control-bar { + visibility: visible; + opacity: 1 !important; + transition: visibility 1s, opacity 1s; +} + +.vjs-fullscreen-control { + order: 2; +} + +.vjs-button--theater-mode.vjs-button { + display: none; + + @media (min-width: $breakpoint-medium) { + display: block; + order: 1; + width: 1.25rem; + height: 1.25rem; + margin-top: 0.3rem; + background-repeat: no-repeat; + background-position: center; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 -2 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-monitor'%3E%3Crect x='2' y='3' width='20' height='14' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='8' y1='21' x2='16' y2='21'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12' y2='21'%3E%3C/line%3E%3C/svg%3E"); + } + + &:focus:not(:focus-visible) { + // Need to repeat these styles because of videojs weirdness + background: black + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 -2 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-monitor'%3E%3Crect x='2' y='3' width='20' height='14' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='8' y1='21' x2='16' y2='21'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12' y2='21'%3E%3C/line%3E%3C/svg%3E") + no-repeat center; + } + + .vjs-icon-placeholder { + display: none; + } +} + .button--link { color: var(--color-link); transition: color 0.2s; diff --git a/ui/scss/component/_content.scss b/ui/scss/component/_content.scss index 4857d7fd5..1fd65cfab 100644 --- a/ui/scss/component/_content.scss +++ b/ui/scss/component/_content.scss @@ -24,6 +24,12 @@ } } +.content__viewer--theater-mode { + top: 0; + border-radius: 0; + border: none; +} + .content__wrapper { position: relative; width: 100%; @@ -33,6 +39,10 @@ .content__wrapper--floating { height: var(--floating-viewer-height); width: var(--floating-viewer-width); + + .vjs-button--theater-mode { + display: none; + } } .content__actions { @@ -107,6 +117,11 @@ } } +.content__cover--theater-mode { + @extend .content__cover; + border-radius: 0; +} + .content__cover--none { @include thumbnail; cursor: default; diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss index daa1565b0..7929ef84f 100644 --- a/ui/scss/component/_main.scss +++ b/ui/scss/component/_main.scss @@ -39,6 +39,10 @@ padding-top: var(--spacing-s); } +.main-wrapper__inner--theater-mode { + padding-top: 0; +} + .main { position: relative; width: calc(100% - var(--side-nav-width) - var(--spacing-l)); @@ -65,15 +69,43 @@ align-items: flex-start; padding-left: var(--spacing-m); padding-right: var(--spacing-m); + position: relative; > :first-child { - flex: 1; - margin-right: var(--spacing-m); + flex-grow: 2; + } + + .file-page__secondary-content { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + margin-top: var(--spacing-m); + max-width: var(--page-max-width--filepage); + margin-left: auto; + margin-right: auto; + + > :first-child { + flex: 1; + } + + @media (min-width: $breakpoint-medium) { + flex-direction: row; + } + } + + .file-page__info { + margin-top: var(--spacing-m); } .file-page__recommended { - width: 25rem; height: 0%; + width: 35rem; + margin-left: var(--spacing-m); + + @media (max-width: $breakpoint-small) { + margin-left: 0; + } @media (max-width: $breakpoint-medium) { width: 100%; @@ -82,8 +114,6 @@ } @media (max-width: $breakpoint-medium) { - flex-direction: column; - > :first-child { margin-right: 0; } @@ -95,13 +125,48 @@ } } +.main--theater-mode { + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + width: 100vw; + max-width: none; + + > :first-child { + margin-right: 0; + } + + .file-page__info { + padding: 0 var(--spacing-m); + margin-top: var(--spacing-m); + max-width: var(--page-max-width--filepage); + display: flex; + flex-direction: column; + margin-left: auto; + margin-right: auto; + } + + .file-page__recommended { + width: 25rem; + } + + .file-page__secondary-content { + padding: 0 var(--spacing-s); + + @media (min-width: $breakpoint-medium) { + flex-direction: row; + } + } +} + .main--full-width { @extend .main; @media (min-width: $breakpoint-large) { max-width: none; width: 100%; - padding: 0 var(--spacing-l); + margin: 0 var(--spacing-l); } } diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index 2edf5a9e2..3d79ef5ce 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -108,6 +108,7 @@ display: flex; align-items: center; margin-top: var(--spacing-l); + margin-right: var(--spacing-s); ~ .section { margin-top: var(--spacing-l); diff --git a/ui/scss/themes/light.scss b/ui/scss/themes/light.scss index e8eb9dbb3..65c7b1640 100644 --- a/ui/scss/themes/light.scss +++ b/ui/scss/themes/light.scss @@ -1,5 +1,4 @@ :root { - // Button --color-navigation-icon: var(--color-gray-5); --color-link-active: var(--color-primary); --color-navigation-link: var(--color-gray-5);