add video theater mode button
This commit is contained in:
parent
b43593a996
commit
d43c4d053e
19 changed files with 206 additions and 24 deletions
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
<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 && (
|
||||
<SideNavigation
|
||||
sidebarOpen={sidebarOpen}
|
||||
|
@ -96,6 +104,7 @@ function Page(props: Props) {
|
|||
'main--full-width': fullWidthPage,
|
||||
'main--auth-page': authPage,
|
||||
'main--file-page': filePage,
|
||||
'main--theater-mode': isOnFilePage && videoTheaterMode,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -55,7 +55,6 @@ export default function RecommendedContent(props: Props) {
|
|||
title={__('Related')}
|
||||
body={
|
||||
<ClaimList
|
||||
isCardBody
|
||||
type="small"
|
||||
loading={isSearching}
|
||||
uris={recommendedContent}
|
||||
|
|
|
@ -8,6 +8,7 @@ import VideoViewer from './view';
|
|||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode } from 'redux/actions/settings';
|
||||
|
||||
const select = (state, props) => {
|
||||
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));
|
||||
|
|
16
ui/component/viewers/videoViewer/internal/theater-mode.js
Normal file
16
ui/component/viewers/videoViewer/internal/theater-mode.js
Normal 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'));
|
||||
}
|
|
@ -31,6 +31,9 @@ export type Player = {
|
|||
userActive: (?boolean) => boolean,
|
||||
overlay: any => void,
|
||||
mobileUi: any => void,
|
||||
controlBar: {
|
||||
addChild: (string, any) => void,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
|
||||
<FileRenderInitiator uri={uri} />
|
||||
<FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
|
||||
</div>
|
||||
{/* playables will be rendered and injected by <FileRenderFloating> */}
|
||||
<FileTitle uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -96,14 +97,14 @@ function FilePage(props: Props) {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FileRenderInitiator uri={uri} />
|
||||
<FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
|
||||
<FileRenderInline uri={uri} />
|
||||
<FileTitle uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBlockedPage() {
|
||||
if (obscureNsfw && isMature) {
|
||||
return (
|
||||
<Page>
|
||||
<FileTitle uri={uri} isNsfwBlocked />
|
||||
|
@ -111,19 +112,21 @@ function FilePage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (obscureNsfw && isMature) {
|
||||
return renderBlockedPage();
|
||||
}
|
||||
|
||||
return (
|
||||
<Page className="file-page" filePage>
|
||||
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||
{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>
|
||||
|
||||
<RecommendedContent uri={uri} />
|
||||
{!videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue