Better Portrait videos support (#892)

This commit is contained in:
Rafael 2022-04-07 23:46:20 +08:00 committed by infinite-persistence
commit bab96276b6
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
25 changed files with 508 additions and 152 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,53 @@ 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;
}
export function getWindowAngle(cb?: () => void) {
// iOS
if (typeof window.orientation === 'number') {
return window.orientation;
}
// Android
if (screen && screen.orientation && screen.orientation.angle) {
return window.orientation;
}
if (cb) cb();
return 0;
}
export function isWindowLandscapeForAngle(angle: number) {
return angle === 90 || angle === 270 || angle === -90;
}

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';
@ -12,13 +17,22 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryChannelName } from 'util/url';
import { useIsMobile } from 'effects/use-screensize';
import { useIsMobile, useIsMobileLandscape, useIsLandscapeScreen } from 'effects/use-screensize';
import debounce from 'util/debounce';
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,14 +103,20 @@ export default function FileRenderFloating(props: Props) {
doUriInitiatePlay,
doSetPlayingUri,
isCurrentClaimLive,
mobilePlayerDimensions,
videoAspectRatio,
geoRestriction,
doSetMobilePlayerDimensions,
appDrawerOpen,
doCommentSocketConnect,
doCommentSocketDisconnect,
} = props;
const isMobile = useIsMobile();
const isTabletLandscape = useIsLandscapeScreen() && !isMobile;
const isLandscapeRotated = useIsMobileLandscape();
const initialMobileState = React.useRef(isMobile);
const initialPlayerHeight = React.useRef();
const resizedBetweenFloating = React.useRef();
const {
location: { state },
@ -103,8 +124,9 @@ export default function FileRenderFloating(props: Props) {
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 +173,18 @@ export default function FileRenderFloating(props: Props) {
x: rect.x,
};
// replace the initial value every time the window is resized if isMobile is true,
// since it could be a portrait -> landscape rotation switch, or if it was a mobile - desktop
// switch, so use the ref to compare the initial state
const resizedEnoughForMobileSwitch = isMobile !== initialMobileState.current;
if (videoAspectRatio && (!initialPlayerHeight.current || isMobile || resizedEnoughForMobileSwitch)) {
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 +249,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 +266,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 +298,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,30 +364,42 @@ 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,
'content__viewer--theater-mode': videoTheaterMode && mainFilePlaying && !isCurrentClaimLive && !isMobile,
'content__viewer--disable-click': wasDragging,
'content__viewer--mobile': isMobile && !playingUriSource,
'content__viewer--mobile': isMobile && !isLandscapeRotated && !playingUriSource,
})}
style={
!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 && !isLandscapeRotated && !isTabletLandscape}
initialPlayerHeight={initialPlayerHeight}
isFloating={isFloating}
fileViewerRect={fileViewerRect}
mainFilePlaying={mainFilePlaying}
isLandscapeRotated={isLandscapeRotated}
isTabletLandscape={isTabletLandscape}
/>
) : null}
<div className={classnames('content__wrapper', { 'content__wrapper--floating': isFloating })}>
{isFloating && (
<Button
@ -398,3 +445,194 @@ export default function FileRenderFloating(props: Props) {
</Draggable>
);
}
type GlobalStylesProps = {
videoAspectRatio: number,
videoTheaterMode: boolean,
appDrawerOpen: boolean,
initialPlayerHeight: ElementRef<any>,
isFloating: boolean,
fileViewerRect: any,
mainFilePlaying: boolean,
isLandscapeRotated: boolean,
isTabletLandscape: boolean,
};
const PlayerGlobalStyles = (props: GlobalStylesProps) => {
const {
videoAspectRatio,
videoTheaterMode,
appDrawerOpen,
initialPlayerHeight,
isFloating,
fileViewerRect,
mainFilePlaying,
isLandscapeRotated,
isTabletLandscape,
} = 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 amountNeededToCenter = getAmountNeededToCenterVideo(heightForViewer, maxLandscapeHeight);
// forceDefaults = no styles should be applied to any of these conditions
// !mainFilePlaying = embeds on markdown (comments or posts)
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 || isLandscapeRotated || isTabletLandscape) 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,
isLandscapeRotated,
isTabletLandscape,
]);
React.useEffect(() => {
if (appDrawerOpen && videoGreaterThanLandscape && isMobilePlayer) {
const videoNode = document.querySelector('.vjs-tech');
if (videoNode) videoNode.style.top = `${amountNeededToCenter}px`;
}
if (isMobile && isFloating) {
const viewer = document.querySelector(`.${CONTENT_VIEWER_CLASS}`);
if (viewer) viewer.removeAttribute('style');
const touchOverlay = document.querySelector('.vjs-touch-overlay');
if (touchOverlay) touchOverlay.removeAttribute('style');
const videoNode = document.querySelector('.vjs-tech');
if (videoNode) videoNode.removeAttribute('style');
}
}, [amountNeededToCenter, appDrawerOpen, isFloating, isMobile, isMobilePlayer, videoGreaterThanLandscape]);
React.useEffect(() => {
if (isTabletLandscape) {
const videoNode = document.querySelector('.vjs-tech');
if (videoNode) videoNode.removeAttribute('style');
const touchOverlay = document.querySelector('.vjs-touch-overlay');
if (touchOverlay) touchOverlay.removeAttribute('style');
}
}, [isTabletLandscape]);
// -- 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 && !isMobile ? '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-touch-overlay': {
maxHeight: isTabletLandscape ? 'var(--desktop-portrait-player-max-height) !important' : undefined,
},
},
'.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 || isLandscapeRotated) && (!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

@ -2,7 +2,7 @@
import 'scss/component/_swipeable-drawer.scss';
import { lazyImport } from 'util/lazyImport';
import { useIsMobile } from 'effects/use-screensize';
import { useIsMobile, useIsMobileLandscape } from 'effects/use-screensize';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import FileTitleSection from 'component/fileTitleSection';
import LivestreamLink from 'component/livestreamLink';
@ -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';
@ -53,8 +53,8 @@ export default function LivestreamLayout(props: Props) {
} = props;
const isMobile = useIsMobile();
const isLandscapeRotated = useIsMobileLandscape();
const [showChat, setShowChat] = React.useState(undefined);
const [superchatsHidden, setSuperchatsHidden] = React.useState(false);
const [chatViewMode, setChatViewMode] = React.useState(VIEW_MODES.CHAT);
@ -101,11 +101,9 @@ export default function LivestreamLayout(props: Props) {
/>
)}
{isMobile && !hideComments && (
{isMobile && !isLandscapeRotated && !hideComments && (
<React.Suspense fallback={null}>
<SwipeableDrawer
open={Boolean(showChat)}
toggleDrawer={() => setShowChat(!showChat)}
title={
<ChatModeSelector
superChats={superChats}
@ -133,7 +131,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

@ -3,7 +3,7 @@ import { lazyImport } from 'util/lazyImport';
import { MAIN_CLASS } from 'constants/classnames';
import { parseURI } from 'util/lbryURI';
import { useHistory } from 'react-router';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import { useIsMobile, useIsMediumScreen, useIsMobileLandscape } from 'effects/use-screensize';
import classnames from 'classnames';
import Header from 'component/header';
import React from 'react';
@ -65,6 +65,7 @@ function Page(props: Props) {
const isMediumScreen = useIsMediumScreen();
const isMobile = useIsMobile();
const isLandscapeRotated = useIsMobileLandscape();
const [sidebarOpen, setSidebarOpen] = usePersistedState('sidebar', false);
const url = pathname.slice(1).replace(/:/g, '#');
@ -143,7 +144,7 @@ function Page(props: Props) {
>
{children}
{!isMobile && (!livestream || !chatDisabled) && rightSide}
{(!isMobile || isLandscapeRotated) && (!livestream || !chatDisabled) && rightSide}
</main>
{!noFooter && (

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

@ -61,9 +61,7 @@ const onPlayerReady = (player, options) => {
!player.el_.ownerDocument.querySelector('.bc-iframe')
) {
player.tech_.el_.setAttribute('playsinline', 'playsinline');
player.tech_.supportsFullScreen = function() {
return false;
};
player.tech_.supportsFullScreen = () => false;
}
const controlBar = player.getChild('ControlBar');
@ -115,7 +113,7 @@ const onPlayerReady = (player, options) => {
screen.orientation.onchange = rotationHandler;
}
player.on('ended', _ => {
player.on('ended', (_) => {
if (locked === true) {
screen.orientation.unlock();
locked = false;
@ -150,14 +148,12 @@ const onPlayerReady = (player, options) => {
* Whether to disable when the video ends (e.g., if there is an endscreen)
* Never shows if the endscreen plugin is present
*/
const mobileUi = function(options) {
function mobileUi(options) {
// if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
if (videojs.browser.IS_ANDROID) {
this.ready(() => {
onPlayerReady(this, videojs.mergeOptions(defaults, options));
});
this.ready(() => onPlayerReady(this, videojs.mergeOptions(defaults, options)));
}
};
}
// Register the plugin with video.js.
registerPlugin('mobileUi', mobileUi);

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

@ -1,6 +1,7 @@
// @flow
// Widths are taken from "ui/scss/init/vars.scss"
import React, { useRef } from 'react';
import { getWindowAngle, isWindowLandscapeForAngle } from 'component/fileRenderFloating/helper-functions';
const DEFAULT_SCREEN_SIZE = 1080;
export function useWindowSize() {
@ -51,6 +52,38 @@ export function useIsMobile() {
return useHasWindowWidthChangedEnough((windowSize) => windowSize < 901);
}
export function useIsMobileLandscape() {
const isMobile = useIsMobile();
const isLandscapeScreen = useIsLandscapeScreen();
return isMobile && isLandscapeScreen;
}
export function useIsLandscapeScreen() {
const isWindowClient = typeof window === 'object';
const windowAngle = getWindowAngle();
const isLandscape = isWindowLandscapeForAngle(windowAngle);
const [landscape, setLandscape] = React.useState<boolean>(isLandscape);
React.useEffect(() => {
function handleResize() {
const currAngle = getWindowAngle();
const isCurrLandscape = isWindowLandscapeForAngle(currAngle);
if (landscape !== isCurrLandscape) {
setLandscape(isCurrLandscape);
}
}
if (isWindowClient) {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}
}, [isWindowClient, landscape]);
return landscape;
}
export function useIsMediumScreen() {
return useHasWindowWidthChangedEnough((windowSize) => windowSize < 1151);
}

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,8 +15,8 @@ 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 { useIsMobile } from 'effects/use-screensize';
import DrawerExpandButton from 'component/swipeableDrawerExpand';
import { useIsMobile, useIsMobileLandscape } from 'effects/use-screensize';
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
const PostViewer = lazyImport(() => import('component/postViewer' /* webpackChunkName: "postViewer" */));
@ -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,11 @@ 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 isLandscapeRotated = useIsMobileLandscape();
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
const commentSettingDisabled = channelSettings && !channelSettings.comments_enabled;
@ -99,6 +99,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
@ -229,17 +238,13 @@ export default function FilePage(props: Props) {
<Empty {...emptyMsgProps} text={__('The creator of this content has disabled comments.')} />
) : commentSettingDisabled ? (
<Empty {...emptyMsgProps} text={__('This channel has disabled comments on their page.')} />
) : isMobile ? (
) : isMobile && !isLandscapeRotated ? (
<>
<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;
};