Add dynamic player aspect ratio resizing

This commit is contained in:
Rafael 2022-04-04 09:13:15 -03:00 committed by Thomas Zarebczan
parent 7e65062613
commit 65b9906086
23 changed files with 381 additions and 134 deletions

View file

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

View file

@ -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,
};

View file

@ -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 });
}
}, [doSetMobilePlayerDimensions, doSetPlayingUri, isFloating, isMobile]);
return () => {
// basically if switched videos (playingUrl change or unmount),
// erase the data so it can be re-calculated
if (playingUrl) {
initialPlayerHeight.current = undefined;
}
};
}, [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,
},
}}
/>
);
};

View file

@ -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) &&

View file

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

View file

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

View file

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

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

View 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} />;
}

View file

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

View file

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

View file

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

View file

@ -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} />

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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%;

View file

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

View file

@ -140,11 +140,6 @@ $control-bar-icon-size: 0.8rem;
transition: 0.1s;
}
}
&.vjs-user-active.vjs-playing {
.vjs-control-bar {
}
}
}
// Button glow

View file

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

View file

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