Merge pull request #2707 from lbryio/not-streaming-but-streaming

kill render-media (range-requests)
This commit is contained in:
Sean Yesmunt 2019-08-13 13:48:52 -04:00 committed by GitHub
commit 1ce826fb4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1063 additions and 1732 deletions

View file

@ -1,27 +0,0 @@
// flow-typed signature: 405ae1983603e8018c018978697f94de
// flow-typed version: 578dff53f6/mime_v2.x.x/flow_>=v0.25.x
declare type $npm$mime$TypeMap = {[mime: string]: Array<string>};
declare class $npm$mime$Mime {
constructor(...typeMap: Array<$npm$mime$TypeMap>): void;
define(typeMap: $npm$mime$TypeMap, force?: boolean): void;
getExtension(mime: string): ?string;
getType(path: string): ?string;
}
declare module 'mime' {
declare type TypeMap = $npm$mime$TypeMap;
declare module.exports: $npm$mime$Mime;
}
declare module 'mime/lite' {
declare type TypeMap = $npm$mime$TypeMap;
declare module.exports: $npm$mime$Mime;
}
declare module 'mime/Mime' {
declare type TypeMap = $npm$mime$TypeMap;
declare module.exports: typeof $npm$mime$Mime;
}

View file

@ -1,3 +0,0 @@
declare module 'render-media' {
declare module.exports: any;
}

View file

@ -125,14 +125,13 @@
"jsmediatags": "^3.8.1",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#8f12baa88f6f057eb3b7d0cf04d6e4bb0eb11763",
"lbry-redux": "lbryio/lbry-redux#05e70648e05c51c51710f6dd698a8e2219b54df2",
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",
"make-runnable": "^1.3.6",
"mammoth": "^1.4.6",
"mime": "^2.3.1",
"moment": "^2.22.0",
"node-abi": "^2.5.1",
"node-fetch": "^2.3.0",
@ -149,6 +148,7 @@
"rc-progress": "^2.0.6",
"react": "^16.8.2",
"react-dom": "^16.8.2",
"react-draggable": "^3.3.0",
"react-ga": "^2.5.7",
"react-hot-loader": "^4.11.1",
"react-modal": "^3.1.7",
@ -169,7 +169,6 @@
"remark-attr": "^0.8.3",
"remark-emoji": "^2.0.1",
"remark-react": "^4.0.3",
"render-media": "^3.1.0",
"reselect": "^3.0.0",
"sass-loader": "^7.1.0",
"semver": "^5.3.0",

View file

@ -1,6 +1,6 @@
import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux';
import { doUpdateBlockHeight, doError, doFetchTransactions } from 'lbry-redux';
import { doError, doFetchTransactions } from 'lbry-redux';
import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken, selectAccessToken } from 'lbryinc';
import { selectThemePath } from 'redux/selectors/settings';
import App from './view';
@ -13,7 +13,6 @@ const select = state => ({
const perform = dispatch => ({
alertError: errorList => dispatch(doError(errorList)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
fetchRewards: () => dispatch(doRewardList()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchTransactions: () => dispatch(doFetchTransactions()),

View file

@ -1,7 +1,7 @@
// @flow
import React, { useEffect, useRef } from 'react';
import analytics from 'analytics';
import { Lbry } from 'lbry-redux';
import { Lbry, buildURI } from 'lbry-redux';
import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal';
@ -10,6 +10,8 @@ import Header from 'component/header';
import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router';
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -20,6 +22,7 @@ type Props = {
theme: string,
accessToken: ?string,
user: ?{ id: string, has_verified_email: boolean },
location: { pathname: string },
fetchRewards: () => void,
fetchRewardedContent: () => void,
fetchTransactions: () => void,
@ -33,6 +36,18 @@ function App(props: Props) {
const userId = user && user.id;
const hasVerifiedEmail = user && user.has_verified_email;
const { pathname } = props.location;
const urlParts = pathname.split('/');
const claimName = urlParts[1];
const claimId = urlParts[2];
// @routingfixme
// claimName and claimId come from the url `{domain}/{claimName}/{claimId}"
let uri;
try {
uri = buildURI({ contentName: claimName, claimId: claimId });
} catch (e) {}
useEffect(() => {
ReactModal.setAppElement(appRef.current);
fetchAccessToken();
@ -42,7 +57,7 @@ function App(props: Props) {
fetchRewards();
fetchTransactions();
// @endif
}, [fetchRewards, fetchRewardedContent, fetchTransactions]);
}, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken]);
useEffect(() => {
// $FlowFixMe
@ -73,9 +88,11 @@ function App(props: Props) {
</div>
<ModalRouter />
<FileViewer pageUri={uri} />
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
</div>
);
}
export default App;
export default withRouter(App);

View file

@ -127,7 +127,12 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
title={title}
aria-label={description || label || title}
className={combinedClassName}
onClick={onClick}
onClick={e => {
if (onClick) {
e.stopPropagation();
onClick(e);
}
}}
disabled={disabled}
type={type}
{...otherProps}

View file

@ -12,7 +12,7 @@ import {
selectChannelIsBlocked,
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import ClaimPreview from './view';
@ -20,7 +20,7 @@ import ClaimPreview from './view';
const select = (state, props) => ({
pending: makeSelectClaimIsPending(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
obscureNsfw: !selectShowMatureContent(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),

View file

@ -1,5 +1,5 @@
// @flow
import React, { Fragment, useEffect } from 'react';
import React, { Fragment, useEffect, forwardRef } from 'react';
import classnames from 'classnames';
import { parseURI, convertToShareLink } from 'lbry-redux';
import { withRouter } from 'react-router-dom';
@ -46,7 +46,7 @@ type Props = {
isSubscribed: boolean,
};
function ClaimPreview(props: Props) {
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
const {
obscureNsfw,
claimIsMine,
@ -150,6 +150,7 @@ function ClaimPreview(props: Props) {
return (
<li
ref={ref}
role="link"
onClick={pending || type === 'inline' ? undefined : onClick}
onContextMenu={handleContextMenu}
@ -209,6 +210,6 @@ function ClaimPreview(props: Props) {
</div>
</li>
);
}
});
export default withRouter(ClaimPreview);

View file

@ -276,4 +276,11 @@ export const icons = {
<polyline points="17 6 23 6 23 12" />
</g>
),
[ICONS.VIEW]: buildIcon(
<g>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</g>
),
};

View file

@ -3,7 +3,7 @@ import React from 'react';
import Spinner from 'component/spinner';
type Props = {
status: string,
status?: string,
spinner: boolean,
};

View file

@ -1,6 +1,7 @@
// @flow
import type { Node } from 'react';
import { Lbryio } from 'lbryinc';
import * as React from 'react';
import React, { Fragment } from 'react';
import Yrbl from 'component/yrbl';
import Button from 'component/button';
import { withRouter } from 'react-router';
@ -8,7 +9,7 @@ import Native from 'native';
import { Lbry } from 'lbry-redux';
type Props = {
children: React.Node,
children: Node,
history: {
replace: string => void,
},
@ -73,7 +74,7 @@ class ErrorBoundary extends React.Component<Props, State> {
type="sad"
title={__('Aw shucks!')}
subtitle={
<div>
<Fragment>
<p>
{__("There was an error. It's been reported and will be fixed")}. {__('Try')}{' '}
<Button
@ -84,7 +85,7 @@ class ErrorBoundary extends React.Component<Props, State> {
/>{' '}
{__('to fix it')}.
</p>
</div>
</Fragment>
}
/>
</div>

View file

@ -1,53 +1,26 @@
// @flow
import type { ElementRef } from 'react';
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import 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,
};
type Props = {
uri: string,
claimId: string,
openModal: (id: string, { uri: string }) => void,
claimIsMine: boolean,
fileInfo: FileInfo,
viewerContainer: { current: ElementRef<any> },
showFullscreen: boolean,
fileInfo: FileListItem,
};
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, showFullscreen } = this.props;
const showDelete = claimIsMine || (fileInfo && Object.keys(fileInfo).length > 0);
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
const showDelete =
claimIsMine ||
(fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed === fileInfo.blobs_in_stream));
return (
<React.Fragment>
{showFullscreen && (
<Tooltip label={__('Full screen (f)')}>
<Button
button="link"
description={__('Fullscreen')}
icon={ICONS.FULLSCREEN}
onClick={this.maximizeViewer}
/>
</Tooltip>
)}
{showDelete && (
<Tooltip label={__('Remove from your library')}>
<Button

View file

@ -30,10 +30,11 @@ class FileDetails extends PureComponent<Props> {
: fileInfo && fileInfo.download_path && formatBytes(fileInfo.written_bytes);
let downloadPath = fileInfo && fileInfo.download_path ? path.normalize(fileInfo.download_path) : null;
let downloadNote;
// If the path is blank, file is not avialable. Create path from name so the folder opens on click.
if (fileInfo && fileInfo.download_path === null) {
// If the path is blank, file is not avialable. Streamed files won't have any blobs saved
// Create path from name so the folder opens on click.
if (fileInfo && fileInfo.blobs_completed >= 1 && fileInfo.download_path === null) {
downloadPath = `${fileInfo.download_directory}/${fileInfo.file_name}`;
downloadNote = 'This file may have been moved or deleted';
downloadNote = 'This file may have been streamed, moved or deleted';
}
return (

View file

@ -3,29 +3,23 @@ import {
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
makeSelectClaimForUri,
makeSelectClaimIsMine,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { doOpenModal } from 'redux/actions/app';
import { doPurchaseUri, doStartDownload, doSetPlayingUri } from 'redux/actions/content';
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
import FileDownloadLink from './view';
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
/* availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix */
downloading: makeSelectDownloadingForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
loading: makeSelectLoadingForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
pause: () => dispatch(doSetPlayingUri(null)),
download: uri => dispatch(doPlayUri(uri, false, true)),
});
export default connect(

View file

@ -4,103 +4,57 @@ import * as MODALS from 'constants/modal_types';
import React from 'react';
import Button from 'component/button';
import ToolTip from 'component/common/tooltip';
import analytics from 'analytics';
type Props = {
claim: StreamClaim,
claimIsMine: boolean,
uri: string,
claimIsMine: boolean,
downloading: boolean,
fileInfo: ?{
written_bytes: number,
total_bytes: number,
outpoint: number,
download_path: string,
completed: boolean,
status: string,
},
loading: boolean,
costInfo: ?{},
restartDownload: (string, number) => void,
isStreamable: boolean,
fileInfo: ?FileListItem,
openModal: (id: string, { path: string }) => void,
purchaseUri: string => void,
pause: () => void,
download: string => void,
};
class FileDownloadLink extends React.PureComponent<Props> {
componentDidMount() {
const { fileInfo, uri, restartDownload } = this.props;
if (
fileInfo &&
!fileInfo.completed &&
fileInfo.status === 'running' &&
fileInfo.written_bytes !== false &&
fileInfo.written_bytes < fileInfo.total_bytes
) {
// This calls file list to show the percentage
restartDownload(uri, fileInfo.outpoint);
}
function FileDownloadLink(props: Props) {
const { fileInfo, downloading, loading, openModal, pause, claimIsMine, download, uri } = props;
if (loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
const label =
fileInfo && fileInfo.written_bytes > 0
? __('Downloading: ') + progress.toFixed(0) + __('% complete')
: __('Connecting...');
return <span>{label}</span>;
}
uri: ?string;
render() {
const {
fileInfo,
downloading,
uri,
openModal,
purchaseUri,
costInfo,
loading,
pause,
claim,
claimIsMine,
} = this.props;
if (loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
const label = fileInfo ? __('Downloading: ') + progress.toFixed(0) + __('% complete') : __('Connecting...');
return <span>{label}</span>;
} else if ((fileInfo === null && !downloading) || (fileInfo && !fileInfo.download_path)) {
if (!costInfo) {
return null;
}
return (
<ToolTip label={__('Add to your library')}>
<Button
button="link"
icon={ICONS.DOWNLOAD}
onClick={() => {
purchaseUri(uri);
const { name, claim_id: claimId, nout, txid } = claim;
// // ideally outpoint would exist inside of claim information
// // we can use it after https://github.com/lbryio/lbry/issues/1306 is addressed
const outpoint = `${txid}:${nout}`;
analytics.apiLogView(`${name}#${claimId}`, outpoint, claimId);
}}
/>
</ToolTip>
);
} else if (fileInfo && fileInfo.download_path) {
return (
<ToolTip label={__('Open file')}>
<Button
button="link"
icon={ICONS.EXTERNAL}
onClick={() => {
pause();
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { path: fileInfo.download_path, isMine: claimIsMine });
}}
/>
</ToolTip>
);
}
return null;
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
return (
<ToolTip label={__('Open file')}>
<Button
button="link"
icon={ICONS.EXTERNAL}
onClick={() => {
pause();
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { path: fileInfo.download_path, isMine: claimIsMine });
}}
/>
</ToolTip>
);
} else {
return (
<ToolTip label={__('Add to your library')}>
<Button
button="link"
icon={ICONS.DOWNLOAD}
onClick={() => {
download(uri);
}}
/>
</ToolTip>
);
}
}

View file

@ -1,11 +1,26 @@
import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectContentTypeForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectDownloadPathForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import { THEME } from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import FileRender from './view';
const select = state => ({
const select = (state, props) => ({
currentTheme: makeSelectClientSetting(THEME)(state),
claim: makeSelectClaimForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
fileName: makeSelectFileNameForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
});
export default connect(select)(FileRender);

View file

@ -3,19 +3,10 @@ import { remote } from 'electron';
import React, { Suspense } from 'react';
import LoadingScreen from 'component/common/loading-screen';
import VideoViewer from 'component/viewers/videoViewer';
// Audio player on hold until the current player is dropped
// This component is half working
// const AudioViewer = React.lazy<*>(() =>
// import(
// /* webpackChunkName: "audioViewer" */
// 'component/viewers/audioViewer'
// )
// );
// const AudioViewer = React.lazy<*>(() =>
// import(/* webpackChunkName: "audioViewer" */
// 'component/viewers/audioViewer')
// );
import ImageViewer from 'component/viewers/imageViewer';
import AppViewer from 'component/viewers/appViewer';
import path from 'path';
import fs from 'fs';
const DocumentViewer = React.lazy<*>(() =>
import(
@ -62,18 +53,14 @@ const ThreeViewer = React.lazy<*>(() =>
// @endif
type Props = {
uri: string,
mediaType: string,
poster?: string,
streamingUrl: string,
contentType: string,
claim: StreamClaim,
source: {
stream: string => void,
fileName: string,
fileType: string,
contentType: string,
downloadPath: string,
url: ?string,
},
currentTheme: string,
downloadPath: string,
fileName: string,
};
class FileRender extends React.PureComponent<Props> {
@ -91,41 +78,6 @@ class FileRender extends React.PureComponent<Props> {
window.removeEventListener('keydown', this.escapeListener, true);
}
// This should use React.createRef()
processSandboxRef(element: any) {
if (!element) {
return;
}
window.sandbox = element;
element.addEventListener('permissionrequest', e => {
console.log('permissionrequest', e);
});
element.addEventListener('console-message', (e: { message: string }) => {
if (/^\$LBRY_IPC:/.test(e.message)) {
// Process command
let message = {};
try {
// $FlowFixMe
message = JSON.parse(/^\$LBRY_IPC:(.*)/.exec(e.message)[1]);
} catch (err) {}
console.log('IPC', message);
} else {
console.log('Sandbox:', e.message);
}
});
element.addEventListener('enter-html-full-screen', () => {
// stub
});
element.addEventListener('leave-html-full-screen', () => {
// stub
});
}
escapeListener(e: SyntheticKeyboardEvent<*>) {
if (e.keyCode === 27) {
e.preventDefault();
@ -141,10 +93,12 @@ class FileRender extends React.PureComponent<Props> {
}
renderViewer() {
const { source, mediaType, currentTheme, poster, claim } = this.props;
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
const fileType = fileName && path.extname(fileName).substring(1);
// Extract relevant data to render file
const { stream, fileType, contentType, downloadPath, fileName } = source;
// Ideally the lbrytv api server would just replace the streaming_url returned by the sdk so we don't need this check
// https://github.com/lbryio/lbrytv/issues/51
const source = IS_WEB ? `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream` : streamingUrl;
// Human-readable files (scripts and plain-text files)
const readableFiles = ['text', 'document', 'script'];
@ -154,42 +108,40 @@ class FileRender extends React.PureComponent<Props> {
// @if TARGET='app'
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
application: <AppViewer uri={uri} />,
// @endif
application: !source.url ? null : (
<webview
ref={element => this.processSandboxRef(element)}
title=""
sandbox="allow-scripts allow-forms allow-pointer-lock"
src={source.url}
autosize="on"
style={{ border: 0, width: '100%', height: '100%' }}
useragent="Mozilla/5.0 AppleWebKit/537 Chrome/60 Safari/537"
enableremotemodule="false"
webpreferences="sandbox=true,contextIsolation=true,webviewTag=false,enableRemoteModule=false,devTools=false"
/>
),
video: (
<VideoViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} poster={poster} />
),
audio: <VideoViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} />,
video: <VideoViewer uri={uri} source={source} contentType={contentType} />,
audio: <VideoViewer uri={uri} source={source} contentType={contentType} />,
image: <ImageViewer uri={uri} source={source} />,
// Add routes to viewer...
};
// Supported fileType
const fileTypes = {
// @if TARGET='app'
pdf: <PdfViewer source={downloadPath} />,
docx: <DocxViewer source={downloadPath} />,
html: <HtmlViewer source={downloadPath} />,
// @endif
// Add routes to viewer...
};
// Check for a valid fileType or mediaType
let viewer = fileTypes[fileType] || mediaTypes[mediaType];
let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType];
// Check for Human-readable files
if (!viewer && readableFiles.includes(mediaType)) {
viewer = <DocumentViewer source={{ stream, fileType, contentType }} theme={currentTheme} />;
viewer = (
<DocumentViewer
source={{
stream: options => fs.createReadStream(downloadPath, options),
fileType,
contentType,
}}
theme={currentTheme}
/>
);
}
// @if TARGET='web'
@ -204,7 +156,7 @@ class FileRender extends React.PureComponent<Props> {
// @endif
// Message Error
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
const unsupportedMessage = __("Sorry, we can't preview this file.");
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
// Return viewer

View file

@ -1,55 +1,42 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { doChangeVolume, doChangeMute } from 'redux/actions/app';
import { selectVolume, selecetMute } from 'redux/selectors/app';
import { doPlayUri, doSetPlayingUri, savePosition } from 'redux/actions/content';
import { doClaimEligiblePurchaseRewards, makeSelectCostInfoForUri } from 'lbryinc';
import {
makeSelectMetadataForUri,
makeSelectContentTypeForUri,
makeSelectClaimForUri,
makeSelectFileInfoForUri,
makeSelectLoadingForUri,
makeSelectDownloadingForUri,
makeSelectFirstRecommendedFileForUri,
makeSelectClaimIsNsfw,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectUriIsStreamable,
makeSelectTitleForUri,
} from 'lbry-redux';
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
import { selectPlayingUri, makeSelectContentPositionForUri } from 'redux/selectors/content';
import { selectFileInfoErrors } from 'redux/selectors/file_info';
import { makeSelectIsPlaying, makeSelectShouldObscurePreview, selectPlayingUri } from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { withRouter } from 'react-router';
import FileViewer from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
isLoading: makeSelectLoadingForUri(props.uri)(state),
isDownloading: makeSelectDownloadingForUri(props.uri)(state),
playingUri: selectPlayingUri(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
volume: selectVolume(state),
position: makeSelectContentPositionForUri(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
fileInfoErrors: selectFileInfoErrors(state),
nextFileToPlay: makeSelectFirstRecommendedFileForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
muted: selecetMute(state),
});
const select = (state, props) => {
const uri = selectPlayingUri(state);
return {
uri,
title: makeSelectTitleForUri(uri)(state),
thumbnail: makeSelectThumbnailForUri(uri)(state),
mediaType: makeSelectMediaTypeForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
isPlaying: makeSelectIsPlaying(uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
isStreamable: makeSelectUriIsStreamable(uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
};
};
const perform = dispatch => ({
play: uri => dispatch(doPlayUri(uri)),
cancelPlay: () => dispatch(doSetPlayingUri(null)),
changeVolume: volume => dispatch(doChangeVolume(volume)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
changeMute: muted => dispatch(doChangeMute(muted)),
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
});
export default connect(
select,
perform
)(FileViewer);
export default withRouter(
connect(
select,
perform
)(FileViewer)
);

View file

@ -1,34 +0,0 @@
// @flow
import classnames from 'classnames';
import React from 'react';
import Button from 'component/button';
type Props = {
play: (SyntheticInputEvent<*>) => void,
isLoading: boolean,
mediaType: string,
fileInfo: ?{},
};
class VideoPlayButton extends React.PureComponent<Props> {
render() {
const { fileInfo, mediaType, isLoading, play } = this.props;
const disabled = isLoading || fileInfo === undefined;
const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1;
const label = doesPlayback ? __('Play') : __('View');
return (
<Button
disabled={disabled}
iconSize={30}
title={label}
className={classnames('button--icon', {
'button--play': doesPlayback,
'button--view': !doesPlayback,
})}
onClick={play}
/>
);
}
}
export default VideoPlayButton;

View file

@ -1,437 +0,0 @@
// @flow
import type { ElementRef } from 'react';
import '@babel/polyfill';
import * as React from 'react';
// @if TARGET='app'
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 detectTyping from 'util/detect-typing';
import { fullscreenElement, requestFullscreen, exitFullscreen } from 'util/full-screen';
// Shorcut key code for fullscreen (f)
const F_KEYCODE = 70;
type Props = {
contentType: string,
mediaType: string,
downloadCompleted: boolean,
volume: number,
position: ?number,
downloadPath: string,
fileName: string,
claim: StreamClaim,
onStartCb: ?() => void,
onFinishCb: ?() => void,
savePosition: number => void,
changeVolume: number => void,
viewerContainer: { current: ElementRef<any> },
changeMute: boolean => void,
muted: boolean,
};
type State = {
hasMetadata: boolean,
unplayable: boolean,
fileSource: ?{
url?: string,
fileName?: string,
contentType?: string,
downloadPath?: string,
fileType?: string,
// Just using `any` because flow isn't working with `fs.createReadStream`
stream?: ({}) => any,
},
};
class MediaPlayer extends React.PureComponent<Props, State> {
static SANDBOX_TYPES = ['application/x-lbry', 'application/x-ext-lbry'];
static FILE_MEDIA_TYPES = [
'text',
'script',
'e-book',
'comic-book',
'document',
'3D-file',
// @if TARGET='web'
'video',
'audio',
// @endif
];
static SANDBOX_SET_BASE_URL = 'http://localhost:5278/set/';
static SANDBOX_CONTENT_BASE_URL = 'http://localhost:5278';
mediaContainer: { current: React.ElementRef<any> };
constructor(props: Props) {
super(props);
this.state = {
hasMetadata: false,
unplayable: false,
fileSource: null,
};
this.mediaContainer = React.createRef();
(this: any).togglePlay = this.togglePlay.bind(this);
}
componentDidMount() {
this.playMedia();
// Temp hack to force the video to play if the metadataloaded event was never fired
// Will be removed with the new video player
// Unoptimized MP4s will fail to render.
// Note: Don't use this for non-playable files
// @if TARGET='app'
setTimeout(() => {
const { hasMetadata } = this.state;
const isPlayableType = this.playableType();
if (!hasMetadata && isPlayableType) {
this.refreshMetadata();
this.playMedia();
}
}, 5000);
// @endif
// Register handler for custom shortcut keys
document.addEventListener('keydown', this.handleKeyDown);
}
componentDidUpdate(prevProps: Props) {
const { fileSource } = this.state;
const { downloadCompleted } = this.props;
// Attemp to render a non-playable file once download is completed
if (prevProps.downloadCompleted !== downloadCompleted) {
const isFileType = this.isSupportedFile();
if (isFileType && !fileSource && downloadCompleted) {
this.playMedia();
}
}
}
componentWillUnmount() {
const mediaElement = this.mediaContainer.current.children[0];
if (mediaElement) {
mediaElement.removeEventListener('click', this.togglePlay);
mediaElement.removeEventListener('dbclick', this.handleDoubleClick);
}
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown = (event: KeyboardEvent) => {
if (!detectTyping()) {
// 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;
const {
downloadCompleted,
changeVolume,
volume,
position,
onFinishCb,
savePosition,
downloadPath,
fileName,
muted,
changeMute,
} = this.props;
// @if TARGET='app'
const renderMediaCallback = error => {
if (error) this.setState({ unplayable: true });
};
// Handle fullscreen change for the Windows platform
const win32FullScreenChange = () => {
const win = remote.BrowserWindow.getFocusedWindow();
if (process.platform === 'win32') {
// $FlowFixMe
win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu());
}
};
// Render custom viewer: FileRender
if (this.isSupportedFile()) {
if (downloadCompleted) {
this.renderFile();
}
} else {
// Render default viewer: render-media (video, audio, img, iframe)
const currentMediaContainer = this.mediaContainer.current;
// Clean any potential rogue instances
while (currentMediaContainer.firstChild) {
currentMediaContainer.removeChild(currentMediaContainer.firstChild);
}
// A slight delay is a hacky way to improve support for videos that aren't web-optimized
// Works... slightly better than not having it ¯\_()_/¯
await this.sleep(400);
player.append(
{
name: fileName,
createReadStream: opts => fs.createReadStream(downloadPath, opts),
},
container,
{ autoplay: true, controls: true },
renderMediaCallback.bind(this)
);
}
const mediaElement = container.children[0];
if (mediaElement) {
if (position) {
mediaElement.currentTime = position;
}
mediaElement.addEventListener('loadedmetadata', () => this.refreshMetadata(), {
once: true,
});
mediaElement.addEventListener('timeupdate', () => savePosition(mediaElement.currentTime));
mediaElement.addEventListener('click', this.togglePlay);
mediaElement.addEventListener('ended', () => {
if (onFinishCb) {
onFinishCb();
}
savePosition(0);
});
mediaElement.addEventListener('webkitfullscreenchange', win32FullScreenChange.bind(this));
mediaElement.addEventListener('volumechange', () => {
changeMute(mediaElement.muted);
changeVolume(mediaElement.volume);
});
mediaElement.volume = volume;
mediaElement.muted = muted;
mediaElement.addEventListener('dblclick', this.handleDoubleClick);
}
// @endif
// On the web, we have viewers for every file like normal people
// @if TARGET='web'
if (this.isSupportedFile()) {
this.renderFile();
}
// @endif
}
// @if TARGET='app'
sleep(ms: number) {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
refreshMetadata() {
const { onStartCb } = this.props;
this.setState({ hasMetadata: true });
if (onStartCb) {
onStartCb();
}
const playerElement = this.mediaContainer.current;
if (playerElement) {
if (playerElement.children && playerElement.children[0]) {
playerElement.children[0].play();
}
}
}
// @endif
togglePlay(event: any) {
// ignore all events except click and spacebar keydown, or input events in a form control
if (
event.type === 'keydown' &&
(event.code !== 'Space' || (event.target && event.target.tagName.toLowerCase() === 'input'))
) {
return;
}
event.preventDefault();
const mediaRef = this.mediaContainer.current;
if (!mediaRef) {
return;
}
const mediaElement = mediaRef.children && mediaRef.children[0];
if (mediaElement) {
if (!mediaElement.paused) {
mediaElement.pause();
} else {
mediaElement.play();
}
}
}
playableType(): boolean {
const { mediaType } = this.props;
return ['audio', 'video', 'image'].indexOf(mediaType) !== -1;
}
isRenderMediaSupported() {
// Files supported by render-media
const { contentType } = this.props;
return Object.values(player.mime).indexOf(contentType) !== -1;
}
isSupportedFile() {
// This files are supported using a custom viewer
const { mediaType, contentType } = this.props;
return MediaPlayer.FILE_MEDIA_TYPES.indexOf(mediaType) > -1 || MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1;
}
renderFile() {
// This is what render-media does with unplayable files
const { claim, fileName, downloadPath, contentType } = this.props;
if (MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1) {
const outpoint = `${claim.txid}:${claim.nout}`;
// Fetch unpacked url
fetch(`${MediaPlayer.SANDBOX_SET_BASE_URL}${outpoint}`)
.then(res => res.text())
.then(url => {
const fileSource = { url: `${MediaPlayer.SANDBOX_CONTENT_BASE_URL}${url}` };
this.setState({ fileSource });
})
.catch(err => {
console.error(err);
});
} else {
// File to render
const fileSource = {
fileName,
contentType,
downloadPath,
fileType: path.extname(fileName).substring(1),
// Readable stream from file
// @if TARGET='app'
stream: opts => fs.createReadStream(downloadPath, opts),
// @endif
};
// Update state
this.setState({ fileSource });
}
}
showLoadingScreen(isFileType: boolean, isPlayableType: boolean) {
const { mediaType } = this.props;
const { unplayable, fileSource, hasMetadata } = this.state;
if (IS_WEB && ['audio', 'video'].indexOf(mediaType) === -1) {
return {
isLoading: false,
loadingStatus: __('This file type is not currently supported on lbry.tv. Try viewing it in the desktop app.'),
};
}
const loader: { isLoading: boolean, loadingStatus: ?string } = {
isLoading: false,
loadingStatus: null,
};
// Loading message
const noFileMessage = __('Waiting for blob.');
const noMetadataMessage = __('Waiting for metadata.');
// Error message
const unplayableMessage = __("Sorry, looks like we can't play this file.");
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
// Files
const isLoadingFile = !fileSource && isFileType;
const isUnsupported = !this.isRenderMediaSupported() && !isFileType && !isPlayableType;
// Media (audio, video)
const isUnplayable = isPlayableType && unplayable;
const isLoadingMetadata = isPlayableType && (!hasMetadata && !unplayable);
// Show loading message
if (isLoadingFile || isLoadingMetadata) {
loader.loadingStatus = isFileType ? noFileMessage : noMetadataMessage;
loader.isLoading = true;
// Show unsupported error message
} else if (isUnsupported || isUnplayable) {
loader.loadingStatus = isUnsupported ? unsupportedMessage : unplayableMessage;
}
return loader;
}
render() {
const { mediaType, claim } = this.props;
const { fileSource } = this.state;
const isFileType = this.isSupportedFile();
const isFileReady = fileSource !== null && isFileType;
const isPlayableType = this.playableType();
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);
return (
<React.Fragment>
{loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />}
{isFileReady && <FileRender claim={claim} source={fileSource} mediaType={mediaType} />}
<div className="content__view--container" style={{ opacity: isLoading ? 0 : 1 }} ref={this.mediaContainer} />
</React.Fragment>
);
}
}
export default MediaPlayer;

View file

@ -1,323 +1,143 @@
// @flow
import type { ElementRef } from 'react';
import * as PAGES from 'constants/pages';
import React, { Suspense } from 'react';
import * as ICONS from 'constants/icons';
import React, { useEffect } from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import analytics from 'analytics';
import LoadingScreen from 'component/common/loading-screen';
import PlayButton from './internal/play-button';
import detectTyping from 'util/detect-typing';
const Player = React.lazy(() =>
import(
/* webpackChunkName: "player-legacy" */
'./internal/player'
)
);
const SPACE_BAR_KEYCODE = 32;
import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'util/use-persisted-state';
import { FILE_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip';
type Props = {
cancelPlay: () => void,
fileInfo: {
outpoint: string,
file_name: string,
written_bytes: number,
download_path: string,
completed: boolean,
blobs_completed: number,
},
fileInfoErrors: ?{
[string]: boolean,
},
autoplay: boolean,
isLoading: boolean,
isDownloading: boolean,
playingUri: ?string,
contentType: string,
changeVolume: number => void,
volume: number,
claim: StreamClaim,
uri: string,
savePosition: (string, string, number) => void,
position: ?number,
className: ?string,
obscureNsfw: boolean,
play: string => void,
mediaType: string,
claimRewards: () => void,
nextFileToPlay: ?string,
navigate: (string, {}) => void,
costInfo: ?{ cost: number },
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
obscurePreview: boolean,
insufficientCredits: boolean,
nsfw: boolean,
thumbnail: ?string,
isPlayableType: boolean,
viewerContainer: { current: ElementRef<any> },
changeMute: boolean => void,
muted: boolean,
isStreamable: boolean,
thumbnail?: string,
streamingUrl?: string,
floatingPlayer: boolean,
pageUri: ?string,
title: ?string,
floatingPlayerEnabled: boolean,
clearPlayingUri: () => void,
};
class FileViewer extends React.PureComponent<Props> {
constructor() {
super();
(this: any).playContent = this.playContent.bind(this);
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
(this: any).logTimeToStart = this.logTimeToStart.bind(this);
(this: any).onFileFinishCb = this.onFileFinishCb.bind(this);
(this: any).onFileStartCb = undefined;
export default function FileViewer(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
isStreamable,
pageUri,
title,
clearPlayingUri,
floatingPlayerEnabled,
} = props;
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
x: -25,
y: window.innerHeight - 400,
});
// Don't add these variables to state because we don't need to re-render when their values change
(this: any).startTime = undefined;
(this: any).playTime = undefined;
}
const inline = pageUri === uri;
const isReadyToPlay = (IS_WEB && isStreamable) || (isStreamable && streamingUrl) || (fileInfo && fileInfo.completed);
const loadingMessage =
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
: __('Loading');
componentDidMount() {
const { fileInfo } = this.props;
if (!fileInfo) {
this.onFileStartCb = this.logTimeToStart;
}
this.handleAutoplay(this.props);
window.addEventListener('keydown', this.handleKeyDown);
}
componentDidUpdate(prev: Props) {
const { fileInfo } = this.props;
if (this.props.uri !== prev.uri) {
// User just directly navigated to another piece of content
if (this.startTime && !this.playTime) {
// They started playing a file but it didn't start streaming
// Fire the analytics event with the previous file
this.fireAnalyticsEvent(prev.claim);
}
this.startTime = null;
this.playTime = null;
// If this new file is already downloaded, remove the startedPlayingCallback
if (fileInfo && this.onFileStartCb) {
this.onFileStartCb = null;
} else if (!fileInfo && !this.onFileStartCb) {
this.onFileStartCb = this.logTimeToStart;
}
}
if (
this.props.autoplay !== prev.autoplay ||
this.props.fileInfo !== prev.fileInfo ||
this.props.isDownloading !== prev.isDownloading ||
this.props.playingUri !== prev.playingUri
) {
// suppress autoplay after download error
if (!this.props.fileInfoErrors || !(this.props.uri in this.props.fileInfoErrors)) {
this.handleAutoplay(this.props);
}
}
}
componentWillUnmount() {
const { claim } = this.props;
if (this.startTime && !this.playTime) {
// The user is navigating away before the file started playing, or a play time was never set
// Currently will not be set for files that don't use render-media
this.fireAnalyticsEvent(claim);
}
this.props.cancelPlay();
window.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(event: KeyboardEvent) {
if (!detectTyping()) {
if (event.keyCode === SPACE_BAR_KEYCODE) {
event.preventDefault(); // prevent page scroll
this.playContent();
}
}
}
handleAutoplay = (props: Props) => {
const { autoplay, playingUri, fileInfo, costInfo, isDownloading, uri, nsfw } = props;
const playable = autoplay && playingUri !== uri && !nsfw;
if (playable && costInfo && costInfo.cost === 0 && !fileInfo && !isDownloading) {
this.playContent();
} else if (playable && fileInfo && fileInfo.download_path && fileInfo.written_bytes > 0) {
this.playContent();
}
};
isMediaSame(nextProps: Props) {
return this.props.fileInfo && nextProps.fileInfo && this.props.fileInfo.outpoint === nextProps.fileInfo.outpoint;
}
playContent() {
const { play, uri, fileInfo, isDownloading, isLoading, insufficientCredits } = this.props;
if (!fileInfo && insufficientCredits) {
return;
}
// @if TARGET='app'
if (fileInfo || isDownloading || isLoading) {
// User may have pressed download before clicking play
this.onFileStartCb = null;
}
if (this.onFileStartCb) {
this.startTime = Date.now();
}
// @endif
play(uri);
}
logTimeToStart() {
const { claim } = this.props;
if (this.startTime) {
this.playTime = Date.now();
this.fireAnalyticsEvent(claim, this.startTime, this.playTime);
}
}
fireAnalyticsEvent(claim: StreamClaim, startTime: ?number, playTime: ?number) {
const { claimRewards } = this.props;
const { name, claim_id: claimId, txid, nout } = claim;
// ideally outpoint would exist inside of claim information
// we can use it after https://github.com/lbryio/lbry/issues/1306 is addressed
const outpoint = `${txid}:${nout}`;
let timeToStart;
if (playTime && startTime) {
timeToStart = playTime - startTime;
}
analytics.apiLogView(`${name}#${claimId}`, outpoint, claimId, timeToStart, claimRewards);
}
onFileFinishCb() {
// If a user has `autoplay` enabled, start playing the next file at the top of "related"
const { autoplay, nextFileToPlay, navigate } = this.props;
if (autoplay && nextFileToPlay) {
navigate(PAGES.SHOW, { uri: nextFileToPlay });
}
}
onFileStartCb: ?() => void;
startTime: ?number;
playTime: ?number;
render() {
const {
isLoading,
isDownloading,
playingUri,
fileInfo = {},
contentType,
changeVolume,
volume,
claim,
uri,
savePosition,
position,
className,
obscureNsfw,
mediaType,
insufficientCredits,
viewerContainer,
thumbnail,
nsfw,
muted,
changeMute,
} = this.props;
const isPlaying = playingUri === uri;
let isReadyToPlay = false;
// @if TARGET='app'
isReadyToPlay = fileInfo && fileInfo.download_path && fileInfo.written_bytes > 0;
// @endif
// @if TARGET='web'
// try to play immediately on web, we don't need to call file_list since we are streaming from reflector
isReadyToPlay = isPlaying;
// @endif
const shouldObscureNsfw = obscureNsfw && nsfw;
let loadStatusMessage = '';
if (fileInfo && fileInfo.completed && (!fileInfo.download_path || !fileInfo.written_bytes)) {
loadStatusMessage = __(
"It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds."
);
} else if (isLoading) {
loadStatusMessage = __('Requesting stream...');
} else if (isDownloading) {
loadStatusMessage = __('Downloading stream... not long left now!');
}
const layoverClass = classnames('content__cover', {
'card__media--nsfw': shouldObscureNsfw,
'card__media--disabled': !fileInfo && insufficientCredits,
function handleDrag(e, ui) {
const { x, y } = position;
const newX = x + ui.deltaX;
const newY = y + ui.deltaY;
setPosition({
x: newX,
y: newY,
});
const layoverStyle = !shouldObscureNsfw && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {};
return (
<div className={classnames('video', {}, className)} ref={viewerContainer}>
{isPlaying && (
<div className="content__view">
{!isReadyToPlay ? (
<div className={layoverClass} style={layoverStyle}>
<LoadingScreen status={loadStatusMessage} />
</div>
) : (
<Suspense fallback={<div />}>
<Player
fileName={fileInfo.file_name}
poster={thumbnail}
downloadPath={fileInfo.download_path}
mediaType={mediaType}
contentType={contentType}
downloadCompleted={fileInfo.completed}
changeVolume={changeVolume}
volume={volume}
savePosition={newPosition => savePosition(claim.claim_id, `${claim.txid}:${claim.nout}`, newPosition)}
claim={claim}
uri={uri}
position={position}
onStartCb={this.onFileStartCb}
onFinishCb={this.onFileFinishCb}
playingUri={playingUri}
viewerContainer={viewerContainer}
muted={muted}
changeMute={changeMute}
/>
</Suspense>
)}
</div>
)}
{!isPlaying && (
<div role="button" onClick={this.playContent} className={layoverClass} style={layoverStyle}>
<PlayButton
play={(e: SyntheticInputEvent<*>) => {
e.stopPropagation();
this.playContent();
}}
fileInfo={fileInfo}
uri={uri}
isLoading={isLoading}
mediaType={mediaType}
/>
</div>
)}
</div>
);
}
}
export default FileViewer;
useEffect(() => {
function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
if (!element) {
throw new Error("Can't find file viewer wrapper to attach to");
}
const rect = element.getBoundingClientRect();
// @FlowFixMe
setFileViewerRect(rect);
}
if (inline) {
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}
}, [setFileViewerRect, inline]);
const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable));
if (hidePlayer) {
clearPlayingUri();
return null;
}
return (
<Draggable
onDrag={handleDrag}
defaultPosition={position}
position={inline ? { x: 0, y: 0 } : position}
bounds="parent"
disabled={inline}
handle=".content__info"
cancel=".button"
>
<div
className={classnames('content__viewer', {
'content__viewer--floating': !inline,
})}
style={
inline && fileViewerRect
? { width: fileViewerRect.width, height: fileViewerRect.height, left: fileViewerRect.x }
: {}
}
>
<div
className={classnames('content__wrapper', {
'content__wrapper--floating': !inline,
})}
>
{!inline && (
<div className="content__actions">
<Tooltip label={__('View File')}>
<Button navigate={uri} icon={ICONS.VIEW} button="primary" />
</Tooltip>
<Tooltip label={__('Close')}>
<Button onClick={clearPlayingUri} icon={ICONS.REMOVE} button="primary" />
</Tooltip>
</div>
)}
{isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={loadingMessage} />}
{!inline && (
<div className="content__info">
<div className="claim-preview-title" title={title || uri}>
{title || uri}
</div>
<UriIndicator link addTooltip={false} uri={uri} />
</div>
)}
</div>
</div>
</Draggable>
);
}

View file

@ -0,0 +1,37 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectUriIsStreamable,
} from 'lbry-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsPlaying, makeSelectShouldObscurePreview, selectPlayingUri } from 'redux/selectors/content';
import FileViewer from './view';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state),
playingUri: selectPlayingUri(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
});
const perform = dispatch => ({
play: uri => {
dispatch(doSetPlayingUri(uri));
dispatch(doPlayUri(uri));
},
});
export default connect(
select,
perform
)(FileViewer);

View file

@ -0,0 +1,94 @@
// @flow
// This component is entirely for triggering the start of a file view
// The actual viewer for a file exists in FileViewer
// 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 React, { useEffect, useCallback } from 'react';
import classnames from 'classnames';
import Button from 'component/button';
import isUserTyping from 'util/detect-typing';
const SPACE_BAR_KEYCODE = 32;
type Props = {
play: string => void,
mediaType: string,
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
obscurePreview: boolean,
insufficientCredits: boolean,
isStreamable: boolean,
thumbnail?: string,
autoplay: boolean,
};
export default function FileViewer(props: Props) {
const { play, mediaType, isPlaying, fileInfo, uri, obscurePreview, insufficientCredits, thumbnail, autoplay } = props;
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;
const fileStatus = fileInfo && fileInfo.status;
// Wrap this in useCallback because we need to use it to the keyboard effect
// If we don't a new instance will be created for every render and react will think the dependencies have change, which will add/remove the listener for every render
const viewFile = useCallback(
(e?: SyntheticInputEvent<*> | KeyboardEvent) => {
if (e) {
e.stopPropagation();
}
play(uri);
},
[play, uri]
);
useEffect(() => {
// This is just for beginning to download a file
// Play/Pause/Fullscreen will be handled by the respective viewers because not every file type should behave the same
function handleKeyDown(e: KeyboardEvent) {
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
e.preventDefault();
if (!isPlaying || fileStatus === 'stopped') {
viewFile(e);
}
}
}
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isPlaying, fileStatus, viewFile]);
useEffect(() => {
const videoOnPage = document.querySelector('video');
if (autoplay && !videoOnPage) {
viewFile();
}
}, [autoplay, viewFile]);
return (
<div
onClick={viewFile}
style={!obscurePreview && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'card__media--nsfw': obscurePreview,
'card__media--disabled': !fileInfo && insufficientCredits,
})}
>
{!isPlaying && (
<Button
onClick={viewFile}
iconSize={30}
title={isPlayable ? __('Play') : __('View')}
className={classnames('button--icon', {
'button--play': isPlayable,
'button--view': !isPlayable,
})}
/>
)}
</div>
);
}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectNsfwCountForChannel, makeSelectNsfwCountFromUris, parseURI } from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import { selectShowMatureContent } from 'redux/selectors/settings';
import HiddenNsfwClaims from './view';
const select = (state, props) => {
@ -18,7 +18,7 @@ const select = (state, props) => {
return {
numberOfNsfwClaims,
obscureNsfw: !selectShowNsfw(state),
obscureNsfw: !selectShowMatureContent(state),
};
};

View file

@ -1,8 +1,7 @@
// @flow
import * as MODALS from 'constants/modal_types';
import { THUMBNAIL_STATUSES } from 'lbry-redux';
import { Lbry, THUMBNAIL_STATUSES } from 'lbry-redux';
import * as React from 'react';
import getMediaType from 'util/get-media-type';
import { FormField } from 'component/common/form';
import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
@ -64,7 +63,7 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
const { thumbnailError } = this.state;
const isSupportedVideo = getMediaType(null, filePath) === 'video';
const isSupportedVideo = Lbry.getMediaType(null, filePath) === 'video';
let thumbnailSrc;
if (!thumbnail) {

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectContentTypeForUri } from 'lbry-redux';
import AppViewer from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
});
const perform = dispatch => ({});
export default connect(
select,
perform
)(AppViewer);

View file

@ -0,0 +1,64 @@
// @flow
import React, { useState, useEffect } from 'react';
import LoadingScreen from 'component/common/loading-screen';
type Props = {
source: string,
claim: StreamClaim,
contentType: string,
};
const SANDBOX_TYPES = ['application/x-lbry', 'application/x-ext-lbry'];
// This server exists in src/platforms/electron/startSandBox.js
const SANDBOX_SET_BASE_URL = 'http://localhost:5278/set/';
const SANDBOX_CONTENT_BASE_URL = 'http://localhost:5278';
function AppViewer(props: Props) {
const { claim, contentType } = props;
const [loading, setLoading] = useState(true);
const [appUrl, setAppUrl] = useState(false);
const outpoint = `${claim.txid}:${claim.nout}`;
useEffect(() => {
if (SANDBOX_TYPES.indexOf(contentType) > -1) {
fetch(`${SANDBOX_SET_BASE_URL}${outpoint}`)
.then(res => res.text())
.then(url => {
const appUrl = `${SANDBOX_CONTENT_BASE_URL}${url}`;
setAppUrl(appUrl);
setLoading(false);
})
.catch(err => {
setLoading(false);
});
} else {
setLoading(false);
}
}, [outpoint, contentType, setAppUrl, setLoading]);
return (
<div className="file-render__viewer">
{!appUrl && (
<LoadingScreen
status={loading ? __('Almost there') : __('Unable to view this file in the app')}
spinner={loading}
/>
)}
{appUrl && (
<webview
title=""
sandbox="allow-scripts allow-forms allow-pointer-lock"
src={appUrl}
autosize="on"
style={{ border: 0, width: '100%', height: '100%' }}
useragent="Mozilla/5.0 AppleWebKit/537 Chrome/60 Safari/537"
enableremotemodule="false"
webpreferences="sandbox=true,contextIsolation=true,webviewTag=false,enableRemoteModule=false,devTools=false"
/>
)}
</div>
);
}
export default AppViewer;

View file

@ -35,8 +35,10 @@ class CodeViewer extends React.PureComponent<Props> {
const me = this;
const { theme, contentType } = me.props;
// Init CodeMirror
import(/* webpackChunkName: "codemirror" */
'codemirror/lib/codemirror').then(CodeMirror => {
import(
/* webpackChunkName: "codemirror" */
'codemirror/lib/codemirror'
).then(CodeMirror => {
me.codeMirror = CodeMirror.fromTextArea(me.textarea, {
// Auto detect syntax with file contentType
mode: contentType,
@ -62,7 +64,7 @@ class CodeViewer extends React.PureComponent<Props> {
render() {
const { value } = this.props;
return (
<div className="code-viewer" onContextMenu={stopContextMenu}>
<div className="file-render__content" onContextMenu={stopContextMenu}>
<textarea ref={textarea => (this.textarea = textarea)} disabled value={value} />
</div>
);

View file

@ -5,8 +5,10 @@ import LoadingScreen from 'component/common/loading-screen';
import MarkdownPreview from 'component/common/markdown-preview';
const LazyCodeViewer = React.lazy<*>(() =>
import(/* webpackChunkName: "codeViewer" */
'component/viewers/codeViewer')
import(
/* webpackChunkName: "codeViewer" */
'component/viewers/codeViewer'
)
);
type Props = {
@ -81,7 +83,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer document-viewer">
<div className="file-render__viewer--document">
{loading && !error && <LoadingScreen status={loadingMessage} spinner />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && <Suspense fallback={<div />}>{this.renderDocument()}</Suspense>}

View file

@ -3,7 +3,6 @@
import React from 'react';
import mammoth from 'mammoth';
import LoadingScreen from 'component/common/loading-screen';
import MarkdownPreview from 'component/common/markdown-preview';
type Props = {
source: string,
@ -59,10 +58,10 @@ class DocxViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="document-viewer file-render__viewer">
<div className="file-render__viewer--document">
{loading && <LoadingScreen status={loadingMessage} spinner />}
{error && <LoadingScreen status={errorMessage} spinner={false} />}
{content && <div className="document-viewer__content" dangerouslySetInnerHTML={{ __html: content }} />}
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}
</div>
);
}

View file

@ -0,0 +1,17 @@
// @flow
import React from 'react';
type Props = {
source: string,
};
function ImageViewer(props: Props) {
const { source } = props;
return (
<div className="file-render__viewer">
<img src={source} />
</div>
);
}
export default ImageViewer;

View file

@ -28,7 +28,7 @@ class PdfViewer extends React.PureComponent<Props> {
shell.openExternal(path);
// @endif
// @if TARGET='web'
console.error('provide stub for shell.openExternal');
console.error('provide stub for shell.openExternal'); // eslint-disable-line
// @endif
}
@ -37,7 +37,7 @@ class PdfViewer extends React.PureComponent<Props> {
// This was disabled on electron@3
// https://github.com/electron/electron/issues/12337
return (
<div className="file-render__viewer file-render--pdf" onContextMenu={stopContextMenu}>
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
<p>
{__('PDF opened externally.')} <Button button="link" label={__('Click here')} onClick={this.openFile} />{' '}
{__('to open it again.')}

View file

@ -1,82 +0,0 @@
// @flow
import React from 'react';
import { stopContextMenu } from 'util/context-menu';
import analytics from 'analytics';
import(
/* webpackChunkName: "videojs" */
/* webpackPreload: true */
'video.js/dist/video-js.css'
);
type Props = {
source: {
downloadPath: string,
fileName: string,
},
contentType: string,
poster?: string,
claim: StreamClaim,
};
class AudioVideoViewer extends React.PureComponent<Props> {
videoNode: ?HTMLVideoElement;
player: ?{ dispose: () => void };
componentDidMount() {
const { contentType, poster, claim } = this.props;
const { name, claim_id: claimId, txid, nout } = claim;
// Quick fix to get file view events on lbry.tv
// Will need to be changed to include time to start
analytics.apiLogView(`${name}#${claimId}`, `${txid}:${nout}`, claimId);
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
const sources = [
{
src: path,
type: contentType,
},
];
const videoJsOptions = {
autoplay: true,
controls: true,
preload: 'auto',
poster,
sources,
playbackRates: [0.5, 1, 1.25, 1.5, 2],
};
import(
/* webpackChunkName: "videojs" */
/* webpackMode: "lazy" */
/* webpackPreload: true */
'video.js'
).then(videojs => {
if (videojs.__esModule) {
videojs = videojs.default;
this.player = videojs(this.videoNode, videoJsOptions, () => {});
} else {
throw Error('Unable to import and use videojs');
}
});
}
componentWillUnmount() {
if (this.player) {
this.player.dispose();
}
}
render() {
return (
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
<div data-vjs-player>
<video ref={node => (this.videoNode = node)} className="video-js" />
</div>
</div>
);
}
}
export default AudioVideoViewer;

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri } from 'lbry-redux';
import { doChangeVolume, doChangeMute } from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition } from 'redux/actions/content';
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
import VideoViewer from './view';
const select = (state, props) => ({
volume: selectVolume(state),
position: makeSelectContentPositionForUri(props.uri)(state),
muted: selectMute(state),
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
});
const perform = dispatch => ({
changeVolume: volume => dispatch(doChangeVolume(volume)),
savePosition: (uri, position) => dispatch(savePosition(uri, position)),
changeMute: muted => dispatch(doChangeMute(muted)),
});
export default connect(
select,
perform
)(VideoViewer);

View file

@ -0,0 +1,94 @@
// @flow
import React, { useRef, useEffect, useState } from 'react';
import { stopContextMenu } from 'util/context-menu';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import isUserTyping from 'util/detect-typing';
const SPACE_BAR_KEYCODE = 32;
const VIDEO_JS_OPTIONS = {
autoplay: true,
controls: true,
preload: 'auto',
playbackRates: [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 2],
};
type Props = {
source: string,
contentType: string,
hasFileInfo: boolean,
};
function VideoViewer(props: Props) {
const { contentType, source } = props;
const videoRef = useRef();
const [requireRedraw, setRequireRedraw] = useState(false);
useEffect(() => {
const videoNode = videoRef.current;
const videoJsOptions = {
...VIDEO_JS_OPTIONS,
sources: [
{
src: source,
type: contentType,
},
],
};
let player;
if (!requireRedraw) {
player = videojs(videoNode, videoJsOptions);
}
return () => {
if (!player) {
return;
}
// Video.js has a player.dispose() function that is meant to cleanup a previous video
// We can't use this because it does some weird stuff to remove the video element from the page
// This makes it really hard to use because the ref we keep still thinks it's on the page
// requireRedraw just makes it so the video component is removed from the page _by react_
// Then it's set to false immediately after so we can re-mount a new player
setRequireRedraw(true);
};
}, [videoRef, source, contentType, setRequireRedraw, requireRedraw]);
useEffect(() => {
if (requireRedraw) {
setRequireRedraw(false);
}
}, [requireRedraw]);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
const videoNode = videoRef.current;
if (videoNode && !isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
e.preventDefault();
videoNode.paused ? videoNode.play() : videoNode.pause();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
// include requireRedraw here so the event listener is re-added when we need to manually remove/add the video player
}, [videoRef, requireRedraw]);
return (
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
{!requireRedraw && (
<div data-vjs-player>
<video ref={videoRef} className="video-js" />
</div>
)}
</div>
);
}
export default VideoViewer;

View file

@ -35,7 +35,6 @@ export const WEB = 'Globe';
export const SHARE = 'Share2';
export const EXTERNAL = 'ExternalLink';
export const TIP = 'Gift';
export const VIEW = 'Eye';
export const PLAY = 'Play';
export const FACEBOOK = 'Facebook';
export const TWITTER = 'Twitter';
@ -73,3 +72,4 @@ export const TAG = 'Tag';
export const SUPPORT = 'TrendingUp';
export const BLOCK = 'Slash';
export const UNBLOCK = 'Circle';
export const VIEW = 'View';

View file

@ -19,3 +19,4 @@ export const AUTO_DOWNLOAD = 'autoDownload';
export const SUPPORT_OPTION = 'supportOption';
export const HIDE_BALANCE = 'hideBalance';
export const HIDE_SPLASH_ANIMATION = 'hideSplashAnimation';
export const FLOATING_PLAYER = 'floatingPlayer';

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { doLoadVideo, doSetPlayingUri } from 'redux/actions/content';
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
import { doHideModal } from 'redux/actions/app';
import { makeSelectMetadataForUri } from 'lbry-redux';
import ModalAffirmPurchase from './view';
@ -14,7 +14,10 @@ const perform = dispatch => ({
dispatch(doHideModal());
},
closeModal: () => dispatch(doHideModal()),
loadVideo: uri => dispatch(doLoadVideo(uri)),
loadVideo: uri => {
dispatch(doSetPlayingUri(uri));
dispatch(doPlayUri(uri, true));
},
});
export default connect(

View file

@ -32,11 +32,11 @@ function ModalRemoveFile(props: Props) {
</section>
<Form onSubmit={() => deleteFile(outpoint || '', deleteChecked, abandonChecked)}>
<FormField
name="claim_abandon"
label={__('Abandon the claim for this URI')}
name="file_delete"
label={__('Also delete this file from my computer')}
type="checkbox"
checked={abandonChecked}
onChange={() => setAbandonChecked(!abandonChecked)}
checked={deleteChecked}
onChange={() => setDeleteChecked(!deleteChecked)}
/>
{claimIsMine && (

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content';
import { selectRewardContentClaimIds } from 'redux/selectors/content';
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSetContentHistoryItem } from 'redux/actions/content';
@ -19,7 +19,7 @@ import {
doPrepareEdit,
} from 'lbry-redux';
import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowNsfw, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import FilePage from './view';
@ -29,12 +29,10 @@ const select = (state, props) => ({
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
obscureNsfw: !selectShowMatureContent(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
playingUri: selectPlayingUri(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state),

View file

@ -3,8 +3,7 @@ import * as MODALS from 'constants/modal_types';
import * as icons from 'constants/icons';
import * as React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux';
import FileViewer from 'component/fileViewer';
import Thumbnail from 'component/common/thumbnail';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileActions from 'component/fileActions';
@ -13,8 +12,6 @@ import DateTime from 'component/dateTime';
import Button from 'component/button';
import Page from 'component/page';
import FileDownloadLink from 'component/fileDownloadLink';
import classnames from 'classnames';
import getMediaType from 'util/get-media-type';
import RecommendedContent from 'component/recommendedContent';
import ClaimTags from 'component/claimTags';
import CommentsList from 'component/commentsList';
@ -22,13 +19,14 @@ import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import ClaimPreview from 'component/claimPreview';
export const FILE_WRAPPER_CLASS = 'grid-area--content';
type Props = {
claim: StreamClaim,
fileInfo: FileListItem,
contentType: string,
uri: string,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
claimIsMine: boolean,
costInfo: ?{ cost: number },
fetchFileInfo: string => void,
@ -39,38 +37,16 @@ type Props = {
channelUri: string,
viewCount: number,
prepareEdit: ({}, string, {}) => void,
openModal: (id: string, { [key: string]: any }) => void,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void,
balance: number,
title: string,
thumbnail: ?string,
nsfw: boolean,
supportOption: boolean,
};
class FilePage extends React.Component<Props> {
static PLAYABLE_MEDIA_TYPES = ['audio', 'video'];
static PREVIEW_MEDIA_TYPES = [
'text',
'model',
'image',
'script',
'document',
'3D-file',
'comic-book',
// Bypass unplayable files
// TODO: Find a better way to detect supported types
'application',
];
constructor(props: Props) {
super(props);
(this: any).viewerContainer = React.createRef();
}
viewerContainer: { current: React.ElementRef<any> };
componentDidMount() {
const {
uri,
@ -136,7 +112,6 @@ class FilePage extends React.Component<Props> {
contentType,
uri,
rewardedContentClaimIds,
obscureNsfw,
openModal,
claimIsMine,
prepareEdit,
@ -146,7 +121,6 @@ class FilePage extends React.Component<Props> {
viewCount,
balance,
title,
thumbnail,
nsfw,
supportOption,
} = this.props;
@ -154,15 +128,7 @@ class FilePage extends React.Component<Props> {
// File info
const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && nsfw;
const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = getMediaType(contentType, fileName);
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', 'audio'].includes(contentType.split('/')[0]);
// We want to use the short form uri for editing
@ -185,7 +151,7 @@ class FilePage extends React.Component<Props> {
return (
<Page className="main--file-page">
<div className="grid-area--content card">
<div className={`card ${FILE_WRAPPER_CLASS}`}>
{!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning">
{__(
@ -195,28 +161,7 @@ class FilePage extends React.Component<Props> {
{__('or send more LBC to your wallet.')}
</div>
)}
{showFile && (
<FileViewer
uri={uri}
className="content__embedded"
mediaType={mediaType}
isPlayableType={isPlayableType}
viewerContainer={this.viewerContainer}
insufficientCredits={insufficientCredits}
/>
)}
{!showFile &&
(thumbnail ? (
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />
) : (
<div
className={classnames('content__empty', {
'content__empty--nsfw': shouldObscureThumbnail,
})}
>
<div className="card__media-text">{__("Sorry, looks like we can't preview this file.")}</div>
</div>
))}
<FileViewerInitiator uri={uri} insufficientCredits={insufficientCredits} />
</div>
<div className="columns">
@ -274,12 +219,7 @@ class FilePage extends React.Component<Props> {
<div className="media__action-group--large">
<FileDownloadLink uri={uri} />
<FileActions
uri={uri}
claimId={claim.claim_id}
showFullscreen={isPreviewType}
viewerContainer={this.viewerContainer}
/>
<FileActions uri={uri} claimId={claim.claim_id} />
</div>
</div>
</div>

View file

@ -28,6 +28,7 @@ const select = state => ({
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
userBlockedChannelsCount: selectBlockedChannelsCount(state),
hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state),
floatingPlayer: makeSelectClientSetting(settings.FLOATING_PLAYER)(state),
});
const perform = dispatch => ({

View file

@ -20,6 +20,9 @@ type DaemonSettings = {
download_dir: string,
share_usage_data: boolean,
max_key_fee?: Price,
max_connections_per_download?: number,
save_files: boolean,
save_blobs: boolean,
};
type Props = {
@ -47,6 +50,8 @@ type Props = {
supportOption: boolean,
userBlockedChannelsCount?: number,
hideBalance: boolean,
floatingPlayer: boolean,
clearPlayingUri: () => void,
};
type State = {
@ -62,6 +67,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
};
(this: any).onKeyFeeChange = this.onKeyFeeChange.bind(this);
(this: any).onMaxConnectionsChange = this.onMaxConnectionsChange.bind(this);
(this: any).onKeyFeeDisableChange = this.onKeyFeeDisableChange.bind(this);
(this: any).onInstantPurchaseMaxChange = this.onInstantPurchaseMaxChange.bind(this);
(this: any).onThemeChange = this.onThemeChange.bind(this);
@ -79,6 +85,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
this.setDaemonSetting('max_key_fee', newValue);
}
onMaxConnectionsChange(event: SyntheticInputEvent<*>) {
const { value } = event.target;
this.setDaemonSetting('max_connections_per_download', value);
}
onKeyFeeDisableChange(isDisabled: boolean) {
if (isDisabled) this.setDaemonSetting('max_key_fee');
}
@ -156,12 +167,15 @@ class SettingsPage extends React.PureComponent<Props, State> {
supportOption,
hideBalance,
userBlockedChannelsCount,
floatingPlayer,
} = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
const connectionOptions = [1, 2, 4, 6, 10, 20];
return (
<Page>
@ -188,7 +202,41 @@ class SettingsPage extends React.PureComponent<Props, State> {
</section>
<section className="card card--section">
<h2 className="card__title">{__('Max Purchase Price')}</h2>
<h2 className="card__title">{__('Network and Data Settings')}</h2>
<Form>
<FormField
type="checkbox"
name="save_files"
onChange={() => setDaemonSetting('save_files', !daemonSettings.save_files)}
checked={daemonSettings.save_files}
label={__('Save all viewed content to your downloads directory')}
helper={__(
'Paid content and some file types are saved by default. Changing this setting will not affect previously downloaded content.'
)}
/>
</Form>
<Form>
<FormField
type="checkbox"
name="save_blobs"
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
checked={daemonSettings.save_blobs}
label={__('Save hosting data to help the LBRY network')}
helper={
<React.Fragment>
{__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />.
</React.Fragment>
}
/>
</Form>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Max Purchase Price')}</h2>
</header>
<Form>
<FormField
@ -266,19 +314,38 @@ class SettingsPage extends React.PureComponent<Props, State> {
<section className="card card--section">
<h2 className="card__title">{__('Content Settings')}</h2>
<FormField
type="checkbox"
name="floating_player"
onChange={() => {
setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
}}
checked={floatingPlayer}
label={__('Floating video player')}
helper={__('Keep content playing in the corner when navigating to a different page.')}
/>
<Form>
<FormField
type="checkbox"
name="show_nsfw"
onChange={() => setClientSetting(SETTINGS.SHOW_NSFW, !showNsfw)}
checked={showNsfw}
label={__('Show mature content')}
helper={__(
'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
)}
/>
</Form>
<FormField
type="checkbox"
name="autoplay"
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
checked={autoplay}
label={__('Autoplay media files')}
helper={__(
'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.'
)}
/>
<FormField
type="checkbox"
name="show_nsfw"
onChange={() => setClientSetting(SETTINGS.SHOW_NSFW, !showNsfw)}
checked={showNsfw}
label={__('Show mature content')}
helper={__(
'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
)}
/>
</section>
<section className="card card--section">
@ -419,34 +486,43 @@ class SettingsPage extends React.PureComponent<Props, State> {
/>
<FormField
type="checkbox"
name="autoplay"
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
checked={autoplay}
label={__('Autoplay media files')}
name="language_select"
type="select"
label={__('Language')}
onChange={this.onLanguageChange}
value={currentLanguage}
helper={__(
'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.'
'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.'
)}
/>
{
>
{Object.keys(languages).map(language => (
<option key={language} value={language}>
{languages[language]}
</option>
))}
</FormField>
</Form>
<Form>
<fieldset-section>
<FormField
name="language_select"
name="max_connections"
type="select"
label={__('Language')}
onChange={this.onLanguageChange}
value={currentLanguage}
label={__('Max Connections')}
helper={__(
'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.'
'For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.'
)}
min={1}
max={100}
onChange={this.onMaxConnectionsChange}
value={daemonSettings.max_connections_per_download}
>
{Object.keys(languages).map(language => (
<option key={language} value={language}>
{languages[language]}
{connectionOptions.map(connectionOption => (
<option key={connectionOption} value={connectionOption}>
{connectionOption}
</option>
))}
</FormField>
}
</fieldset-section>
</Form>
</section>
@ -455,7 +531,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
<p className="card__subtitle--status">
{__(
'This will clear the application cache. Your wallet will not be affected. Currently, followed tags will be cleared.'
'This will clear the application cache. Your wallet will not be affected. Currently, followed tags and blocked channels will be cleared.'
)}
</p>

View file

@ -1,5 +1,5 @@
// @flow
import React, { createRef } from 'react';
import React, { useRef } from 'react';
import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover';
import Button from 'component/button';
@ -18,7 +18,7 @@ function TagsPage(props: Props) {
followedTags,
doToggleTagFollow,
} = props;
const buttonRef = createRef();
const buttonRef = useRef();
const isHovering = useHover(buttonRef);
const urlParams = new URLSearchParams(search);

View file

@ -10,6 +10,7 @@ import {
commentReducer,
blockedReducer,
publishReducer,
fileReducer,
} from 'lbry-redux';
import {
userReducer,
@ -38,6 +39,7 @@ export default history =>
content: contentReducer,
costInfo: costInfoReducer,
fileInfo: fileInfoReducer,
file: fileReducer,
homepage: homepageReducer,
notifications: notificationsReducer,
publish: publishReducer,

View file

@ -17,15 +17,15 @@ import {
buildURI,
makeSelectFileInfoForUri,
selectFileInfosByOutpoint,
selectDownloadingByOutpoint,
selectBalance,
makeSelectChannelForClaimUri,
parseURI,
doError,
doPurchaseUri,
makeSelectUriIsStreamable,
selectDownloadingByOutpoint,
makeSelectClaimForUri,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
import analytics from 'analytics';
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
import { formatLbryUriForWeb } from 'util/uri';
const DOWNLOAD_POLL_INTERVAL = 250;
@ -88,9 +88,9 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
dispatch(doUpdateUnreadSubscriptions(channelUri, null, NOTIFICATION_TYPES.DOWNLOADED));
} else {
// If notifications are disabled(false) just return
if (!selectosNotificationsEnabled(getState())) return;
if (!selectosNotificationsEnabled(getState()) || !fileInfo.written_bytes) return;
const notif = new window.Notification('LBRY Download Complete', {
const notif = new window.Notification(__('LBRY Download Complete'), {
body: fileInfo.metadata.title,
silent: false,
});
@ -123,39 +123,6 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
// @endif
}
export function doStartDownload(uri: string, outpoint: string) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
if (!outpoint) {
throw new Error('outpoint is required to begin a download');
}
const { downloadingByOutpoint = {} } = state.fileInfo;
if (downloadingByOutpoint[outpoint]) return;
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
dispatch({
type: ACTIONS.DOWNLOADING_STARTED,
data: {
uri,
outpoint,
fileInfo,
},
});
dispatch(doUpdateLoadStatus(uri, outpoint));
});
};
}
export function doDownloadFile(uri: string, streamInfo: { outpoint: string }) {
return (dispatch: Dispatch) => {
dispatch(doStartDownload(uri, streamInfo.outpoint));
};
}
export function doSetPlayingUri(uri: ?string) {
return (dispatch: Dispatch) => {
dispatch({
@ -165,126 +132,6 @@ export function doSetPlayingUri(uri: ?string) {
};
}
function handleLoadVideoError(uri: string, errorType: string = '') {
return (dispatch: Dispatch, getState: GetState) => {
// suppress error when another media is playing
const { playingUri } = getState().content;
const errorText = typeof errorType === 'object' ? errorType.message : errorType;
if (playingUri && playingUri === uri) {
dispatch({
type: ACTIONS.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doSetPlayingUri(null));
// this is not working, but should be it's own separate modal in the future (https://github.com/lbryio/lbry-desktop/issues/892)
if (errorType === 'timeout') {
doOpenModal(MODALS.FILE_TIMEOUT, { uri });
} else {
dispatch(
doError(
`Failed to download ${uri}, please try again or see error details:\n\n${errorText}\n\nIf this problem persists, visit https://lbry.com/support for help. `
)
);
}
}
};
}
export function doLoadVideo(uri: string, shouldRecordViewEvent: boolean = false) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.LOADING_VIDEO_STARTED,
data: {
uri,
},
});
Lbry.get({ uri })
.then(streamInfo => {
// need error code from SDK to capture properly
const timeout = streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout';
if (timeout) {
dispatch(handleLoadVideoError(uri, 'timeout'));
} else {
dispatch(doDownloadFile(uri, streamInfo));
if (shouldRecordViewEvent) {
analytics.apiLogView(
`${streamInfo.claim_name}#${streamInfo.claim_id}`,
streamInfo.outpoint,
streamInfo.claim_id
);
}
}
})
.catch(error => {
dispatch(handleLoadVideoError(uri, error));
});
};
}
export function doPurchaseUri(uri: string, specificCostInfo?: ?{}, shouldRecordViewEvent?: boolean = false) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const balance = selectBalance(state);
const fileInfo = makeSelectFileInfoForUri(uri)(state);
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
function attemptPlay(cost, instantPurchaseMax = null) {
// If you have a file entry with correct manifest, you won't pay for the key fee again
if (cost > 0 && (!instantPurchaseMax || cost > instantPurchaseMax) && !fileInfo) {
dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
} else {
dispatch(doLoadVideo(uri, shouldRecordViewEvent));
}
}
// we already fully downloaded the file.
if (fileInfo && fileInfo.completed) {
// If path is null or bytes written is 0 means the user has deleted/moved the
// file manually on their file system, so we need to dispatch a
// doLoadVideo action to reconstruct the file from the blobs
if (!fileInfo.download_path || !fileInfo.written_bytes) {
dispatch(doLoadVideo(uri, shouldRecordViewEvent));
}
Promise.resolve();
return;
}
// we are already downloading the file
if (alreadyDownloading) {
Promise.resolve();
return;
}
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
const { cost } = costInfo;
if (cost > balance) {
dispatch(doSetPlayingUri(null));
Promise.resolve();
return;
}
if (cost === 0 || !makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state)) {
attemptPlay(cost);
} else {
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
if (instantPurchaseMax.currency === 'LBC') {
attemptPlay(cost, instantPurchaseMax.amount);
} else {
// Need to convert currency of instant purchase maximum before trying to play
Lbryapi.getExchangeRates().then(({ LBC_USD }) => {
attemptPlay(cost, instantPurchaseMax.amount / LBC_USD);
});
}
}
};
}
export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize: number = PAGE_SIZE) {
return (dispatch: Dispatch) => {
dispatch({
@ -334,17 +181,73 @@ export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize:
};
}
export function doPlayUri(uri: string) {
return (dispatch: Dispatch) => {
dispatch(doSetPlayingUri(uri));
// @if TARGET='app'
dispatch(doPurchaseUri(uri));
// @endif
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean) {
return (dispatch: Dispatch, getState: () => any) => {
function onSuccess(fileInfo) {
dispatch(doUpdateLoadStatus(uri, fileInfo.outpoint));
}
// Only pass the sucess callback if we are saving the file, otherwise we don't show the download percentage
const successCallBack = saveFile ? onSuccess : undefined;
dispatch(doPurchaseUri(uri, { costInfo: cost }, saveFile, successCallBack));
};
}
export function savePosition(claimId: string, outpoint: string, position: number) {
return (dispatch: Dispatch) => {
export function doPlayUri(uri: string, skipCostCheck: boolean = false, saveFileOverride: boolean = false) {
return (dispatch: Dispatch, getState: () => any) => {
const state = getState();
const fileInfo = makeSelectFileInfoForUri(uri)(state);
const uriIsStreamable = makeSelectUriIsStreamable(uri)(state);
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloaded = fileInfo && (fileInfo.completed || (fileInfo.blobs_remaining === 0 && uriIsStreamable));
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
if (alreadyDownloading || alreadyDownloaded) {
return;
}
const daemonSettings = selectDaemonSettings(state);
const costInfo = makeSelectCostInfoForUri(uri)(state);
const cost = Number(costInfo.cost);
const saveFile = !uriIsStreamable ? true : daemonSettings.save_files || saveFileOverride || cost > 0;
const instantPurchaseEnabled = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state);
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
function beginGetFile() {
dispatch(doPurchaseUriWrapper(uri, cost, saveFile));
}
function attemptPlay(instantPurchaseMax = null) {
// If you have a file_list entry, you have already purchased the file
if (!fileInfo && (!instantPurchaseMax || cost > instantPurchaseMax)) {
dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
} else {
beginGetFile();
}
}
if (cost === 0 || skipCostCheck) {
beginGetFile();
return;
}
if (instantPurchaseEnabled || instantPurchaseMax.currency === 'LBC') {
attemptPlay(instantPurchaseMax.amount);
} else {
// Need to convert currency of instant purchase maximum before trying to play
Lbryapi.getExchangeRates().then(({ LBC_USD }) => {
attemptPlay(instantPurchaseMax.amount / LBC_USD);
});
}
};
}
export function savePosition(uri: string, position: number) {
return (dispatch: Dispatch, getState: () => any) => {
const state = getState();
const claim = makeSelectClaimForUri(uri)(state);
const { claim_id: claimId, txid, nout } = claim;
const outpoint = `${txid}:${nout}`;
dispatch({
type: ACTIONS.SET_CONTENT_POSITION,
data: { claimId, outpoint, position },

View file

@ -6,8 +6,7 @@ import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
import { doPurchaseUri } from 'redux/actions/content';
import { Lbry, buildURI, parseURI, doResolveUris, doPurchaseUri } from 'lbry-redux';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;

View file

@ -25,12 +25,13 @@ const defaultState = {
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
[SETTINGS.SUPPORT_OPTION]: getLocalStorageSetting(SETTINGS.SUPPORT_OPTION, false),
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, true),
[SETTINGS.RESULT_COUNT]: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
[SETTINGS.AUTO_DOWNLOAD]: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: Boolean(getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)),
[SETTINGS.HIDE_BALANCE]: Boolean(getLocalStorageSetting(SETTINGS.HIDE_BALANCE, false)),
[SETTINGS.HIDE_SPLASH_ANIMATION]: Boolean(getLocalStorageSetting(SETTINGS.HIDE_SPLASH_ANIMATION, false)),
[SETTINGS.FLOATING_PLAYER]: Boolean(getLocalStorageSetting(SETTINGS.FLOATING_PLAYER, true)),
},
isNight: false,
languages: { en: 'English', pl: 'Polish', id: 'Bahasa Indonesia' }, // temporarily hard code these so we can advance i18n testing

View file

@ -100,7 +100,7 @@ export const selectVolume = createSelector(
state => state.volume
);
export const selecetMute = createSelector(
export const selectMute = createSelector(
selectState,
state => state.muted
);

View file

@ -1,6 +1,12 @@
// @flow
import { createSelector } from 'reselect';
import { makeSelectClaimForUri, selectClaimsByUri, makeSelectClaimsInChannelForCurrentPageState } from 'lbry-redux';
import {
makeSelectClaimForUri,
selectClaimsByUri,
makeSelectClaimsInChannelForCurrentPageState,
makeSelectClaimIsNsfw,
} from 'lbry-redux';
import { selectShowMatureContent } from 'redux/selectors/settings';
const RECENT_HISTORY_AMOUNT = 10;
const HISTORY_ITEMS_PER_PAGE = 50;
@ -12,6 +18,12 @@ export const selectPlayingUri = createSelector(
state => state.playingUri
);
export const makeSelectIsPlaying = (uri: string) =>
createSelector(
selectPlayingUri,
playingUri => playingUri === uri
);
export const selectRewardContentClaimIds = createSelector(
selectState,
state => state.rewardedContentClaimIds
@ -85,3 +97,12 @@ export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string
return null;
}
);
export const makeSelectShouldObscurePreview = (uri: string) =>
createSelector(
selectShowMatureContent,
makeSelectClaimIsNsfw(uri),
(showMatureContent, isClaimMature) => {
return isClaimMature && !showMatureContent;
}
);

View file

@ -20,7 +20,7 @@ export const makeSelectClientSetting = setting =>
);
// refactor me
export const selectShowNsfw = makeSelectClientSetting(SETTINGS.SHOW_NSFW);
export const selectShowMatureContent = makeSelectClientSetting(SETTINGS.SHOW_NSFW);
export const selectLanguages = createSelector(
selectState,

View file

@ -81,11 +81,11 @@
right: var(--spacing-miniscule);
padding: 0.3rem;
transition: all var(--transition-duration) var(--transition-style);
border-radius: var(--card-radius);
&:hover {
background-color: $lbry-black;
color: $lbry-white;
border-radius: var(--card-radius);
}
}

View file

@ -193,3 +193,12 @@
@extend .card__title;
justify-content: space-between;
}
.card__media--nsfw {
background-color: $lbry-grape-3;
}
.card__media--disabled {
opacity: 0.5;
pointer-events: none;
}

View file

@ -213,6 +213,9 @@ $border-color--dark: var(--dm-color-04);
margin-right: auto;
padding-right: var(--spacing-medium);
font-size: larger;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.claim-preview-metadata {

View file

@ -1,14 +1,65 @@
.content__cover {
// Video thumbnail with play/download button
top: 0;
left: 0;
bottom: 0;
right: 0;
.content__viewer {
@extend .card;
position: absolute;
top: var(--spacing-large);
}
.content__viewer--floating {
position: fixed;
margin-bottom: 0;
height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
overflow: hidden;
left: calc(var(--spacing-large) + var(--spacing-small));
z-index: 9999;
&:hover {
.content__actions {
visibility: visible;
}
}
}
.content__wrapper {
position: relative;
width: 100%;
height: 100%;
}
.content__wrapper--floating {
height: var(--floating-viewer-height);
width: var(--floating-viewer-width);
}
.content__actions {
visibility: hidden;
position: absolute;
right: var(--spacing-small);
top: var(--spacing-small);
z-index: 1;
display: flex;
.button {
position: relative;
margin-left: var(--spacing-small);
}
}
.content__info {
cursor: grab;
height: var(--floating-viewer-info-height);
padding: var(--spacing-medium);
display: flex;
flex-direction: column;
align-items: flex-start;
white-space: nowrap;
}
.content__cover {
@include thumbnail;
position: relative;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100%;
@ -26,23 +77,6 @@
background-color: $lbry-teal-2;
}
}
}
.content__embedded {
@include thumbnail;
position: relative;
video {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
&:hover {
cursor: pointer;
}
}
&:-webkit-full-screen {
width: 100%;
@ -50,32 +84,14 @@
}
}
.content__empty {
@include thumbnail;
align-items: center;
background-color: $lbry-black;
color: $lbry-white;
display: flex;
justify-content: center;
width: 100%;
}
.content__empty--nsfw,
.card__media--nsfw {
background-color: $lbry-grape-3;
}
.card__media--disabled {
opacity: 0.5;
pointer-events: none;
}
.content__loading {
width: 100%;
height: 100%;
align-items: center;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 0 var(--spacing-large);
@ -85,32 +101,3 @@
.content__loading-text {
color: $lbry-white;
}
.content__view {
width: 100%;
height: 100%;
top: 0;
left: 0;
align-items: center;
display: flex;
justify-content: center;
position: absolute;
iframe {
width: 100%;
height: 100%;
background-color: $lbry-white;
}
}
.content__view--container {
background-color: black;
width: 100%;
height: 100%;
top: 0;
left: 0;
align-items: center;
display: flex;
justify-content: center;
position: absolute;
}

View file

@ -1,24 +1,8 @@
.file-render {
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
position: absolute;
[data-mode='dark'] & {
border: 1px solid rgba($lbry-gray-1, 0.3);
}
}
.file-render__viewer,
.document-viewer {
background-color: $lbry-white;
[data-mode='dark'] & {
background-color: transparent;
}
}
.file-render__viewer {
@ -27,27 +11,21 @@
iframe,
webview,
.video-js {
img {
width: 100%;
height: 100%;
}
// Removing the play button because we have autoplay turned on
// These are classes added by video.js
.video-js .vjs-big-play-button {
display: none;
object-fit: contain;
}
}
.file-render--pdf {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
.document-viewer {
.file-render__viewer--document {
@extend .file-render__viewer;
overflow: auto;
background-color: $lbry-white;
[data-mode='dark'] & {
background-color: transparent;
}
.markdown-preview {
height: 100%;
@ -56,18 +34,28 @@
}
}
.code-viewer,
.document-viewer__content {
.file-render__viewer--pdf {
@extend .file-render__viewer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
.file-render__content {
width: 100%;
height: 100%;
overflow: auto;
}
//
// Custom viewers live below here
// These either have custom class names that can't be changed or have styles that need to be ovverridden
//
// Code-viewer
.code-viewer .CodeMirror {
width: 100%;
height: 100%;
min-height: 100px;
.CodeMirror {
@extend .file-render__content;
.cm-invalidchar {
display: none;
@ -97,3 +85,14 @@
color: $lbry-gray-5;
}
}
.video-js {
height: 100%;
width: 100%;
// Removing the play button because we have autoplay turned on
// These are classes added by video.js
.vjs-big-play-button {
display: none;
}
}

View file

@ -1,5 +1,6 @@
.main-wrapper {
position: relative;
min-height: 100vh;
[data-mode='dark'] & {
background-color: var(--dm-color-08);

View file

@ -90,6 +90,7 @@
display: flex;
font-size: var(--font-multiplier-medium);
margin-bottom: var(--spacing-large);
line-height: 1;
> * {
&:not(:last-child) {

View file

@ -69,6 +69,11 @@ ul {
}
}
input,
label {
user-select: none;
}
blockquote {
margin-bottom: 1rem;
padding: 0.8rem;

View file

@ -20,10 +20,12 @@ $large-breakpoint: 1921px;
--file-page-max-width: 1787px;
--file-max-height: 788px;
--file-max-width: 1400px;
--video-aspect-ratio: 56.25%; // 9 x 16
--floating-viewer-width: 32rem;
--floating-viewer-height: 18rem; // 32 * 9/16
--floating-viewer-info-height: 5rem;
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
// Font
--font-multiplier-small: 0.9em;
--font-multiplier-medium: 1.1em;
--font-multiplier-large: 1.4em;

View file

@ -43,7 +43,7 @@ const fileInfoFilter = createFilter('fileInfo', [
'fileListDownloadedSort',
'fileListSubscriptionSort',
]);
const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpanded']);
const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpanded', 'volume', 'muted']);
// We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']);
const searchFilter = createFilter('search', ['options']);

View file

@ -1,7 +1,7 @@
// A simple function to detect if a user is typing:
// useful when hanlding shorcut keys.
export default function detectTyping() {
export default function isUserTyping() {
const activeElement = document.activeElement;
if (activeElement) {

View file

@ -1,35 +0,0 @@
import mime from 'mime';
const formats = [
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
[/\.(h|go|ja|java|js|jsx|c|cpp|cs|css|rb|scss|sh|php|py)$/i, 'script'],
[/\.(json|csv|txt|log|md|markdown|docx|pdf|xml|yml|yaml)$/i, 'document'],
[/\.(pdf|odf|doc|docx|epub|org|rtf)$/i, 'e-book'],
[/\.(stl|obj|fbx|gcode)$/i, '3D-file'],
[/\.(cbr|cbt|cbz)$/i, 'comic-book'],
];
export default function getMediaType(contentType, fileName) {
const extName = mime.getExtension(contentType);
const fileExt = extName ? `.${extName}` : null;
const testString = fileName || fileExt;
// Get mediaType from file extension
if (testString) {
const res = formats.reduce((ret, testpair) => {
const [regex, mediaType] = testpair;
return regex.test(ret) ? mediaType : ret;
}, testString);
if (res !== testString) return res;
}
// Get mediaType from contentType
if (contentType) {
return /^[^/]+/.exec(contentType)[0];
}
return 'unknown';
}

View file

@ -4,13 +4,19 @@ export default function usePersistedState(key, firstTimeDefault) {
// If no key is passed in, act as a normal `useState`
let defaultValue;
if (key) {
const item = localStorage.getItem(key);
if (item === 'true') {
defaultValue = true;
} else if (item === 'false') {
defaultValue = false;
} else {
defaultValue = item;
let item = localStorage.getItem(key);
if (item) {
let parsedItem;
try {
parsedItem = JSON.parse(item);
} catch (e) {}
if (parsedItem) {
defaultValue = parsedItem;
} else {
defaultValue = item;
}
}
}
@ -22,7 +28,7 @@ export default function usePersistedState(key, firstTimeDefault) {
useEffect(() => {
if (key) {
localStorage.setItem(key, value);
localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value);
}
}, [key, value]);

116
yarn.lock
View file

@ -1850,11 +1850,6 @@ binary-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
binary-search@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.5.tgz#479ad009589e0273cf54e5d74ab1546c489078ce"
integrity sha512-RHFP0AdU6KAB0CCZsRMU2CJTk2EpL8GLURT+4gilpjr1f/7M91FgUMnXuQLmf3OKLet34gjuNFwO7e4agdX5pw==
binary@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
@ -2074,7 +2069,7 @@ buffer-alloc-unsafe@^1.1.0:
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.1.0, buffer-alloc@^1.2.0:
buffer-alloc@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
@ -2092,7 +2087,7 @@ buffer-fill@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
buffer-from@^1.0.0, buffer-from@^1.1.0:
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@ -6090,11 +6085,6 @@ is-arrayish@^0.3.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-ascii@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929"
integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk=
is-binary-path@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@ -6772,10 +6762,11 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#8f12baa88f6f057eb3b7d0cf04d6e4bb0eb11763:
lbry-redux@lbryio/lbry-redux#05e70648e05c51c51710f6dd698a8e2219b54df2:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/8f12baa88f6f057eb3b7d0cf04d6e4bb0eb11763"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/05e70648e05c51c51710f6dd698a8e2219b54df2"
dependencies:
mime "^2.4.4"
proxy-polyfill "0.1.6"
reselect "^3.0.0"
uuid "^3.3.2"
@ -7329,15 +7320,6 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
mediasource@^2.1.0, mediasource@^2.2.2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/mediasource/-/mediasource-2.3.0.tgz#4c7b49e7ea4fb88f1cc181d8fcf0d94649271dc6"
integrity sha512-fqm86UwHvAnneIv40Uy1sDQaFtAByq/k0SQ3uCtbnEeSQNT1s5TDHCZOD1VmYCHwfY1jL2NjoZVwzZKYqy3L7A==
dependencies:
inherits "^2.0.1"
readable-stream "^3.0.0"
to-arraybuffer "^1.0.1"
mem@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
@ -7597,26 +7579,6 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mp4-box-encoding@^1.1.0, mp4-box-encoding@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.3.0.tgz#2a6f750947ff68c3a498fd76cd6424c53d995d48"
integrity sha512-U4pMLpjT/UzB8d36dxj6Mf1bG9xypEvgbuRIa1fztRXNKKTCAtRxsnFZhNOd7YDFOKtjBgssYGvo4H/Q3ZY1MA==
dependencies:
buffer-alloc "^1.2.0"
buffer-from "^1.1.0"
uint64be "^2.0.2"
mp4-stream@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-2.0.3.tgz#30acee07709d323f8dcd87a07b3ce9c3c4bfb364"
integrity sha512-5NzgI0+bGakoZEwnIYINXqB3mnewkt3Y7jcvkXsTubnCNUSdM8cpP0Vemxf6FLg0qUN8fydTgNMVAc3QU8B92g==
dependencies:
buffer-alloc "^1.1.0"
inherits "^2.0.1"
mp4-box-encoding "^1.1.0"
next-event "^1.0.0"
readable-stream "^2.0.3"
mpd-parser@0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.7.0.tgz#d36e3322579fce23d657f71a3c2f3e6cc5ce4002"
@ -7648,14 +7610,6 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
multistream@^2.0.2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c"
integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==
dependencies:
inherits "^2.0.1"
readable-stream "^2.0.5"
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
@ -7722,11 +7676,6 @@ neo-async@^2.5.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
next-event@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-event/-/next-event-1.0.0.tgz#e7778acde2e55802e0ad1879c39cf6f75eda61d8"
integrity sha1-53eKzeLlWALgrRh5w5z2917aYdg=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -9561,13 +9510,6 @@ range-parser@^1.2.1, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
range-slice-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/range-slice-stream/-/range-slice-stream-2.0.0.tgz#1f25fc7a2cacf9ccd140c46f9cf670a1a7fe3ce6"
integrity sha512-PPYLwZ63lXi6Tv2EZ8w3M4FzC0rVqvxivaOVS8pXSp5FMIHFnvi4MWHL3UdFLhwSy50aNtJsgjY0mBC6oFL26Q==
dependencies:
readable-stream "^3.0.2"
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
@ -9624,6 +9566,14 @@ react-dom@^16.8.2, react-dom@^16.8.6:
prop-types "^15.6.2"
scheduler "^0.13.6"
react-draggable@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.3.0.tgz#2ed7ea3f92e7d742d747f9e6324860606cd4d997"
integrity sha512-U7/jD0tAW4T0S7DCPK0kkKLyL0z61sC/eqU+NUfDjnq+JtBKaYKDHpsK2wazctiA4alEzCXUnzkREoxppOySVw==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-ga@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.7.tgz#1c80a289004bf84f84c26d46f3a6a6513081bf2e"
@ -9825,7 +9775,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
@ -9838,7 +9788,7 @@ read-pkg@^2.0.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1:
readable-stream@^3.0.6, readable-stream@^3.1.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9"
integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==
@ -10107,17 +10057,6 @@ remove-trailing-separator@^1.0.1:
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
render-media@^3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/render-media/-/render-media-3.1.3.tgz#aa8c8cd3f720049370067180709b551d3c566254"
integrity sha512-K7ziKKlIcgYpAovRsABDiSaNn7TzDDyyuFGpRwM52cloNcajInB6sCxFPUEzOuTJUeyvKCqT/k5INOjpKLCjhQ==
dependencies:
debug "^3.1.0"
is-ascii "^1.0.0"
mediasource "^2.1.0"
stream-to-blob-url "^2.0.0"
videostream "^2.5.1"
renderkid@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
@ -10988,7 +10927,7 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.1:
stream-to-blob-url@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
integrity sha512-DKJPEmCmIZoBfGVle9IhSfERiWaN5cuOtmfPxP2dZbLDRZxkBWZ4QbYxEJOSALk1Kf+WjBgedAMO6qkkf7Lmrg==
@ -11455,7 +11394,7 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
to-arraybuffer@^1.0.0, to-arraybuffer@^1.0.1:
to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
@ -11672,13 +11611,6 @@ uglify-js@3.4.x:
commander "~2.19.0"
source-map "~0.6.1"
uint64be@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5"
integrity sha512-9QqdvpGQTXgxthP+lY4e/gIBy+RuqcBaC6JVwT5I3bDLgT/btL6twZMR0pI3/Fgah9G/pdwzIprE5gL6v9UvyQ==
dependencies:
buffer-alloc "^1.1.0"
unbzip2-stream@^1.0.9:
version "1.3.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
@ -12120,20 +12052,6 @@ videojs-vtt.js@0.14.1:
dependencies:
global "^4.3.1"
videostream@^2.5.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/videostream/-/videostream-2.6.0.tgz#7f0b2b84bc457c12cfe599aa2345f5cc06241ab6"
integrity sha512-nSsullx1BYClJxVSt4Fa+Ulsv0Cf7UwaHq+4LQdLkAUdmqNhY1DlGxXDWVY2gui5XV4FvDiSbXmSbGryMrrUCQ==
dependencies:
binary-search "^1.3.4"
inherits "^2.0.1"
mediasource "^2.2.2"
mp4-box-encoding "^1.3.0"
mp4-stream "^2.0.0"
multistream "^2.0.2"
pump "^3.0.0"
range-slice-stream "^2.0.0"
villain@btzr-io/Villain:
version "0.0.7"
resolved "https://codeload.github.com/btzr-io/Villain/tar.gz/1f39a679cd78b08f8acc0b36615550eb91f6ee03"