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);