Merge pull request #2515 from lbryio/fullscreen

Improve fullscreen mode of the viewer
This commit is contained in:
Sean Yesmunt 2019-06-10 14:43:44 -04:00 committed by GitHub
commit 7fea7239c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 41 deletions

View file

@ -4,6 +4,7 @@ import * as ICONS from 'constants/icons';
import * as React from 'react';
import Button from 'component/button';
import Tooltip from 'component/common/tooltip';
import { requestFullscreen, fullscreenElement } from 'util/full-screen';
type FileInfo = {
claim_id: string,
@ -15,15 +16,32 @@ type Props = {
openModal: (id: string, { uri: string }) => void,
claimIsMine: boolean,
fileInfo: FileInfo,
viewerContainer: React.Ref,
showFullscreen: boolean,
};
class FileActions extends React.PureComponent<Props> {
maximizeViewer = () => {
const { viewerContainer } = this.props;
const isFullscreen = fullscreenElement();
// Request fullscreen if viewer is ready
// And if there is no fullscreen element active
if (!isFullscreen && viewerContainer && viewerContainer.current !== null) {
requestFullscreen(viewerContainer.current);
}
};
render() {
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
const { fileInfo, uri, openModal, claimIsMine, claimId, showFullscreen } = this.props;
const showDelete = claimIsMine || (fileInfo && Object.keys(fileInfo).length > 0);
return (
<React.Fragment>
{showFullscreen && (
<Tooltip onComponent body={__('Full screen (f)')}>
<Button button="alt" description={__('Fullscreen')} icon={ICONS.FULLSCREEN} onClick={this.maximizeViewer} />
</Tooltip>
)}
{showDelete && (
<Tooltip onComponent body={__('Delete this file')}>
<Button

View file

@ -1,14 +1,20 @@
// @flow
import '@babel/polyfill';
import * as React from 'react';
// @if TARGET='app'
import { remote } from 'electron';
import fs from 'fs';
import { remote } from 'electron';
// @endif
import path from 'path';
import player from 'render-media';
import FileRender from 'component/fileRender';
import LoadingScreen from 'component/common/loading-screen';
import { fullscreenElement, requestFullscreen, exitFullscreen } from 'util/full-screen';
// Shorcut key code for fullscreen (f)
const F_KEYCODE = 70;
type Props = {
contentType: string,
@ -23,6 +29,8 @@ type Props = {
onFinishCb: ?() => void,
savePosition: number => void,
changeVolume: number => void,
viewerContainer: React.Ref,
searchBarFocused: boolean,
};
type State = {
@ -69,7 +77,6 @@ class MediaPlayer extends React.PureComponent<Props, State> {
this.mediaContainer = React.createRef();
(this: any).togglePlay = this.togglePlay.bind(this);
(this: any).toggleFullScreen = this.toggleFullScreen.bind(this);
}
componentDidMount() {
@ -107,25 +114,65 @@ class MediaPlayer extends React.PureComponent<Props, State> {
componentWillUnmount() {
const mediaElement = this.mediaContainer.current.children[0];
document.removeEventListener('keydown', this.togglePlay);
document.removeEventListener('keydown', this.handleKeyDown);
if (mediaElement) {
mediaElement.removeEventListener('click', this.togglePlay);
mediaElement.removeEventListener('dbclick', this.handleDoubleClick);
}
}
toggleFullScreen() {
const mediaElement = this.mediaContainer.current;
if (mediaElement) {
// $FlowFixMe
if (document.webkitIsFullScreen) {
// $FlowFixMe
document.webkitExitFullscreen();
} else {
mediaElement.webkitRequestFullScreen();
handleKeyDown = (event: SyntheticKeyboardEvent<*>) => {
const { searchBarFocused } = this.props;
if (!searchBarFocused) {
// Handle fullscreen shortcut key (f)
if (event.keyCode === F_KEYCODE) {
this.toggleFullscreen();
}
// Handle toggle play
// @if TARGET='app'
this.togglePlay(event);
// @endif
}
}
};
handleDoubleClick = (event: SyntheticInputEvent<*>) => {
// Prevent pause / play
event.preventDefault();
event.stopPropagation();
// Trigger fullscreen mode
this.toggleFullscreen();
};
toggleFullscreen = () => {
const { viewerContainer } = this.props;
const isFullscreen = fullscreenElement();
const isSupportedFile = this.isSupportedFile();
const isPlayableType = this.playableType();
if (!isFullscreen) {
// Enter fullscreen mode if content is not playable
// Otherwise it should be handle internally on the video player
// or it will break the toggle fullscreen button
if (!isPlayableType && isSupportedFile && viewerContainer && viewerContainer.current !== null) {
requestFullscreen(viewerContainer.current);
}
// Request fullscreen mode for the media player (renderMedia)
// Don't use this with the new player
// @if TARGET='app'
else if (isPlayableType) {
const mediaContainer = this.mediaContainer.current;
const mediaElement = mediaContainer && mediaContainer.children[0];
if (mediaElement) {
requestFullscreen(mediaElement);
}
}
// @endif
} else {
exitFullscreen();
}
};
async playMedia() {
const container = this.mediaContainer.current;
@ -183,7 +230,6 @@ class MediaPlayer extends React.PureComponent<Props, State> {
);
}
document.addEventListener('keydown', this.togglePlay);
const mediaElement = container.children[0];
if (mediaElement) {
if (position) {
@ -206,7 +252,7 @@ class MediaPlayer extends React.PureComponent<Props, State> {
changeVolume(mediaElement.volume);
});
mediaElement.volume = volume;
mediaElement.addEventListener('dblclick', this.toggleFullScreen);
mediaElement.addEventListener('dblclick', this.handleDoubleClick);
}
// @endif
@ -217,6 +263,9 @@ class MediaPlayer extends React.PureComponent<Props, State> {
this.renderFile();
}
// @endif
// Fullscreen event for web and app
document.addEventListener('keydown', this.handleKeyDown);
}
// @if TARGET='app'

View file

@ -7,8 +7,10 @@ import LoadingScreen from 'component/common/loading-screen';
import PlayButton from './internal/play-button';
const Player = React.lazy(() =>
import(/* webpackChunkName: "player-legacy" */
'./internal/player')
import(
/* webpackChunkName: "player-legacy" */
'./internal/player'
)
);
const SPACE_BAR_KEYCODE = 32;
@ -49,6 +51,8 @@ type Props = {
insufficientCredits: boolean,
nsfw: boolean,
thumbnail: ?string,
isPlayableType: boolean,
viewerContainer: React.Ref,
};
class FileViewer extends React.PureComponent<Props> {
@ -125,9 +129,12 @@ class FileViewer extends React.PureComponent<Props> {
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { searchBarFocused } = this.props;
if (!searchBarFocused && event.keyCode === SPACE_BAR_KEYCODE) {
event.preventDefault(); // prevent page scroll
this.playContent();
if (!searchBarFocused) {
if (event.keyCode === SPACE_BAR_KEYCODE) {
event.preventDefault(); // prevent page scroll
this.playContent();
}
}
}
@ -222,6 +229,8 @@ class FileViewer extends React.PureComponent<Props> {
obscureNsfw,
mediaType,
insufficientCredits,
viewerContainer,
searchBarFocused,
thumbnail,
nsfw,
} = this.props;
@ -257,7 +266,7 @@ class FileViewer extends React.PureComponent<Props> {
const layoverStyle = !shouldObscureNsfw && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {};
return (
<div className={classnames('video', {}, className)}>
<div className={classnames('video', {}, className)} ref={viewerContainer}>
{isPlaying && (
<div className="content__view">
{!isReadyToPlay ? (
@ -282,6 +291,8 @@ class FileViewer extends React.PureComponent<Props> {
onStartCb={this.onFileStartCb}
onFinishCb={this.onFileFinishCb}
playingUri={playingUri}
searchBarFocused={searchBarFocused}
viewerContainer={viewerContainer}
/>
</Suspense>
)}

View file

@ -41,6 +41,7 @@ export const ACCOUNT = 'User';
export const SETTINGS = 'Settings';
export const INVITE = 'Users';
export const FILE = 'File';
export const FULLSCREEN = 'Maximize';
export const OPTIONS = 'Sliders';
export const YES = 'ThumbsUp';
export const NO = 'ThumbsDown';

View file

@ -157,6 +157,13 @@ ipcRenderer.on('window-is-focused', () => {
ipcRenderer.on('devtools-is-opened', () => {
doLogWarningConsoleMessage();
});
// Force exit mode for html5 fullscreen api
// See: https://github.com/electron/electron/issues/18188
remote.getCurrentWindow().on('leave-full-screen', event => {
document.webkitExitFullscreen();
});
// @endif
document.addEventListener('dragover', event => {

View file

@ -62,16 +62,21 @@ class FilePage extends React.Component<Props> {
'application',
];
constructor(props: Props) {
super(props);
(this: any).viewerContainer = React.createRef();
}
componentDidMount() {
const {
uri,
claim,
fetchFileInfo,
fetchCostInfo,
setViewed,
isSubscribed,
claimIsMine,
fetchViewCount,
claim,
} = this.props;
if (isSubscribed) {
@ -92,21 +97,8 @@ class FilePage extends React.Component<Props> {
setViewed(uri);
}
componentWillReceiveProps(nextProps: Props) {
const { fetchFileInfo, uri, setViewed } = this.props;
// @if TARGET='app'
if (nextProps.fileInfo === undefined) {
fetchFileInfo(uri);
}
// @endif
if (uri !== nextProps.uri) {
setViewed(nextProps.uri);
}
}
componentDidUpdate(prevProps: Props) {
const { isSubscribed, claim, uri, fetchViewCount, claimIsMine } = this.props;
const { isSubscribed, claim, uri, fileInfo, setViewed, fetchViewCount, claimIsMine, fetchFileInfo } = this.props;
if (!prevProps.isSubscribed && isSubscribed) {
this.removeFromSubscriptionNotifications();
@ -115,6 +107,16 @@ class FilePage extends React.Component<Props> {
if (prevProps.uri !== uri && claimIsMine) {
fetchViewCount(claim.claim_id);
}
// @if TARGET='app'
if (fileInfo === undefined) {
fetchFileInfo(uri);
}
// @endif
if (prevProps.uri !== uri) {
setViewed(uri);
}
}
removeFromSubscriptionNotifications() {
@ -152,7 +154,9 @@ class FilePage extends React.Component<Props> {
const shouldObscureThumbnail = obscureNsfw && nsfw;
const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = getMediaType(contentType, fileName);
const showFile = PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);
const isPreviewType = PREVIEW_MEDIA_TYPES.includes(mediaType);
const isPlayableType = PLAYABLE_MEDIA_TYPES.includes(mediaType);
const showFile = isPlayableType || isPreviewType;
const speechShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image'].includes(contentType.split('/')[0]);
@ -199,10 +203,12 @@ class FilePage extends React.Component<Props> {
)}
{showFile && (
<FileViewer
insufficientCredits={insufficientCredits}
className="content__embedded"
uri={uri}
className="content__embedded"
mediaType={mediaType}
isPlayableType={isPlayableType}
viewerContainer={this.viewerContainer}
insufficientCredits={insufficientCredits}
/>
)}
{!showFile &&
@ -288,7 +294,12 @@ class FilePage extends React.Component<Props> {
<div className="media__action-group--large">
<FileDownloadLink uri={uri} />
<FileActions uri={uri} claimId={claim.claim_id} />
<FileActions
uri={uri}
claimId={claim.claim_id}
showFullscreen={isPreviewType}
viewerContainer={this.viewerContainer}
/>
</div>
</div>

View file

@ -43,6 +43,11 @@
cursor: pointer;
}
}
&:-webkit-full-screen {
width: 100%;
height: 100%;
}
}
.content__empty {

View file

@ -0,0 +1,49 @@
/*
Polyfill functions for the HTML5 fullscreen api:
https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
const prefixes = {
exitFullscreen: ['exitFullscreen', 'msExitFullscreen', 'mozCancelFullScreen', 'webkitExitFullscreen'],
fullscreenChange: ['fullscreenChange', 'MSFullscreenChange', 'mozfullscreenchange', 'webkitfullscreenchange'],
fullscreenEnabled: ['fullscreenEnabled', 'msFullscreenEnabled', 'mozFullScreenEnabled', 'webkitFullscreenEnabled'],
fullscreenElement: ['fullscreenElement', 'msFullscreenElement', 'mozFullScreenElement', 'webkitFullscreenElement'],
requestFullscreen: ['requestFullscreen', 'msRequestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen'],
};
const getPrefix = () => {
let prefixIndex = 0;
// validate prefix
prefixes.fullscreenEnabled.some((prefix, index) => {
if (document[prefix] || document[prefix] === false) {
prefixIndex = index;
return true;
}
});
// prefix vendor index
return prefixIndex;
};
export const fullscreenElement = () => {
const index = getPrefix();
const prefix = prefixes.fullscreenElement[index];
return document[prefix];
};
export const requestFullscreen = elem => {
const index = getPrefix();
const prefix = prefixes.requestFullscreen[index];
elem[prefix] && elem[prefix]();
};
export const exitFullscreen = () => {
const index = getPrefix();
const prefix = prefixes.exitFullscreen[index];
document[prefix] && document[prefix]();
};
export const onFullscreenChange = (event, callback) => {
const index = getPrefix();
const prefix = prefixes.fullscreenChange[index];
document[`${event}EventListener`](prefix, callback, false);
};