add video theater mode button

This commit is contained in:
Sean Yesmunt 2021-01-08 10:21:27 -05:00
parent b43593a996
commit d43c4d053e
19 changed files with 206 additions and 24 deletions

View file

@ -26,6 +26,7 @@ const select = (state, props) => {
streamingUrl: makeSelectStreamingUrlForUri(uri)(state), streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state), floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state), renderMode: makeSelectFileRenderModeForUri(uri)(state),
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
}; };
}; };

View file

@ -31,6 +31,7 @@ type Props = {
renderMode: string, renderMode: string,
playingUri: ?PlayingUri, playingUri: ?PlayingUri,
primaryUri: ?string, primaryUri: ?string,
videoTheaterMode: boolean,
}; };
export default function FileRenderFloating(props: Props) { export default function FileRenderFloating(props: Props) {
@ -45,6 +46,7 @@ export default function FileRenderFloating(props: Props) {
renderMode, renderMode,
playingUri, playingUri,
primaryUri, primaryUri,
videoTheaterMode,
} = props; } = props;
const { const {
location: { pathname }, location: { pathname },
@ -187,7 +189,7 @@ export default function FileRenderFloating(props: Props) {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
onFullscreenChange(window, 'remove', handleResize); onFullscreenChange(window, 'remove', handleResize);
}; };
}, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying]); }, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying, videoTheaterMode]);
useEffect(() => { useEffect(() => {
// @if TARGET='app' // @if TARGET='app'
@ -248,6 +250,7 @@ export default function FileRenderFloating(props: Props) {
className={classnames('content__viewer', { className={classnames('content__viewer', {
'content__viewer--floating': isFloating, 'content__viewer--floating': isFloating,
'content__viewer--inline': !isFloating, 'content__viewer--inline': !isFloating,
'content__viewer--theater-mode': !isFloating && videoTheaterMode,
})} })}
style={ style={
!isFloating && fileViewerRect !isFloating && fileViewerRect

View file

@ -32,6 +32,7 @@ type Props = {
claim: StreamClaim, claim: StreamClaim,
claimWasPurchased: boolean, claimWasPurchased: boolean,
authenticated: boolean, authenticated: boolean,
videoTheaterMode: boolean,
}; };
export default function FileRenderInitiator(props: Props) { export default function FileRenderInitiator(props: Props) {
@ -51,6 +52,7 @@ export default function FileRenderInitiator(props: Props) {
costInfo, costInfo,
claimWasPurchased, claimWasPurchased,
authenticated, authenticated,
videoTheaterMode,
} = props; } = props;
const cost = costInfo && costInfo.cost; const cost = costInfo && costInfo.cost;
const isFree = hasCostInfo && cost === 0; const isFree = hasCostInfo && cost === 0;
@ -118,6 +120,7 @@ export default function FileRenderInitiator(props: Props) {
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}} style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', { className={classnames('content__cover', {
'content__cover--disabled': disabled, 'content__cover--disabled': disabled,
'content__cover--theater-mode': videoTheaterMode,
'card__media--nsfw': obscurePreview, 'card__media--nsfw': obscurePreview,
})} })}
> >

View file

@ -1,4 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { SETTINGS } from 'lbry-redux';
import Page from './view'; import Page from './view';
export default connect()(Page); const select = (state, props) => ({
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
});
export default connect(select)(Page);

View file

@ -26,6 +26,7 @@ type Props = {
noFooter: boolean, noFooter: boolean,
noSideNavigation: boolean, noSideNavigation: boolean,
fullWidthPage: boolean, fullWidthPage: boolean,
videoTheaterMode: boolean,
backout: { backout: {
backLabel?: string, backLabel?: string,
backNavDefault?: string, backNavDefault?: string,
@ -45,7 +46,9 @@ function Page(props: Props) {
noFooter = false, noFooter = false,
noSideNavigation = false, noSideNavigation = false,
backout, backout,
videoTheaterMode,
} = props; } = props;
const { const {
location: { pathname }, location: { pathname },
} = useHistory(); } = useHistory();
@ -82,7 +85,12 @@ function Page(props: Props) {
setSidebarOpen={setSidebarOpen} setSidebarOpen={setSidebarOpen}
/> />
)} )}
<div className={classnames('main-wrapper__inner', { 'main-wrapper__inner--filepage': isOnFilePage })}> <div
className={classnames('main-wrapper__inner', {
'main-wrapper__inner--filepage': isOnFilePage,
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
})}
>
{!authPage && !noSideNavigation && ( {!authPage && !noSideNavigation && (
<SideNavigation <SideNavigation
sidebarOpen={sidebarOpen} sidebarOpen={sidebarOpen}
@ -96,6 +104,7 @@ function Page(props: Props) {
'main--full-width': fullWidthPage, 'main--full-width': fullWidthPage,
'main--auth-page': authPage, 'main--auth-page': authPage,
'main--file-page': filePage, 'main--file-page': filePage,
'main--theater-mode': isOnFilePage && videoTheaterMode,
})} })}
> >
{children} {children}

View file

@ -55,7 +55,6 @@ export default function RecommendedContent(props: Props) {
title={__('Related')} title={__('Related')}
body={ body={
<ClaimList <ClaimList
isCardBody
type="small" type="small"
loading={isSearching} loading={isSearching}
uris={recommendedContent} uris={recommendedContent}

View file

@ -8,6 +8,7 @@ import VideoViewer from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { toggleVideoTheaterMode } from 'redux/actions/settings';
const select = (state, props) => { const select = (state, props) => {
const { search } = props.location; const { search } = props.location;
@ -35,6 +36,7 @@ const perform = dispatch => ({
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)), doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)), doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()), claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
}); });
export default withRouter(connect(select, perform)(VideoViewer)); export default withRouter(connect(select, perform)(VideoViewer));

View file

@ -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'));
}

View file

@ -31,6 +31,9 @@ export type Player = {
userActive: (?boolean) => boolean, userActive: (?boolean) => boolean,
overlay: any => void, overlay: any => void,
mobileUi: any => void, mobileUi: any => void,
controlBar: {
addChild: (string, any) => void,
},
}; };
type Props = { type Props = {

View file

@ -3,7 +3,6 @@ import React, { useEffect, useState, useContext, useCallback } from 'react';
import { stopContextMenu } from 'util/context-menu'; import { stopContextMenu } from 'util/context-menu';
import type { Player } from './internal/videojs'; import type { Player } from './internal/videojs';
import VideoJs from './internal/videojs'; import VideoJs from './internal/videojs';
import analytics from 'analytics'; import analytics from 'analytics';
import { EmbedContext } from 'page/embedWrapper/view'; import { EmbedContext } from 'page/embedWrapper/view';
import classnames from 'classnames'; import classnames from 'classnames';
@ -13,6 +12,7 @@ import usePrevious from 'effects/use-previous';
import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded'; import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle'; import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import { addTheaterModeButton } from './internal/theater-mode';
const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
@ -35,6 +35,7 @@ type Props = {
claimRewards: () => void, claimRewards: () => void,
savePosition: (string, number) => void, savePosition: (string, number) => void,
clearPosition: string => void, clearPosition: string => void,
toggleVideoTheaterMode: () => void,
}; };
/* /*
@ -62,6 +63,7 @@ function VideoViewer(props: Props) {
savePosition, savePosition,
clearPosition, clearPosition,
desktopPlayStartTime, desktopPlayStartTime,
toggleVideoTheaterMode,
} = props; } = props;
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const isAudio = contentType.includes('audio'); const isAudio = contentType.includes('audio');
@ -135,6 +137,8 @@ function VideoViewer(props: Props) {
if (!embedded) { if (!embedded) {
player.muted(muted); player.muted(muted);
player.volume(volume); player.volume(volume);
addTheaterModeButton(player, toggleVideoTheaterMode);
} }
const shouldPlay = !embedded || autoplayIfEmbedded; const shouldPlay = !embedded || autoplayIfEmbedded;

View file

@ -1,9 +1,15 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content'; import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content';
import { withRouter } from 'react-router'; 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 { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content'; import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import { makeSelectCommentForCommentId } from 'redux/selectors/comments'; import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
import FilePage from './view'; import FilePage from './view';
@ -21,6 +27,7 @@ const select = (state, props) => {
isMature: makeSelectClaimIsNsfw(props.uri)(state), isMature: makeSelectClaimIsNsfw(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state), renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
}; };
}; };

View file

@ -24,6 +24,7 @@ type Props = {
isMature: boolean, isMature: boolean,
linkedComment: any, linkedComment: any,
setPrimaryUri: (?string) => void, setPrimaryUri: (?string) => void,
videoTheaterMode: boolean,
}; };
function FilePage(props: Props) { function FilePage(props: Props) {
@ -39,6 +40,7 @@ function FilePage(props: Props) {
costInfo, costInfo,
linkedComment, linkedComment,
setPrimaryUri, setPrimaryUri,
videoTheaterMode,
} = props; } = props;
const cost = costInfo ? costInfo.cost : null; const cost = costInfo ? costInfo.cost : null;
const hasFileInfo = fileInfo !== undefined; const hasFileInfo = fileInfo !== undefined;
@ -67,10 +69,9 @@ function FilePage(props: Props) {
return ( return (
<React.Fragment> <React.Fragment>
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}> <div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
<FileRenderInitiator uri={uri} /> <FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
</div> </div>
{/* playables will be rendered and injected by <FileRenderFloating> */} {/* playables will be rendered and injected by <FileRenderFloating> */}
<FileTitle uri={uri} />
</React.Fragment> </React.Fragment>
); );
} }
@ -96,14 +97,14 @@ function FilePage(props: Props) {
return ( return (
<React.Fragment> <React.Fragment>
<FileRenderInitiator uri={uri} /> <FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
<FileRenderInline uri={uri} /> <FileRenderInline uri={uri} />
<FileTitle uri={uri} /> <FileTitle uri={uri} />
</React.Fragment> </React.Fragment>
); );
} }
function renderBlockedPage() { if (obscureNsfw && isMature) {
return ( return (
<Page> <Page>
<FileTitle uri={uri} isNsfwBlocked /> <FileTitle uri={uri} isNsfwBlocked />
@ -111,19 +112,21 @@ function FilePage(props: Props) {
); );
} }
if (obscureNsfw && isMature) {
return renderBlockedPage();
}
return ( return (
<Page className="file-page" filePage> <Page className="file-page" filePage>
<div className={classnames('section card-stack', `file-page__${renderMode}`)}> <div className={classnames('section card-stack', `file-page__${renderMode}`)}>
{renderFilePageLayout()} {renderFilePageLayout()}
<CommentsList uri={uri} linkedComment={linkedComment} /> <div className="file-page__secondary-content">
<div>
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitle uri={uri} />}
<CommentsList uri={uri} linkedComment={linkedComment} />
</div>
{videoTheaterMode && <RecommendedContent uri={uri} />}
</div>
</div> </div>
<RecommendedContent uri={uri} /> {!videoTheaterMode && <RecommendedContent uri={uri} />}
</Page> </Page>
); );
} }

View file

@ -414,3 +414,12 @@ export function doSetAppToTrayWhenClosed(value) {
dispatch(doSetClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED, 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));
};
}

View file

@ -46,6 +46,7 @@ const defaultState = {
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: true, [SETTINGS.OS_NOTIFICATIONS_ENABLED]: true,
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false, [SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false,
[SETTINGS.TILE_LAYOUT]: true, [SETTINGS.TILE_LAYOUT]: true,
[SETTINGS.VIDEO_THEATER_MODE]: false,
[SETTINGS.DARK_MODE_TIMES]: { [SETTINGS.DARK_MODE_TIMES]: {
from: { hour: '21', min: '00', formattedTime: '21:00' }, from: { hour: '21', min: '00', formattedTime: '21:00' },

View file

@ -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 { .button--link {
color: var(--color-link); color: var(--color-link);
transition: color 0.2s; transition: color 0.2s;

View file

@ -24,6 +24,12 @@
} }
} }
.content__viewer--theater-mode {
top: 0;
border-radius: 0;
border: none;
}
.content__wrapper { .content__wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@ -33,6 +39,10 @@
.content__wrapper--floating { .content__wrapper--floating {
height: var(--floating-viewer-height); height: var(--floating-viewer-height);
width: var(--floating-viewer-width); width: var(--floating-viewer-width);
.vjs-button--theater-mode {
display: none;
}
} }
.content__actions { .content__actions {
@ -107,6 +117,11 @@
} }
} }
.content__cover--theater-mode {
@extend .content__cover;
border-radius: 0;
}
.content__cover--none { .content__cover--none {
@include thumbnail; @include thumbnail;
cursor: default; cursor: default;

View file

@ -39,6 +39,10 @@
padding-top: var(--spacing-s); padding-top: var(--spacing-s);
} }
.main-wrapper__inner--theater-mode {
padding-top: 0;
}
.main { .main {
position: relative; position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-l)); width: calc(100% - var(--side-nav-width) - var(--spacing-l));
@ -65,15 +69,43 @@
align-items: flex-start; align-items: flex-start;
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
padding-right: var(--spacing-m); padding-right: var(--spacing-m);
position: relative;
> :first-child { > :first-child {
flex: 1; flex-grow: 2;
margin-right: var(--spacing-m); }
.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 { .file-page__recommended {
width: 25rem;
height: 0%; height: 0%;
width: 35rem;
margin-left: var(--spacing-m);
@media (max-width: $breakpoint-small) {
margin-left: 0;
}
@media (max-width: $breakpoint-medium) { @media (max-width: $breakpoint-medium) {
width: 100%; width: 100%;
@ -82,8 +114,6 @@
} }
@media (max-width: $breakpoint-medium) { @media (max-width: $breakpoint-medium) {
flex-direction: column;
> :first-child { > :first-child {
margin-right: 0; 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 { .main--full-width {
@extend .main; @extend .main;
@media (min-width: $breakpoint-large) { @media (min-width: $breakpoint-large) {
max-width: none; max-width: none;
width: 100%; width: 100%;
padding: 0 var(--spacing-l); margin: 0 var(--spacing-l);
} }
} }

View file

@ -108,6 +108,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
margin-right: var(--spacing-s);
~ .section { ~ .section {
margin-top: var(--spacing-l); margin-top: var(--spacing-l);

View file

@ -1,5 +1,4 @@
:root { :root {
// Button
--color-navigation-icon: var(--color-gray-5); --color-navigation-icon: var(--color-gray-5);
--color-link-active: var(--color-primary); --color-link-active: var(--color-primary);
--color-navigation-link: var(--color-gray-5); --color-navigation-link: var(--color-gray-5);