Add dynamic player aspect ratio resizing
This commit is contained in:
parent
7e65062613
commit
65b9906086
23 changed files with 381 additions and 134 deletions
|
@ -1,18 +1,18 @@
|
|||
// @flow
|
||||
import { FLOATING_PLAYER_CLASS } from './view';
|
||||
|
||||
function getRootEl() {
|
||||
export function getRootEl() {
|
||||
return document && document.documentElement;
|
||||
}
|
||||
|
||||
export function getScreenWidth() {
|
||||
const mainEl = getRootEl();
|
||||
return mainEl ? mainEl.clientWidth : window.innerWidth;
|
||||
const rootEl = getRootEl();
|
||||
return rootEl ? rootEl.clientWidth : window.innerWidth;
|
||||
}
|
||||
|
||||
export function getScreenHeight() {
|
||||
const mainEl = getRootEl();
|
||||
return mainEl ? mainEl.clientHeight : window.innerHeight;
|
||||
const rootEl = getRootEl();
|
||||
return rootEl ? rootEl.clientHeight : window.innerHeight;
|
||||
}
|
||||
|
||||
export function getFloatingPlayerRect() {
|
||||
|
@ -52,3 +52,36 @@ export function calculateRelativePos(x: number, y: number) {
|
|||
y: y / getScreenHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
// Max landscape height = calculates the maximum size the player would be at
|
||||
// if it was at landscape aspect ratio
|
||||
export function getMaxLandscapeHeight(width?: number) {
|
||||
const windowWidth = width || getScreenWidth();
|
||||
const maxLandscapeHeight = (windowWidth * 9) / 16;
|
||||
|
||||
return maxLandscapeHeight;
|
||||
}
|
||||
|
||||
// If a video is higher than landscape, this calculates how much is needed in order
|
||||
// for the video to be centered in a container at the landscape height
|
||||
export function getAmountNeededToCenterVideo(height: number, fromValue: number) {
|
||||
const minVideoHeight = getMaxLandscapeHeight();
|
||||
const timesHigherThanLandscape = height / minVideoHeight;
|
||||
const amountNeededToCenter = (height - fromValue) / timesHigherThanLandscape;
|
||||
|
||||
return amountNeededToCenter * -1;
|
||||
}
|
||||
|
||||
export function getPossiblePlayerHeight(height: number, isMobile: boolean) {
|
||||
// min player height = landscape size based on screen width (only for mobile, since
|
||||
// comment expansion will default to landscape view height)
|
||||
const minHeight = getMaxLandscapeHeight();
|
||||
const maxPercentOfScreen = isMobile ? 70 : 80;
|
||||
// max player height
|
||||
const maxHeight = (getScreenHeight() * maxPercentOfScreen) / 100;
|
||||
|
||||
const forceMaxHeight = height < maxHeight ? height : maxHeight;
|
||||
const forceMinHeight = isMobile && height < minHeight ? minHeight : forceMaxHeight;
|
||||
|
||||
return forceMinHeight;
|
||||
}
|
||||
|
|
|
@ -22,11 +22,10 @@ import { selectCostInfoForUri } from 'lbryinc';
|
|||
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
|
||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||
import { withRouter } from 'react-router';
|
||||
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
||||
import { selectAppDrawerOpen } from 'redux/selectors/app';
|
||||
import { selectIsActiveLivestreamForUri, selectCommentSocketConnected } from 'redux/selectors/livestream';
|
||||
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
|
||||
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
||||
import { isStreamPlaceholderClaim } from 'util/claim';
|
||||
import { isStreamPlaceholderClaim, getVideoClaimAspectRatio } from 'util/claim';
|
||||
import FileRenderFloating from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -57,10 +56,11 @@ const select = (state, props) => {
|
|||
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
|
||||
collectionId,
|
||||
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
|
||||
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
||||
videoAspectRatio: getVideoClaimAspectRatio(claim),
|
||||
socketConnected: selectCommentSocketConnected(state),
|
||||
isLivestreamClaim: isStreamPlaceholderClaim(claim),
|
||||
geoRestriction: selectGeoRestrictionForUri(state, uri),
|
||||
appDrawerOpen: selectAppDrawerOpen(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,6 @@ const perform = {
|
|||
doFetchRecommendedContent,
|
||||
doUriInitiatePlay,
|
||||
doSetPlayingUri,
|
||||
doSetMobilePlayerDimensions,
|
||||
doCommentSocketConnect,
|
||||
doCommentSocketDisconnect,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import { Global } from '@emotion/react';
|
||||
|
||||
import type { ElementRef } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import React from 'react';
|
||||
|
@ -18,7 +23,16 @@ import { useHistory } from 'react-router';
|
|||
import { isURIEqual } from 'util/lbryURI';
|
||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||
import usePlayNext from 'effects/use-play-next';
|
||||
import { getScreenWidth, getScreenHeight, clampFloatingPlayerToScreen, calculateRelativePos } from './helper-functions';
|
||||
import {
|
||||
getRootEl,
|
||||
getScreenWidth,
|
||||
getScreenHeight,
|
||||
clampFloatingPlayerToScreen,
|
||||
calculateRelativePos,
|
||||
getMaxLandscapeHeight,
|
||||
getAmountNeededToCenterVideo,
|
||||
getPossiblePlayerHeight,
|
||||
} from './helper-functions';
|
||||
|
||||
// scss/init/vars.scss
|
||||
// --header-height
|
||||
|
@ -26,9 +40,10 @@ const HEADER_HEIGHT = 60;
|
|||
// --header-height-mobile
|
||||
export const HEADER_HEIGHT_MOBILE = 56;
|
||||
|
||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
||||
|
||||
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
|
||||
export const CONTENT_VIEWER_CLASS = 'content__viewer';
|
||||
export const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
|
||||
|
||||
// ****************************************************************************
|
||||
|
@ -55,11 +70,11 @@ type Props = {
|
|||
doUriInitiatePlay: (playingOptions: PlayingUri, isPlayable: ?boolean, isFloating: ?boolean) => void,
|
||||
doSetPlayingUri: ({ uri?: ?string }) => void,
|
||||
isCurrentClaimLive?: boolean,
|
||||
mobilePlayerDimensions?: any,
|
||||
videoAspectRatio: number,
|
||||
socketConnected: boolean,
|
||||
isLivestreamClaim: boolean,
|
||||
geoRestriction: ?GeoRestriction,
|
||||
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void,
|
||||
appDrawerOpen: boolean,
|
||||
doCommentSocketConnect: (string, string, string) => void,
|
||||
doCommentSocketDisconnect: (string, string) => void,
|
||||
};
|
||||
|
@ -88,23 +103,27 @@ export default function FileRenderFloating(props: Props) {
|
|||
doUriInitiatePlay,
|
||||
doSetPlayingUri,
|
||||
isCurrentClaimLive,
|
||||
mobilePlayerDimensions,
|
||||
videoAspectRatio,
|
||||
geoRestriction,
|
||||
doSetMobilePlayerDimensions,
|
||||
appDrawerOpen,
|
||||
doCommentSocketConnect,
|
||||
doCommentSocketDisconnect,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const initialPlayerHeight = React.useRef();
|
||||
const resizedBetweenFloating = React.useRef();
|
||||
|
||||
const {
|
||||
location: { state },
|
||||
} = useHistory();
|
||||
const hideFloatingPlayer = state && state.hideFloatingPlayer;
|
||||
|
||||
const { uri: playingUrl, source: playingUriSource, primaryUri: playingPrimaryUri } = playingUri;
|
||||
|
||||
const isComment = playingUriSource === 'comment';
|
||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||
const mainFilePlaying = Boolean(!isFloating && primaryUri && isURIEqual(uri, primaryUri));
|
||||
const noFloatingPlayer = !isFloating || !floatingPlayerEnabled || hideFloatingPlayer;
|
||||
|
||||
const [fileViewerRect, setFileViewerRect] = React.useState();
|
||||
|
@ -151,13 +170,14 @@ export default function FileRenderFloating(props: Props) {
|
|||
x: rect.x,
|
||||
};
|
||||
|
||||
if (videoAspectRatio && !initialPlayerHeight.current) {
|
||||
const heightForRect = getPossiblePlayerHeight(videoAspectRatio * rect.width, isMobile);
|
||||
initialPlayerHeight.current = heightForRect;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
setFileViewerRect({ ...objectRect, windowOffset: window.pageYOffset });
|
||||
|
||||
if (!mobilePlayerDimensions || mobilePlayerDimensions.height !== rect.height) {
|
||||
doSetMobilePlayerDimensions({ height: rect.height, width: getScreenWidth() });
|
||||
}
|
||||
}, [doSetMobilePlayerDimensions, mainFilePlaying, mobilePlayerDimensions]);
|
||||
}, [isMobile, mainFilePlaying, videoAspectRatio]);
|
||||
|
||||
const restoreToRelativePosition = React.useCallback(() => {
|
||||
const SCROLL_BAR_PX = 12; // root: --body-scrollbar-width
|
||||
|
@ -222,11 +242,16 @@ export default function FileRenderFloating(props: Props) {
|
|||
|
||||
// Listen to main-window resizing and adjust the floating player position accordingly:
|
||||
React.useEffect(() => {
|
||||
// intended to only run once: when floating player switches between true - false
|
||||
// otherwise handleResize() can run twice when this effect re-runs, so use
|
||||
// resizedBetweenFloating ref
|
||||
if (isFloating) {
|
||||
// Ensure player is within screen when 'isFloating' changes.
|
||||
restoreToRelativePosition();
|
||||
} else {
|
||||
resizedBetweenFloating.current = false;
|
||||
} else if (!resizedBetweenFloating.current) {
|
||||
handleResize();
|
||||
resizedBetweenFloating.current = true;
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
|
@ -234,14 +259,13 @@ export default function FileRenderFloating(props: Props) {
|
|||
}
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
if (!isFloating) onFullscreenChange(window, 'add', handleResize);
|
||||
if (!isFloating && !isMobile) onFullscreenChange(window, 'add', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
if (!isFloating) onFullscreenChange(window, 'remove', handleResize);
|
||||
if (!isFloating && !isMobile) onFullscreenChange(window, 'remove', handleResize);
|
||||
};
|
||||
|
||||
// Only listen to these and avoid infinite loops
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clampToScreenOnResize, handleResize, isFloating]);
|
||||
|
||||
|
@ -267,10 +291,14 @@ export default function FileRenderFloating(props: Props) {
|
|||
}, [doFetchRecommendedContent, isFloating, uri]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFloating && isMobile) {
|
||||
doSetMobilePlayerDimensions({ height: null, width: null });
|
||||
return () => {
|
||||
// basically if switched videos (playingUrl change or unmount),
|
||||
// erase the data so it can be re-calculated
|
||||
if (playingUrl) {
|
||||
initialPlayerHeight.current = undefined;
|
||||
}
|
||||
}, [doSetMobilePlayerDimensions, doSetPlayingUri, isFloating, isMobile]);
|
||||
};
|
||||
}, [playingUrl]);
|
||||
|
||||
if (
|
||||
geoRestriction ||
|
||||
|
@ -329,7 +357,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
cancel=".button"
|
||||
>
|
||||
<div
|
||||
className={classnames('content__viewer', {
|
||||
className={classnames([CONTENT_VIEWER_CLASS], {
|
||||
[FLOATING_PLAYER_CLASS]: isFloating,
|
||||
'content__viewer--inline': !isFloating,
|
||||
'content__viewer--secondary': isComment,
|
||||
|
@ -341,18 +369,28 @@ export default function FileRenderFloating(props: Props) {
|
|||
!isFloating && fileViewerRect
|
||||
? {
|
||||
width: fileViewerRect.width,
|
||||
height: fileViewerRect.height,
|
||||
height: appDrawerOpen ? `${getMaxLandscapeHeight()}px` : fileViewerRect.height,
|
||||
left: fileViewerRect.x,
|
||||
top:
|
||||
isMobile && !playingUriSource
|
||||
? HEADER_HEIGHT_MOBILE
|
||||
: fileViewerRect.windowOffset +
|
||||
fileViewerRect.top -
|
||||
(!isMobile ? HEADER_HEIGHT - (IS_DESKTOP_MAC ? 24 : 0) : 0),
|
||||
: fileViewerRect.windowOffset + fileViewerRect.top - HEADER_HEIGHT,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{uri && videoAspectRatio && fileViewerRect ? (
|
||||
<PlayerGlobalStyles
|
||||
videoAspectRatio={videoAspectRatio}
|
||||
videoTheaterMode={videoTheaterMode}
|
||||
appDrawerOpen={appDrawerOpen}
|
||||
initialPlayerHeight={initialPlayerHeight}
|
||||
isFloating={isFloating}
|
||||
fileViewerRect={fileViewerRect}
|
||||
mainFilePlaying={mainFilePlaying}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className={classnames('content__wrapper', { 'content__wrapper--floating': isFloating })}>
|
||||
{isFloating && (
|
||||
<Button
|
||||
|
@ -398,3 +436,141 @@ export default function FileRenderFloating(props: Props) {
|
|||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
type GlobalStylesProps = {
|
||||
videoAspectRatio: number,
|
||||
videoTheaterMode: boolean,
|
||||
appDrawerOpen: boolean,
|
||||
initialPlayerHeight: ElementRef<any>,
|
||||
isFloating: boolean,
|
||||
fileViewerRect: any,
|
||||
mainFilePlaying: boolean,
|
||||
};
|
||||
|
||||
const PlayerGlobalStyles = (props: GlobalStylesProps) => {
|
||||
const {
|
||||
videoAspectRatio,
|
||||
videoTheaterMode,
|
||||
appDrawerOpen,
|
||||
initialPlayerHeight,
|
||||
isFloating,
|
||||
fileViewerRect,
|
||||
mainFilePlaying,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isMobilePlayer = isMobile && !isFloating; // to avoid miniplayer -> file page only
|
||||
|
||||
const heightForViewer = getPossiblePlayerHeight(videoAspectRatio * fileViewerRect.width, isMobile);
|
||||
const widthForViewer = heightForViewer / videoAspectRatio;
|
||||
const maxLandscapeHeight = getMaxLandscapeHeight(isMobile ? undefined : widthForViewer);
|
||||
const heightResult = appDrawerOpen ? `${maxLandscapeHeight}px` : `${heightForViewer}px`;
|
||||
|
||||
const forceDefaults = !mainFilePlaying || videoTheaterMode || isFloating || isMobile;
|
||||
const videoGreaterThanLandscape = heightForViewer > maxLandscapeHeight;
|
||||
|
||||
// Handles video shrink + center on mobile view
|
||||
// direct DOM manipulation due to performance for every scroll
|
||||
React.useEffect(() => {
|
||||
if (!isMobilePlayer || !mainFilePlaying || appDrawerOpen) return;
|
||||
|
||||
const viewer = document.querySelector(`.${CONTENT_VIEWER_CLASS}`);
|
||||
if (viewer) viewer.style.height = `${heightForViewer}px`;
|
||||
|
||||
function handleScroll() {
|
||||
const rootEl = getRootEl();
|
||||
|
||||
const viewer = document.querySelector(`.${CONTENT_VIEWER_CLASS}`);
|
||||
const videoNode = document.querySelector('.vjs-tech');
|
||||
const touchOverlay = document.querySelector('.vjs-touch-overlay');
|
||||
|
||||
if (rootEl && viewer) {
|
||||
const scrollTop = window.pageYOffset || rootEl.scrollTop;
|
||||
const isHigherThanLandscape = scrollTop < initialPlayerHeight.current - maxLandscapeHeight;
|
||||
|
||||
if (videoNode) {
|
||||
if (isHigherThanLandscape) {
|
||||
if (initialPlayerHeight.current > maxLandscapeHeight) {
|
||||
const result = initialPlayerHeight.current - scrollTop;
|
||||
const amountNeededToCenter = getAmountNeededToCenterVideo(videoNode.offsetHeight, result);
|
||||
|
||||
videoNode.style.top = `${amountNeededToCenter}px`;
|
||||
if (touchOverlay) touchOverlay.style.height = `${result}px`;
|
||||
viewer.style.height = `${result}px`;
|
||||
}
|
||||
} else {
|
||||
if (touchOverlay) touchOverlay.style.height = `${maxLandscapeHeight}px`;
|
||||
viewer.style.height = `${maxLandscapeHeight}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
// clear the added styles on unmount
|
||||
const viewer = document.querySelector(`.${CONTENT_VIEWER_CLASS}`);
|
||||
// $FlowFixMe
|
||||
if (viewer) viewer.style.height = undefined;
|
||||
const touchOverlay = document.querySelector('.vjs-touch-overlay');
|
||||
if (touchOverlay) touchOverlay.removeAttribute('style');
|
||||
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [appDrawerOpen, heightForViewer, isMobilePlayer, mainFilePlaying, maxLandscapeHeight, initialPlayerHeight]);
|
||||
|
||||
// -- render styles --
|
||||
|
||||
// declaring some style objects as variables makes it easier for repeated cases
|
||||
const transparentBackground = {
|
||||
background: videoGreaterThanLandscape && mainFilePlaying && !forceDefaults ? 'transparent !important' : undefined,
|
||||
};
|
||||
const maxHeight = { maxHeight: !videoTheaterMode ? 'var(--desktop-portrait-player-max-height)' : undefined };
|
||||
|
||||
return (
|
||||
<Global
|
||||
styles={{
|
||||
[`.${PRIMARY_PLAYER_WRAPPER_CLASS}`]: {
|
||||
height: !videoTheaterMode && mainFilePlaying ? `${heightResult} !important` : undefined,
|
||||
opacity: !videoTheaterMode && mainFilePlaying ? '0 !important' : undefined,
|
||||
},
|
||||
|
||||
'.file-render--video': {
|
||||
...transparentBackground,
|
||||
...maxHeight,
|
||||
|
||||
video: maxHeight,
|
||||
},
|
||||
'.content__wrapper': transparentBackground,
|
||||
'.video-js': transparentBackground,
|
||||
|
||||
'.vjs-fullscreen': {
|
||||
video: {
|
||||
top: 'unset !important',
|
||||
height: '100% !important',
|
||||
},
|
||||
'.vjs-touch-overlay': {
|
||||
height: '100% !important',
|
||||
maxHeight: 'unset !important',
|
||||
},
|
||||
},
|
||||
|
||||
'.vjs-tech': {
|
||||
opacity: '1',
|
||||
height:
|
||||
isMobilePlayer && ((appDrawerOpen && videoGreaterThanLandscape) || videoGreaterThanLandscape)
|
||||
? 'unset !important'
|
||||
: '100%',
|
||||
position: 'absolute',
|
||||
top: isFloating ? '0px !important' : undefined,
|
||||
},
|
||||
|
||||
[`.${CONTENT_VIEWER_CLASS}`]: {
|
||||
height: !forceDefaults && (!isMobile || isMobilePlayer) ? `${heightResult} !important` : undefined,
|
||||
...maxHeight,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -133,7 +133,8 @@ export default function FileRenderInitiator(props: Props) {
|
|||
}, [collectionId, doUriInitiatePlay, isMarkdownPost, isPlayable, parentCommentId, pathname, uri]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const videoOnPage = document.querySelector('video');
|
||||
// avoid selecting 'video' anymore -> can cause conflicts with Ad popup videos
|
||||
const videoOnPage = document.querySelector('.vjs-tech');
|
||||
|
||||
if (
|
||||
(canViewFile || forceAutoplayParam) &&
|
||||
|
|
|
@ -12,7 +12,7 @@ import FileRenderInitiator from 'component/fileRenderInitiator';
|
|||
import LivestreamScheduledInfo from 'component/livestreamScheduledInfo';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import SwipeableDrawer from 'component/swipeableDrawer';
|
||||
import { DrawerExpandButton } from 'component/swipeableDrawer/view';
|
||||
import DrawerExpandButton from 'component/swipeableDrawerExpand';
|
||||
import LivestreamMenu from 'component/livestreamChatLayout/livestream-menu';
|
||||
import Icon from 'component/common/icon';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
@ -54,7 +54,6 @@ export default function LivestreamLayout(props: Props) {
|
|||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [showChat, setShowChat] = React.useState(undefined);
|
||||
const [superchatsHidden, setSuperchatsHidden] = React.useState(false);
|
||||
const [chatViewMode, setChatViewMode] = React.useState(VIEW_MODES.CHAT);
|
||||
|
||||
|
@ -104,8 +103,6 @@ export default function LivestreamLayout(props: Props) {
|
|||
{isMobile && !hideComments && (
|
||||
<React.Suspense fallback={null}>
|
||||
<SwipeableDrawer
|
||||
open={Boolean(showChat)}
|
||||
toggleDrawer={() => setShowChat(!showChat)}
|
||||
title={
|
||||
<ChatModeSelector
|
||||
superChats={superChats}
|
||||
|
@ -133,7 +130,7 @@ export default function LivestreamLayout(props: Props) {
|
|||
/>
|
||||
</SwipeableDrawer>
|
||||
|
||||
<DrawerExpandButton label={__('Open Live Chat')} toggleDrawer={() => setShowChat(!showChat)} />
|
||||
<DrawerExpandButton label={__('Open Live Chat')} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import SwipeableDrawer from './view';
|
||||
import { selectTheme } from 'redux/selectors/settings';
|
||||
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
||||
import { selectAppDrawerOpen } from 'redux/selectors/app';
|
||||
import { doToggleAppDrawer } from 'redux/actions/app';
|
||||
|
||||
const select = (state) => ({
|
||||
open: selectAppDrawerOpen(state),
|
||||
theme: selectTheme(state),
|
||||
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
||||
});
|
||||
|
||||
export default connect(select)(SwipeableDrawer);
|
||||
const perform = {
|
||||
toggleDrawer: doToggleAppDrawer,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(SwipeableDrawer);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { grey } from '@mui/material/colors';
|
|||
|
||||
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderFloating/view';
|
||||
import { PRIMARY_PLAYER_WRAPPER_CLASS, PRIMARY_IMAGE_WRAPPER_CLASS } from 'page/file/view';
|
||||
import { getMaxLandscapeHeight } from 'component/fileRenderFloating/helper-functions';
|
||||
import { SwipeableDrawer as MUIDrawer } from '@mui/material';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
|
@ -18,21 +19,21 @@ const DRAWER_PULLER_HEIGHT = 42;
|
|||
|
||||
type Props = {
|
||||
children: Node,
|
||||
open: boolean,
|
||||
theme: string,
|
||||
mobilePlayerDimensions?: { height: number },
|
||||
title: any,
|
||||
hasSubtitle?: boolean,
|
||||
actions?: any,
|
||||
// -- redux --
|
||||
open: boolean,
|
||||
theme: string,
|
||||
toggleDrawer: () => void,
|
||||
};
|
||||
|
||||
export default function SwipeableDrawer(props: Props) {
|
||||
const { mobilePlayerDimensions, title, hasSubtitle, children, open, theme, actions, toggleDrawer } = props;
|
||||
const { title, hasSubtitle, children, open, theme, actions, toggleDrawer } = props;
|
||||
|
||||
const [coverHeight, setCoverHeight] = React.useState();
|
||||
|
||||
const videoHeight = (mobilePlayerDimensions && mobilePlayerDimensions.height) || coverHeight || 0;
|
||||
const videoHeight = coverHeight || getMaxLandscapeHeight() || 0;
|
||||
|
||||
const handleResize = React.useCallback(() => {
|
||||
const element =
|
||||
|
@ -47,14 +48,14 @@ export default function SwipeableDrawer(props: Props) {
|
|||
|
||||
React.useEffect(() => {
|
||||
// Drawer will follow the cover image on resize, so it's always visible
|
||||
if (open && (!mobilePlayerDimensions || !mobilePlayerDimensions.height)) {
|
||||
if (open) {
|
||||
handleResize();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
}, [handleResize, mobilePlayerDimensions, open]);
|
||||
}, [handleResize, open]);
|
||||
|
||||
// Reset scroll position when opening: avoid broken position where
|
||||
// the drawer is lower than the video
|
||||
|
@ -94,12 +95,12 @@ export default function SwipeableDrawer(props: Props) {
|
|||
}
|
||||
|
||||
type GlobalStylesProps = {
|
||||
open?: boolean,
|
||||
open: boolean,
|
||||
videoHeight: number,
|
||||
};
|
||||
|
||||
const DrawerGlobalStyles = (globalStylesProps: GlobalStylesProps) => {
|
||||
const { open, videoHeight } = globalStylesProps;
|
||||
const DrawerGlobalStyles = (props: GlobalStylesProps) => {
|
||||
const { open, videoHeight } = props;
|
||||
|
||||
return (
|
||||
<Global
|
||||
|
@ -126,8 +127,8 @@ type PullerProps = {
|
|||
theme: string,
|
||||
};
|
||||
|
||||
const Puller = (pullerProps: PullerProps) => {
|
||||
const { theme } = pullerProps;
|
||||
const Puller = (props: PullerProps) => {
|
||||
const { theme } = props;
|
||||
|
||||
return (
|
||||
<span className="swipeable-drawer__puller" style={{ backgroundColor: theme === 'light' ? grey[300] : grey[800] }} />
|
||||
|
@ -141,8 +142,8 @@ type HeaderProps = {
|
|||
toggleDrawer: () => void,
|
||||
};
|
||||
|
||||
const HeaderContents = (headerProps: HeaderProps) => {
|
||||
const { title, hasSubtitle, actions, toggleDrawer } = headerProps;
|
||||
const HeaderContents = (props: HeaderProps) => {
|
||||
const { title, hasSubtitle, actions, toggleDrawer } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -160,22 +161,3 @@ const HeaderContents = (headerProps: HeaderProps) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ExpandButtonProps = {
|
||||
label: any,
|
||||
toggleDrawer: () => void,
|
||||
};
|
||||
|
||||
export const DrawerExpandButton = (expandButtonProps: ExpandButtonProps) => {
|
||||
const { label, toggleDrawer } = expandButtonProps;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="swipeable-drawer__expand-button"
|
||||
label={label}
|
||||
button="primary"
|
||||
icon={ICONS.CHAT}
|
||||
onClick={toggleDrawer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
9
ui/component/swipeableDrawerExpand/index.js
Normal file
9
ui/component/swipeableDrawerExpand/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToggleAppDrawer } from 'redux/actions/app';
|
||||
import DrawerExpandButton from './view';
|
||||
|
||||
const perform = {
|
||||
onClick: doToggleAppDrawer,
|
||||
};
|
||||
|
||||
export default connect(null, perform)(DrawerExpandButton);
|
15
ui/component/swipeableDrawerExpand/view.jsx
Normal file
15
ui/component/swipeableDrawerExpand/view.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import 'scss/component/_swipeable-drawer.scss';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
label: any,
|
||||
// -- redux --
|
||||
onClick: () => void,
|
||||
};
|
||||
|
||||
export default function DrawerExpandButton(buttonProps: Props) {
|
||||
return <Button className="swipeable-drawer__expand-button" button="primary" icon={ICONS.CHAT} {...buttonProps} />;
|
||||
}
|
|
@ -46,7 +46,7 @@
|
|||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: 80%;
|
||||
height: 5rem;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
|
|
@ -32,7 +32,6 @@ export const TOGGLE_YOUTUBE_SYNC_INTEREST = 'TOGGLE_YOUTUBE_SYNC_INTEREST';
|
|||
export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION';
|
||||
export const SET_ACTIVE_CHANNEL = 'SET_ACTIVE_CHANNEL';
|
||||
export const SET_INCOGNITO = 'SET_INCOGNITO';
|
||||
export const SET_MOBILE_PLAYER_DIMENSIONS = 'SET_MOBILE_PLAYER_DIMENSIONS';
|
||||
export const SET_AD_BLOCKER_FOUND = 'SET_AD_BLOCKER_FOUND';
|
||||
export const RELOAD_REQUIRED = 'RELOAD_REQUIRED';
|
||||
|
||||
|
@ -40,6 +39,7 @@ export const RELOAD_REQUIRED = 'RELOAD_REQUIRED';
|
|||
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
|
||||
export const WINDOW_SCROLLED = 'WINDOW_SCROLLED';
|
||||
export const HISTORY_NAVIGATE = 'HISTORY_NAVIGATE';
|
||||
export const DRAWER_OPENED = 'DRAWER_OPENED';
|
||||
|
||||
// Upgrades
|
||||
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED';
|
||||
|
|
|
@ -16,6 +16,7 @@ import { selectShowMatureContent, selectClientSetting } from 'redux/selectors/se
|
|||
import { makeSelectFileRenderModeForUri, selectContentPositionForUri } from 'redux/selectors/content';
|
||||
import { selectCommentsListTitleForUri, selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import { doToggleAppDrawer } from 'redux/actions/app';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
|
||||
import FilePage from './view';
|
||||
|
@ -53,6 +54,7 @@ const perform = {
|
|||
doSetContentHistoryItem,
|
||||
doSetPrimaryUri,
|
||||
clearPosition,
|
||||
doToggleAppDrawer,
|
||||
};
|
||||
|
||||
export default withRouter(connect(select, perform)(FilePage));
|
||||
|
|
|
@ -15,7 +15,7 @@ import CollectionContent from 'component/collectionContentSidebar';
|
|||
import Button from 'component/button';
|
||||
import Empty from 'component/common/empty';
|
||||
import SwipeableDrawer from 'component/swipeableDrawer';
|
||||
import { DrawerExpandButton } from 'component/swipeableDrawer/view';
|
||||
import DrawerExpandButton from 'component/swipeableDrawerExpand';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
|
||||
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
|
||||
|
@ -49,6 +49,7 @@ type Props = {
|
|||
doSetPrimaryUri: (uri: ?string) => void,
|
||||
clearPosition: (uri: string) => void,
|
||||
doClearPlayingUri: () => void,
|
||||
doToggleAppDrawer: () => void,
|
||||
};
|
||||
|
||||
export default function FilePage(props: Props) {
|
||||
|
@ -76,12 +77,12 @@ export default function FilePage(props: Props) {
|
|||
doSetContentHistoryItem,
|
||||
doSetPrimaryUri,
|
||||
clearPosition,
|
||||
doToggleAppDrawer,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Auto-open the drawer on Mobile view if there is a linked comment
|
||||
const [showComments, setShowComments] = React.useState(linkedCommentId);
|
||||
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const commentSettingDisabled = channelSettings && !channelSettings.comments_enabled;
|
||||
|
@ -99,6 +100,15 @@ export default function FilePage(props: Props) {
|
|||
return durationInSecs ? isVideoTooShort || almostFinishedPlaying : false;
|
||||
}, [audioVideoDuration, fileInfo, position]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (linkedCommentId && isMobile) {
|
||||
doToggleAppDrawer();
|
||||
}
|
||||
// only on mount, otherwise clicking on a comments timestamp and linking it
|
||||
// would trigger the drawer
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// always refresh file info when entering file page to see if we have the file
|
||||
// this could probably be refactored into more direct components now
|
||||
|
@ -231,15 +241,11 @@ export default function FilePage(props: Props) {
|
|||
<Empty {...emptyMsgProps} text={__('This channel has disabled comments on their page.')} />
|
||||
) : isMobile ? (
|
||||
<>
|
||||
<SwipeableDrawer
|
||||
open={Boolean(showComments)}
|
||||
toggleDrawer={() => setShowComments(!showComments)}
|
||||
title={commentsListTitle}
|
||||
>
|
||||
<SwipeableDrawer title={commentsListTitle}>
|
||||
<CommentsList {...commentsListProps} />
|
||||
</SwipeableDrawer>
|
||||
|
||||
<DrawerExpandButton label={commentsListTitle} toggleDrawer={() => setShowComments(!showComments)} />
|
||||
<DrawerExpandButton label={commentsListTitle} />
|
||||
</>
|
||||
) : (
|
||||
<CommentsList {...commentsListProps} />
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
selectUpgradeTimer,
|
||||
selectModal,
|
||||
selectAllowAnalytics,
|
||||
selectAppDrawerOpen,
|
||||
} from 'redux/selectors/app';
|
||||
import { selectDaemonSettings, selectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -669,6 +670,7 @@ export function doHandleSyncComplete(error, hasNewData, syncId) {
|
|||
dispatch(doGetAndPopulatePreferences(syncId));
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error in doHandleSyncComplete', error);
|
||||
}
|
||||
};
|
||||
|
@ -737,14 +739,21 @@ export function doSetIncognito(incognitoEnabled) {
|
|||
};
|
||||
}
|
||||
|
||||
export const doSetMobilePlayerDimensions = ({ height, width }) => ({
|
||||
type: ACTIONS.SET_MOBILE_PLAYER_DIMENSIONS,
|
||||
data: { heightWidth: { height, width } },
|
||||
});
|
||||
|
||||
export function doSetAdBlockerFound(found) {
|
||||
return {
|
||||
type: ACTIONS.SET_AD_BLOCKER_FOUND,
|
||||
data: found,
|
||||
};
|
||||
}
|
||||
|
||||
export function doToggleAppDrawer(open) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const isOpen = selectAppDrawerOpen(state);
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.DRAWER_OPENED,
|
||||
data: !isOpen,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ export type AppState = {
|
|||
interestedInYoutubeSync: boolean,
|
||||
activeChannel: ?string,
|
||||
incognito: boolean,
|
||||
mobilePlayerDimensions?: { height: number, width: number },
|
||||
adBlockerFound: ?boolean, // undefined = unknown; true/false = yes/no;
|
||||
appDrawerOpen: boolean,
|
||||
};
|
||||
|
||||
const defaultState: AppState = {
|
||||
|
@ -87,8 +87,8 @@ const defaultState: AppState = {
|
|||
interestedInYoutubeSync: false,
|
||||
activeChannel: undefined,
|
||||
incognito: false,
|
||||
mobilePlayerDimensions: undefined,
|
||||
adBlockerFound: undefined,
|
||||
appDrawerOpen: false,
|
||||
};
|
||||
|
||||
// @@router comes from react-router
|
||||
|
@ -328,13 +328,6 @@ reducers[ACTIONS.SET_INCOGNITO] = (state, action) => {
|
|||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SET_MOBILE_PLAYER_DIMENSIONS] = (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
mobilePlayerDimensions: action.data.heightWidth,
|
||||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SET_AD_BLOCKER_FOUND] = (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
|
@ -342,6 +335,13 @@ reducers[ACTIONS.SET_AD_BLOCKER_FOUND] = (state, action) => {
|
|||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.DRAWER_OPENED] = (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
appDrawerOpen: action.data,
|
||||
};
|
||||
};
|
||||
|
||||
reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => {
|
||||
const { welcomeVersion, allowAnalytics } = action.data;
|
||||
return {
|
||||
|
|
|
@ -107,5 +107,5 @@ export const selectActiveChannelStakedLevel = (state) => {
|
|||
|
||||
export const selectIncognito = (state) => selectState(state).incognito;
|
||||
|
||||
export const selectMobilePlayerDimensions = (state) => selectState(state).mobilePlayerDimensions;
|
||||
export const selectAdBlockerFound = (state) => selectState(state).adBlockerFound;
|
||||
export const selectAppDrawerOpen = (state) => selectState(state).appDrawerOpen;
|
||||
|
|
|
@ -22,6 +22,12 @@
|
|||
@media (max-width: $breakpoint-small) {
|
||||
max-height: var(--mobile-player-max-height);
|
||||
}
|
||||
|
||||
video {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
max-height: var(--mobile-player-max-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content__viewer--secondary {
|
||||
|
@ -330,7 +336,6 @@
|
|||
border-radius: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
max-height: var(--mobile-player-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,10 +124,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.card__main-actions {
|
||||
// padding: var(--spacing-s) var(--spacing-xxs) !important;
|
||||
}
|
||||
|
||||
.claim-preview--inline {
|
||||
align-items: flex-start;
|
||||
line-height: 1.3;
|
||||
|
@ -203,12 +199,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-page__video-container {
|
||||
max-height: var(--desktop-portrait-player-max-height);
|
||||
}
|
||||
|
||||
.file-render {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
max-height: var(--inline-player-max-height);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
max-height: var(--mobile-player-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
.file-render--video {
|
||||
|
@ -724,6 +727,8 @@ $control-bar-icon-size: 0.8rem;
|
|||
&.skip {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
max-height: var(--mobile-player-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -761,6 +766,10 @@ $control-bar-icon-size: 0.8rem;
|
|||
}
|
||||
|
||||
.video-js.vjs-fullscreen {
|
||||
video {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.vjs-button--theater-mode {
|
||||
display: none;
|
||||
}
|
||||
|
@ -804,12 +813,6 @@ $control-bar-icon-size: 0.8rem;
|
|||
}
|
||||
|
||||
.file-render {
|
||||
.video-js {
|
||||
/*display: flex;*/
|
||||
/*align-items: center;*/
|
||||
/*justify-content: center;*/
|
||||
}
|
||||
|
||||
.vjs-big-play-button {
|
||||
@extend .button--icon;
|
||||
@extend .button--play;
|
||||
|
|
|
@ -566,6 +566,10 @@ body {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.file-page__video-container {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.file-page__recommended {
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
width: 100%;
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
backdrop-filter: blur(4px);
|
||||
background-color: var(--mui-background);
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
|
||||
.button--close {
|
||||
top: 2px !important;
|
||||
|
@ -28,6 +30,12 @@
|
|||
left: calc(50% - 15px);
|
||||
}
|
||||
|
||||
.MuiDrawer-root > .MuiPaper-root {
|
||||
overflow: visible;
|
||||
color: var(--color-text);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.swipeable-drawer__header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -102,16 +110,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swipeable-drawer__expand {
|
||||
border-top: 1px solid var(--color-border);
|
||||
position: fixed;
|
||||
background-color: var(--color-card-background);
|
||||
visibility: visible;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -140,11 +140,6 @@ $control-bar-icon-size: 0.8rem;
|
|||
transition: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.vjs-user-active.vjs-playing {
|
||||
.vjs-control-bar {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button glow
|
||||
|
|
|
@ -76,9 +76,9 @@
|
|||
--header-height-mobile: 56px;
|
||||
|
||||
// Inline Player
|
||||
// --inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 2);
|
||||
--inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 4);
|
||||
--mobile-player-max-height: 50vh;
|
||||
--mobile-player-max-height: 70vh;
|
||||
--desktop-portrait-player-max-height: 80vh;
|
||||
|
||||
// Card
|
||||
--card-radius: var(--border-radius);
|
||||
|
|
|
@ -124,6 +124,17 @@ export function getClaimTitle(claim: ?Claim) {
|
|||
return metadata && metadata.title;
|
||||
}
|
||||
|
||||
export function getClaimVideoInfo(claim: ?Claim) {
|
||||
const metadata = getClaimMetadata(claim);
|
||||
// $FlowFixMe
|
||||
return metadata && metadata.video;
|
||||
}
|
||||
|
||||
export function getVideoClaimAspectRatio(claim: ?Claim) {
|
||||
const { width, height } = getClaimVideoInfo(claim) || {};
|
||||
return width && height ? height / width : undefined;
|
||||
}
|
||||
|
||||
export const isStreamPlaceholderClaim = (claim: ?StreamClaim) => {
|
||||
return claim ? Boolean(claim.value_type === 'stream' && !claim.value.source) : false;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue