File Page and Player style changes on mobile view

- Biggest change: Moved mobile player logic outside of fileRenderFloating into its own component fileRenderMobile, since there is no need for all that extra resizing and dragging code (for now, as mobile doesn't have a floating player)
- Moved player to the header height
- Removed rounded borders and margins
This commit is contained in:
Rafael 2022-02-01 17:30:57 -03:00 committed by Thomas Zarebczan
parent 0c47f1daa9
commit 8c3e376873
11 changed files with 366 additions and 26 deletions

View file

@ -15,6 +15,7 @@ import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FileRenderFloating from 'component/fileRenderFloating';
import FileRenderMobile from 'component/fileRenderMobile';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
@ -518,7 +519,7 @@ function App(props: Props) {
<Router />
<ModalRouter />
<React.Suspense fallback={null}>{renderFiledrop && <FileDrop />}</React.Suspense>
<FileRenderFloating />
{isMobile ? <FileRenderMobile /> : <FileRenderFloating />}
<React.Suspense fallback={null}>
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { doUriInitiatePlay } from 'redux/actions/content';
import { selectThumbnailForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
import { selectThumbnailForUri, selectClaimForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import * as SETTINGS from 'constants/settings';
import { selectCostInfoForUri } from 'lbryinc';
@ -14,10 +14,16 @@ import {
makeSelectFileRenderModeForUri,
} from 'redux/selectors/content';
import FileRenderInitiator from './view';
import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
const select = (state, props) => {
const { uri } = props;
const claim = selectClaimForUri(state, uri);
const claimId = claim && claim.claim_id;
const channelClaimId = claim && getChannelIdFromClaim(claim);
return {
claimThumbnail: selectThumbnailForUri(state, uri),
fileInfo: makeSelectFileInfoForUri(uri)(state),
@ -29,11 +35,14 @@ const select = (state, props) => {
renderMode: makeSelectFileRenderModeForUri(uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
authenticated: selectUserVerifiedEmail(state),
activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId),
claimId,
};
};
const perform = {
doUriInitiatePlay,
doSetPlayingUri,
};
export default withRouter(connect(select, perform)(FileRenderInitiator));

View file

@ -3,6 +3,7 @@
// The actual viewer for a file exists in TextViewer and FileRenderFloating
// They can't exist in one component because we need to handle/listen for the start of a new file view
// while a file is currently being viewed
import { useIsMobile } from 'effects/use-screensize';
import React from 'react';
import classnames from 'classnames';
import * as PAGES from 'constants/pages';
@ -30,7 +31,10 @@ type Props = {
claimWasPurchased: boolean,
authenticated: boolean,
videoTheaterMode: boolean,
activeLivestreamForChannel?: any,
claimId?: string,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
doSetPlayingUri: ({ uri: ?string }) => void,
};
export default function FileRenderInitiator(props: Props) {
@ -49,11 +53,16 @@ export default function FileRenderInitiator(props: Props) {
claimWasPurchased,
authenticated,
videoTheaterMode,
activeLivestreamForChannel,
claimId,
doUriInitiatePlay,
doSetPlayingUri,
} = props;
const containerRef = React.useRef<any>();
const isMobile = useIsMobile();
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
const { search, href, state: locationState } = location;
@ -69,11 +78,27 @@ export default function FileRenderInitiator(props: Props) {
const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
const isCurrentClaimLive = activeLivestreamForChannel && claimId && activeLivestreamForChannel.claimId === claimId;
const isMobileClaimLive = isMobile && isCurrentClaimLive;
const foundCover = thumbnail !== FileRenderPlaceholder;
const renderUnsupported = RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode);
const disabled = renderUnsupported || (!fileInfo && insufficientCredits && !claimWasPurchased);
const shouldRedirect = !authenticated && !isFree;
React.useEffect(() => {
// Set livestream as playing uri so it can be rendered by <FileRenderMobile />
// instead of showing an empty cover image. Needs cover to fill the space with the player.
if (isMobileClaimLive && foundCover) {
doSetPlayingUri({ uri });
}
// No floating player on mobile as of now, so clear the playing uri
if (isMobile && (isPlayable || isMobileClaimLive)) {
return () => doSetPlayingUri({ uri: null });
}
}, [doSetPlayingUri, foundCover, isMobile, isMobileClaimLive, isPlayable, uri]);
function doAuthRedirect() {
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
}
@ -128,7 +153,7 @@ export default function FileRenderInitiator(props: Props) {
return (
<div
ref={containerRef}
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
onClick={disabled || isMobileClaimLive ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'content__cover--disabled': disabled,
@ -158,7 +183,7 @@ export default function FileRenderInitiator(props: Props) {
)
)}
{!disabled && (
{!disabled && !isMobileClaimLive && (
<Button
requiresAuth={shouldRedirect}
onClick={viewFile}

View file

@ -0,0 +1,44 @@
import { connect } from 'react-redux';
import { makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import {
makeSelectNextUrlForCollectionAndUrl,
makeSelectPreviousUrlForCollectionAndUrl,
} from 'redux/selectors/collections';
import { selectPlayingUri, makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import { selectCostInfoForUri } from 'lbryinc';
import { doPlayUri } from 'redux/actions/content';
import { withRouter } from 'react-router';
import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
import FileRenderMobile from './view';
const select = (state, props) => {
const playingUri = selectPlayingUri(state);
const uri = playingUri && playingUri.uri;
const collectionId = playingUri && playingUri.collectionId;
const claim = selectClaimForUri(state, uri);
const claimId = claim && claim.claim_id;
const channelClaimId = claim && getChannelIdFromClaim(claim);
return {
uri,
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
costInfo: selectCostInfoForUri(state, uri),
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
nextListUri: collectionId && makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state),
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
collectionId,
activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId),
claimId,
channelClaimId,
};
};
const perform = {
doPlayUri,
};
export default withRouter(connect(select, perform)(FileRenderMobile));

View file

@ -0,0 +1,177 @@
// @flow
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useEffect, useState } from 'react';
import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useHistory } from 'react-router';
import LoadingScreen from 'component/common/loading-screen';
import FileRender from 'component/fileRender';
import AutoplayCountdown from 'component/autoplayCountdown';
import LivestreamIframeRender from 'component/livestreamLayout/iframe-render';
const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container';
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
// ****************************************************************************
// ****************************************************************************
type Props = {
claimId?: string,
uri: string,
streamingUrl?: string,
renderMode: string,
collectionId: string,
costInfo: any,
claimWasPurchased: boolean,
nextListUri: string,
previousListUri: string,
activeLivestreamForChannel?: any,
channelClaimId?: any,
doPlayUri: (string) => void,
};
export default function FileRenderMobile(props: Props) {
const {
claimId,
uri,
streamingUrl,
renderMode,
collectionId,
costInfo,
claimWasPurchased,
nextListUri,
previousListUri,
activeLivestreamForChannel,
channelClaimId,
doPlayUri,
} = props;
const { push } = useHistory();
const [fileViewerRect, setFileViewerRect] = useState();
const [doNavigate, setDoNavigate] = useState(false);
const [playNextUrl, setPlayNextUrl] = useState(true);
const [countdownCanceled, setCountdownCanceled] = useState(false);
const isCurrentClaimLive = activeLivestreamForChannel && activeLivestreamForChannel.claimId === claimId;
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || activeLivestreamForChannel;
const isReadyToPlay = isPlayable && streamingUrl;
const handleResize = React.useCallback(() => {
const element = document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
if (!element) return;
const rect = element.getBoundingClientRect();
// getBoundingClientRect returns a DomRect, not an object
const objectRect = {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.width,
height: rect.height,
// $FlowFixMe
x: rect.x,
};
// $FlowFixMe
setFileViewerRect({ ...objectRect });
}, []);
// Initial resize, will place the player correctly above the cover when starts playing
// (remember the URI here is from playingUri). The cover then keeps on the page and kind of serves as a placeholder
// for the player size and gives the content layered behind the player a "max scroll height"
useEffect(() => {
if (uri) {
handleResize();
setCountdownCanceled(false);
}
}, [handleResize, uri]);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
onFullscreenChange(window, 'add', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
onFullscreenChange(window, 'remove', handleResize);
};
}, [handleResize]);
const doPlay = React.useCallback(
(playUri) => {
setDoNavigate(false);
const navigateUrl = formatLbryUrlForWeb(playUri);
push({
pathname: navigateUrl,
search: collectionId && generateListSearchUrlParams(collectionId),
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
},
[collectionId, push]
);
React.useEffect(() => {
if (!doNavigate) return;
if (playNextUrl && nextListUri) {
doPlay(nextListUri);
} else if (previousListUri) {
doPlay(previousListUri);
}
setPlayNextUrl(true);
}, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]);
if (!isPlayable || !uri || countdownCanceled || (collectionId && !canViewFile && !nextListUri)) {
return null;
}
return (
<div
className="content__viewer content__viewer--inline content__viewer--mobile"
style={
fileViewerRect
? {
width: fileViewerRect.width,
height: fileViewerRect.height,
left: fileViewerRect.x,
}
: {}
}
>
<div className="content__wrapper">
<React.Suspense fallback={<Loading />}>
{isCurrentClaimLive && channelClaimId ? (
<LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion />
) : isReadyToPlay ? (
<FileRender uri={uri} />
) : !canViewFile ? (
<div className="content__loading">
<AutoplayCountdown
nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)}
doReplay={() => doPlayUri(uri)}
doPrevious={() => {
setPlayNextUrl(false);
setDoNavigate(true);
}}
onCanceled={() => setCountdownCanceled(true)}
skipPaid
/>
</div>
) : (
<Loading />
)}
</React.Suspense>
</div>
</div>
);
}
const Loading = () => <LoadingScreen status={__('Loading')} />;

View file

@ -0,0 +1,39 @@
// @flow
import { LIVESTREAM_EMBED_URL } from 'constants/livestream';
import LivestreamScheduledInfo from 'component/livestreamScheduledInfo';
import React from 'react';
import classnames from 'classnames';
type Props = {
channelClaimId: string,
release?: any,
showLivestream: boolean,
showScheduledInfo?: boolean,
mobileVersion?: boolean,
};
export default function LivestreamIframeRender(props: Props) {
const { channelClaimId, release, showLivestream, showScheduledInfo, mobileVersion } = props;
const className = mobileVersion
? 'file-render file-render--video'
: classnames('file-render file-render--video livestream', {
'file-render--scheduledLivestream': !showLivestream,
});
return (
<div className={className}>
<div className="file-viewer">
{showLivestream && (
<iframe
src={`${LIVESTREAM_EMBED_URL}/${channelClaimId}?skin=odysee&autoplay=1`}
scrolling="no"
allowFullScreen
/>
)}
{showScheduledInfo && release && <LivestreamScheduledInfo release={release} />}
</div>
</div>
);
}

View file

@ -1,12 +1,12 @@
// @flow
import { lazyImport } from 'util/lazyImport';
import { LIVESTREAM_EMBED_URL } from 'constants/livestream';
import { useIsMobile } from 'effects/use-screensize';
import classnames from 'classnames';
import FileTitleSection from 'component/fileTitleSection';
import LivestreamLink from 'component/livestreamLink';
import LivestreamScheduledInfo from 'component/livestreamScheduledInfo';
import React from 'react';
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import FileRenderInitiator from 'component/fileRenderInitiator';
import LivestreamIframeRender from './iframe-render';
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
@ -42,23 +42,21 @@ export default function LivestreamLayout(props: Props) {
return (
<>
<div className="section card-stack">
<div
className={classnames('file-render file-render--video livestream', {
'file-render--scheduledLivestream': !showLivestream,
})}
>
<div className="file-viewer">
{showLivestream && (
<iframe
src={`${LIVESTREAM_EMBED_URL}/${channelClaimId}?skin=odysee&autoplay=1`}
scrolling="no"
allowFullScreen
/>
)}
{showScheduledInfo && <LivestreamScheduledInfo release={release} />}
</div>
</div>
<React.Suspense fallback={null}>
{isMobile && isCurrentClaimLive ? (
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
{/* Mobile needs to handle the livestream player like any video player */}
<FileRenderInitiator uri={uri} />
</div>
) : (
<LivestreamIframeRender
channelClaimId={channelClaimId}
release={release}
showLivestream={showLivestream}
showScheduledInfo={showScheduledInfo}
/>
)}
</React.Suspense>
{hideComments && !showScheduledInfo && (
<div className="help--notice">

View file

@ -4,6 +4,12 @@
top: var(--spacing-s);
}
.content__viewer--mobile {
border-radius: 0;
position: fixed;
top: var(--header-height-mobile);
}
.content__viewer--disable-click {
pointer-events: none;
}
@ -11,6 +17,10 @@
.content__viewer--inline {
max-height: var(--inline-player-max-height);
border: none;
@media (max-width: $breakpoint-small) {
max-height: var(--mobile-player-max-height);
}
}
.content__viewer--secondary {
@ -127,6 +137,13 @@
width: 100%;
height: 100%;
}
@media (max-width: $breakpoint-small) {
border-radius: 0;
border: none;
margin: 0;
max-height: var(--mobile-player-max-height);
}
}
.content__cover--text {

View file

@ -24,6 +24,10 @@ $recent-msg-button__height: 2rem;
padding: 0;
}
}
@media (max-width: $breakpoint-small) {
margin: 0 !important;
}
}
.livestream__chat--popout {

View file

@ -217,6 +217,19 @@
padding: var(--spacing-xs);
flex-direction: column;
padding-top: 0;
margin: 0;
padding: 0;
.file-page__secondary-content {
margin: 0;
padding: 0;
}
.card {
border-radius: 0;
margin-bottom: var(--spacing-xxs) !important;
padding: 0;
}
}
}
@ -297,7 +310,19 @@
}
}
@media (max-width: $breakpoint-medium) {
@media (max-width: $breakpoint-small) {
padding: 0;
.card {
margin: 0;
}
.card-stack {
margin: 0;
}
}
@media (min-width: $breakpoint-small) and (max-width: $breakpoint-medium) {
padding: 0 var(--spacing-m);
}
}

View file

@ -72,6 +72,7 @@
// Inline Player
--inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 4);
--mobile-player-max-height: 50vh;
// Card
--card-radius: var(--border-radius);