oh boy
This commit is contained in:
parent
c68d7edec5
commit
f25559adfb
40 changed files with 1052 additions and 1517 deletions
27
flow-typed/npm/mime_v2.x.x.js
vendored
27
flow-typed/npm/mime_v2.x.x.js
vendored
|
@ -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;
|
|
||||||
}
|
|
3
flow-typed/render-media.js
vendored
3
flow-typed/render-media.js
vendored
|
@ -1,3 +0,0 @@
|
||||||
declare module 'render-media' {
|
|
||||||
declare module.exports: any;
|
|
||||||
}
|
|
|
@ -125,14 +125,13 @@
|
||||||
"jsmediatags": "^3.8.1",
|
"jsmediatags": "^3.8.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#8f12baa88f6f057eb3b7d0cf04d6e4bb0eb11763",
|
"lbry-redux": "lbryio/lbry-redux#1b7bb1cc9f2cb6a8efcce1869031d4da8ddbf4ca",
|
||||||
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb",
|
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
"make-runnable": "^1.3.6",
|
"make-runnable": "^1.3.6",
|
||||||
"mammoth": "^1.4.6",
|
"mammoth": "^1.4.6",
|
||||||
"mime": "^2.3.1",
|
|
||||||
"moment": "^2.22.0",
|
"moment": "^2.22.0",
|
||||||
"node-abi": "^2.5.1",
|
"node-abi": "^2.5.1",
|
||||||
"node-fetch": "^2.3.0",
|
"node-fetch": "^2.3.0",
|
||||||
|
@ -169,7 +168,6 @@
|
||||||
"remark-attr": "^0.8.3",
|
"remark-attr": "^0.8.3",
|
||||||
"remark-emoji": "^2.0.1",
|
"remark-emoji": "^2.0.1",
|
||||||
"remark-react": "^4.0.3",
|
"remark-react": "^4.0.3",
|
||||||
"render-media": "^3.1.0",
|
|
||||||
"reselect": "^3.0.0",
|
"reselect": "^3.0.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
selectChannelIsBlocked,
|
selectChannelIsBlocked,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
|
@ -20,7 +20,7 @@ import ClaimPreview from './view';
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
obscureNsfw: !selectShowMatureContent(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { Fragment, useEffect } from 'react';
|
import React, { Fragment, useEffect, forwardRef } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { parseURI, convertToShareLink } from 'lbry-redux';
|
import { parseURI, convertToShareLink } from 'lbry-redux';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
@ -46,7 +46,7 @@ type Props = {
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimPreview(props: Props) {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
const {
|
const {
|
||||||
obscureNsfw,
|
obscureNsfw,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
@ -150,6 +150,7 @@ function ClaimPreview(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
ref={ref}
|
||||||
role="link"
|
role="link"
|
||||||
onClick={pending || type === 'inline' ? undefined : onClick}
|
onClick={pending || type === 'inline' ? undefined : onClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
@ -209,6 +210,6 @@ function ClaimPreview(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default withRouter(ClaimPreview);
|
export default withRouter(ClaimPreview);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: string,
|
status?: string,
|
||||||
spinner: boolean,
|
spinner: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,11 @@ class FileDetails extends PureComponent<Props> {
|
||||||
: fileInfo && fileInfo.download_path && formatBytes(fileInfo.written_bytes);
|
: fileInfo && fileInfo.download_path && formatBytes(fileInfo.written_bytes);
|
||||||
let downloadPath = fileInfo && fileInfo.download_path ? path.normalize(fileInfo.download_path) : null;
|
let downloadPath = fileInfo && fileInfo.download_path ? path.normalize(fileInfo.download_path) : null;
|
||||||
let downloadNote;
|
let downloadNote;
|
||||||
// If the path is blank, file is not avialable. Create path from name so the folder opens on click.
|
// If the path is blank, file is not avialable. Streamed files won't have any blobs saved
|
||||||
if (fileInfo && fileInfo.download_path === null) {
|
// 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}`;
|
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 (
|
return (
|
||||||
|
|
|
@ -3,28 +3,23 @@ import {
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
makeSelectDownloadingForUri,
|
makeSelectDownloadingForUri,
|
||||||
makeSelectLoadingForUri,
|
makeSelectLoadingForUri,
|
||||||
makeSelectClaimForUri,
|
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectUriIsStreamable,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import { doPurchaseUri, doStartDownload, doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import FileDownloadLink from './view';
|
import FileDownloadLink from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
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),
|
downloading: makeSelectDownloadingForUri(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
|
||||||
loading: makeSelectLoadingForUri(props.uri)(state),
|
loading: makeSelectLoadingForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
|
|
||||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
|
||||||
pause: () => dispatch(doSetPlayingUri(null)),
|
pause: () => dispatch(doSetPlayingUri(null)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,104 +4,46 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ToolTip from 'component/common/tooltip';
|
import ToolTip from 'component/common/tooltip';
|
||||||
import analytics from 'analytics';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: StreamClaim,
|
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
uri: string,
|
|
||||||
downloading: boolean,
|
downloading: boolean,
|
||||||
fileInfo: ?{
|
|
||||||
written_bytes: number,
|
|
||||||
total_bytes: number,
|
|
||||||
outpoint: number,
|
|
||||||
download_path: string,
|
|
||||||
completed: boolean,
|
|
||||||
status: string,
|
|
||||||
},
|
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
costInfo: ?{},
|
isStreamable: boolean,
|
||||||
restartDownload: (string, number) => void,
|
fileInfo: ?FileInfo,
|
||||||
openModal: (id: string, { path: string }) => void,
|
openModal: (id: string, { path: string }) => void,
|
||||||
purchaseUri: string => void,
|
|
||||||
pause: () => void,
|
pause: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileDownloadLink extends React.PureComponent<Props> {
|
function FileDownloadLink(props: Props) {
|
||||||
componentDidMount() {
|
const { fileInfo, downloading, loading, openModal, pause, claimIsMine, isStreamable } = props;
|
||||||
const { fileInfo, uri, restartDownload } = this.props;
|
|
||||||
if (
|
if (!isStreamable && (loading || downloading)) {
|
||||||
fileInfo &&
|
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
|
||||||
!fileInfo.completed &&
|
const label =
|
||||||
fileInfo.status === 'running' &&
|
fileInfo && fileInfo.written_bytes > 0
|
||||||
fileInfo.written_bytes !== false &&
|
? __('Downloading: ') + progress.toFixed(0) + __('% complete')
|
||||||
fileInfo.written_bytes < fileInfo.total_bytes
|
: __('Connecting...');
|
||||||
) {
|
|
||||||
// This calls file list to show the percentage
|
return <span>{label}</span>;
|
||||||
restartDownload(uri, fileInfo.outpoint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uri: ?string;
|
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
||||||
|
return (
|
||||||
render() {
|
<ToolTip label={__('Open file')}>
|
||||||
const {
|
<Button
|
||||||
fileInfo,
|
button="link"
|
||||||
downloading,
|
icon={ICONS.EXTERNAL}
|
||||||
uri,
|
onClick={() => {
|
||||||
openModal,
|
pause();
|
||||||
purchaseUri,
|
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { path: fileInfo.download_path, isMine: claimIsMine });
|
||||||
costInfo,
|
}}
|
||||||
loading,
|
/>
|
||||||
pause,
|
</ToolTip>
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileDownloadLink;
|
export default FileDownloadLink;
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectContentTypeForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
|
makeSelectMediaTypeForUri,
|
||||||
|
makeSelectDownloadPathForUri,
|
||||||
|
makeSelectFileNameForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
import { THEME } from 'constants/settings';
|
import { THEME } from 'constants/settings';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
import FileRender from './view';
|
import FileRender from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state, props) => ({
|
||||||
currentTheme: makeSelectClientSetting(THEME)(state),
|
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);
|
export default connect(select)(FileRender);
|
||||||
|
|
|
@ -3,19 +3,16 @@ import { remote } from 'electron';
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import VideoViewer from 'component/viewers/videoViewer';
|
import VideoViewer from 'component/viewers/videoViewer';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
// Audio player on hold until the current player is dropped
|
// This is half complete, the video viewer works fine for audio, it just doesn't look pretty
|
||||||
// This component is half working
|
|
||||||
// const AudioViewer = React.lazy<*>(() =>
|
// const AudioViewer = React.lazy<*>(() =>
|
||||||
// import(
|
// import(
|
||||||
// /* webpackChunkName: "audioViewer" */
|
// /* webpackChunkName: "audioViewer" */
|
||||||
// 'component/viewers/audioViewer'
|
// 'component/viewers/audioViewer'
|
||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
// const AudioViewer = React.lazy<*>(() =>
|
|
||||||
// import(/* webpackChunkName: "audioViewer" */
|
|
||||||
// 'component/viewers/audioViewer')
|
|
||||||
// );
|
|
||||||
|
|
||||||
const DocumentViewer = React.lazy<*>(() =>
|
const DocumentViewer = React.lazy<*>(() =>
|
||||||
import(
|
import(
|
||||||
|
@ -63,17 +60,12 @@ const ThreeViewer = React.lazy<*>(() =>
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mediaType: string,
|
mediaType: string,
|
||||||
poster?: string,
|
streamingUrl: string,
|
||||||
|
contentType: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
source: {
|
|
||||||
stream: string => void,
|
|
||||||
fileName: string,
|
|
||||||
fileType: string,
|
|
||||||
contentType: string,
|
|
||||||
downloadPath: string,
|
|
||||||
url: ?string,
|
|
||||||
},
|
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
|
downloadPath?: string,
|
||||||
|
fileName?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileRender extends React.PureComponent<Props> {
|
class FileRender extends React.PureComponent<Props> {
|
||||||
|
@ -85,6 +77,23 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('keydown', this.escapeListener, true);
|
window.addEventListener('keydown', this.escapeListener, true);
|
||||||
|
|
||||||
|
// ugh
|
||||||
|
// const { claim, streamingUrl, fileStatus, fileName, downloadPath, downloadCompleted, 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 source = {url: `${MediaPlayer.SANDBOX_CONTENT_BASE_URL}${url}`};
|
||||||
|
// this.setState({source});
|
||||||
|
// })
|
||||||
|
// .catch(err => {
|
||||||
|
// console.error(err);
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// File to render
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -92,39 +101,39 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should use React.createRef()
|
// This should use React.createRef()
|
||||||
processSandboxRef(element: any) {
|
// processSandboxRef(element: any) {
|
||||||
if (!element) {
|
// if (!element) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
window.sandbox = element;
|
// window.sandbox = element;
|
||||||
|
|
||||||
element.addEventListener('permissionrequest', e => {
|
// element.addEventListener('permissionrequest', e => {
|
||||||
console.log('permissionrequest', e);
|
// console.log('permissionrequest', e);
|
||||||
});
|
// });
|
||||||
|
|
||||||
element.addEventListener('console-message', (e: { message: string }) => {
|
// element.addEventListener('console-message', (e: { message: string }) => {
|
||||||
if (/^\$LBRY_IPC:/.test(e.message)) {
|
// if (/^\$LBRY_IPC:/.test(e.message)) {
|
||||||
// Process command
|
// // Process command
|
||||||
let message = {};
|
// let message = {};
|
||||||
try {
|
// try {
|
||||||
// $FlowFixMe
|
// // $FlowFixMe
|
||||||
message = JSON.parse(/^\$LBRY_IPC:(.*)/.exec(e.message)[1]);
|
// message = JSON.parse(/^\$LBRY_IPC:(.*)/.exec(e.message)[1]);
|
||||||
} catch (err) {}
|
// } catch (err) {}
|
||||||
console.log('IPC', message);
|
// console.log('IPC', message);
|
||||||
} else {
|
// } else {
|
||||||
console.log('Sandbox:', e.message);
|
// console.log('Sandbox:', e.message);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
element.addEventListener('enter-html-full-screen', () => {
|
// element.addEventListener('enter-html-full-screen', () => {
|
||||||
// stub
|
// // stub
|
||||||
});
|
// });
|
||||||
|
|
||||||
element.addEventListener('leave-html-full-screen', () => {
|
// element.addEventListener('leave-html-full-screen', () => {
|
||||||
// stub
|
// // stub
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
escapeListener(e: SyntheticKeyboardEvent<*>) {
|
escapeListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
if (e.keyCode === 27) {
|
if (e.keyCode === 27) {
|
||||||
|
@ -141,10 +150,9 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderViewer() {
|
renderViewer() {
|
||||||
const { source, mediaType, currentTheme, poster, claim } = this.props;
|
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl } = this.props;
|
||||||
|
|
||||||
// Extract relevant data to render file
|
const fileType = fileName && path.extname(fileName).substring(1);
|
||||||
const { stream, fileType, contentType, downloadPath, fileName } = source;
|
|
||||||
|
|
||||||
// Human-readable files (scripts and plain-text files)
|
// Human-readable files (scripts and plain-text files)
|
||||||
const readableFiles = ['text', 'document', 'script'];
|
const readableFiles = ['text', 'document', 'script'];
|
||||||
|
@ -154,25 +162,30 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
||||||
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
||||||
|
// 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"
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
application: !source.url ? null : (
|
video: <VideoViewer source={streamingUrl} contentType={contentType} />,
|
||||||
<webview
|
audio: <VideoViewer source={streamingUrl} contentType={contentType} />,
|
||||||
ref={element => this.processSandboxRef(element)}
|
// audio: (
|
||||||
title=""
|
// <AudioViewer
|
||||||
sandbox="allow-scripts allow-forms allow-pointer-lock"
|
// claim={claim}
|
||||||
src={source.url}
|
// source={{ url: streamingUrl, downloadPath, downloadCompleted, status }}
|
||||||
autosize="on"
|
// contentType={contentType}
|
||||||
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} />,
|
|
||||||
// Add routes to viewer...
|
// Add routes to viewer...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -189,7 +202,16 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
|
|
||||||
// Check for Human-readable files
|
// Check for Human-readable files
|
||||||
if (!viewer && readableFiles.includes(mediaType)) {
|
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'
|
// @if TARGET='web'
|
||||||
|
@ -204,7 +226,7 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
// Message Error
|
// Message Error
|
||||||
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
|
const unsupportedMessage = __("We can't preview this file.");
|
||||||
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
|
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
|
||||||
|
|
||||||
// Return viewer
|
// Return viewer
|
||||||
|
|
|
@ -1,52 +1,27 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as settings from 'constants/settings';
|
import { doPlayUri } from 'redux/actions/content';
|
||||||
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 {
|
import {
|
||||||
makeSelectMetadataForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
makeSelectClaimForUri,
|
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
makeSelectLoadingForUri,
|
|
||||||
makeSelectDownloadingForUri,
|
|
||||||
makeSelectFirstRecommendedFileForUri,
|
|
||||||
makeSelectClaimIsNsfw,
|
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
|
makeSelectMediaTypeForUri,
|
||||||
|
makeSelectUriIsStreamable,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
|
import { makeSelectIsPlaying, makeSelectShouldObscurePreview } from 'redux/selectors/content';
|
||||||
import { selectPlayingUri, makeSelectContentPositionForUri } from 'redux/selectors/content';
|
|
||||||
import { selectFileInfoErrors } from 'redux/selectors/file_info';
|
|
||||||
import FileViewer from './view';
|
import FileViewer from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
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),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
muted: selecetMute(state),
|
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||||
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
|
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
play: uri => dispatch(doPlayUri(uri)),
|
play: (uri, saveFile) => dispatch(doPlayUri(uri, saveFile)),
|
||||||
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)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -1,323 +1,108 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { ElementRef } from 'react';
|
import React, { Fragment, useEffect, useCallback } from 'react';
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React, { Suspense } from 'react';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import analytics from 'analytics';
|
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import PlayButton from './internal/play-button';
|
import Button from 'component/button';
|
||||||
import detectTyping from 'util/detect-typing';
|
import FileRender from 'component/fileRender';
|
||||||
|
import isUserTyping from 'util/detect-typing';
|
||||||
const Player = React.lazy(() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "player-legacy" */
|
|
||||||
'./internal/player'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const SPACE_BAR_KEYCODE = 32;
|
const SPACE_BAR_KEYCODE = 32;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cancelPlay: () => void,
|
play: (string, boolean) => 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,
|
mediaType: string,
|
||||||
claimRewards: () => void,
|
isLoading: boolean,
|
||||||
nextFileToPlay: ?string,
|
isPlaying: boolean,
|
||||||
navigate: (string, {}) => void,
|
fileInfo: FileInfo,
|
||||||
costInfo: ?{ cost: number },
|
uri: string,
|
||||||
|
obscurePreview: boolean,
|
||||||
insufficientCredits: boolean,
|
insufficientCredits: boolean,
|
||||||
nsfw: boolean,
|
isStreamable: boolean,
|
||||||
thumbnail: ?string,
|
thumbnail?: string,
|
||||||
isPlayableType: boolean,
|
streamingUrl?: string,
|
||||||
viewerContainer: { current: ElementRef<any> },
|
|
||||||
changeMute: boolean => void,
|
|
||||||
muted: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileViewer extends React.PureComponent<Props> {
|
export default function FileViewer(props: Props) {
|
||||||
constructor() {
|
const {
|
||||||
super();
|
play,
|
||||||
(this: any).playContent = this.playContent.bind(this);
|
mediaType,
|
||||||
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
|
isPlaying,
|
||||||
(this: any).logTimeToStart = this.logTimeToStart.bind(this);
|
fileInfo,
|
||||||
(this: any).onFileFinishCb = this.onFileFinishCb.bind(this);
|
uri,
|
||||||
(this: any).onFileStartCb = undefined;
|
obscurePreview,
|
||||||
|
insufficientCredits,
|
||||||
|
thumbnail,
|
||||||
|
streamingUrl,
|
||||||
|
isStreamable,
|
||||||
|
// Add this back for full-screen support
|
||||||
|
// viewerContainer,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// Don't add these variables to state because we don't need to re-render when their values change
|
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||||
(this: any).startTime = undefined;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
(this: any).playTime = undefined;
|
const isReadyToPlay = (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() {
|
// Wrap this in useCallback because we need to use it to the keyboard effect
|
||||||
const { fileInfo } = this.props;
|
// 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
|
||||||
if (!fileInfo) {
|
const viewFile = useCallback(
|
||||||
this.onFileStartCb = this.logTimeToStart;
|
(e: SyntheticInputEvent<*> | KeyboardEvent) => {
|
||||||
}
|
e.stopPropagation();
|
||||||
|
|
||||||
this.handleAutoplay(this.props);
|
// Check for user setting here
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
const saveFile = !isStreamable;
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prev: Props) {
|
play(uri, saveFile);
|
||||||
const { fileInfo } = this.props;
|
},
|
||||||
|
[play, uri, isStreamable]
|
||||||
|
);
|
||||||
|
|
||||||
if (this.props.uri !== prev.uri) {
|
useEffect(() => {
|
||||||
// User just directly navigated to another piece of content
|
// This is just for beginning to download a file
|
||||||
if (this.startTime && !this.playTime) {
|
// Play/Pause/Fullscreen will be handled by the respective viewers because not every file type should behave the same
|
||||||
// They started playing a file but it didn't start streaming
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
// Fire the analytics event with the previous file
|
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||||
this.fireAnalyticsEvent(prev.claim);
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
this.startTime = null;
|
if (!isPlaying || fileStatus === 'stopped') {
|
||||||
this.playTime = null;
|
viewFile(e);
|
||||||
|
}
|
||||||
// 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 (
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
this.props.autoplay !== prev.autoplay ||
|
return () => {
|
||||||
this.props.fileInfo !== prev.fileInfo ||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
this.props.isDownloading !== prev.isDownloading ||
|
};
|
||||||
this.props.playingUri !== prev.playingUri
|
}, [isPlaying, fileStatus, viewFile]);
|
||||||
) {
|
|
||||||
// suppress autoplay after download error
|
|
||||||
if (!this.props.fileInfoErrors || !(this.props.uri in this.props.fileInfoErrors)) {
|
|
||||||
this.handleAutoplay(this.props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
return (
|
||||||
const { claim } = this.props;
|
<div
|
||||||
|
onClick={viewFile}
|
||||||
|
style={!obscurePreview && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
|
className={classnames('video content__cover content__embedded', {
|
||||||
|
'card__media--nsfw': obscurePreview,
|
||||||
|
'card__media--disabled': !fileInfo && insufficientCredits,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isPlaying && (
|
||||||
|
<Fragment>{isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={loadingMessage} />}</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
if (this.startTime && !this.playTime) {
|
{!isPlaying && (
|
||||||
// The user is navigating away before the file started playing, or a play time was never set
|
<Button
|
||||||
// Currently will not be set for files that don't use render-media
|
onClick={viewFile}
|
||||||
this.fireAnalyticsEvent(claim);
|
iconSize={30}
|
||||||
}
|
title={isPlayable ? __('Play') : __('View')}
|
||||||
|
className={classnames('button--icon', {
|
||||||
this.props.cancelPlay();
|
'button--play': isPlayable,
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
'button--view': !isPlayable,
|
||||||
}
|
})}
|
||||||
|
/>
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
)}
|
||||||
if (!detectTyping()) {
|
</div>
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectNsfwCountForChannel, makeSelectNsfwCountFromUris, parseURI } from 'lbry-redux';
|
import { makeSelectNsfwCountForChannel, makeSelectNsfwCountFromUris, parseURI } from 'lbry-redux';
|
||||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import HiddenNsfwClaims from './view';
|
import HiddenNsfwClaims from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -18,7 +18,7 @@ const select = (state, props) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
numberOfNsfwClaims,
|
numberOfNsfwClaims,
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
obscureNsfw: !selectShowMatureContent(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as MODALS from 'constants/modal_types';
|
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 * as React from 'react';
|
||||||
import getMediaType from 'util/get-media-type';
|
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import FileSelector from 'component/common/file-selector';
|
import FileSelector from 'component/common/file-selector';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -64,7 +63,7 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const { thumbnailError } = this.state;
|
const { thumbnailError } = this.state;
|
||||||
|
|
||||||
const isSupportedVideo = getMediaType(null, filePath) === 'video';
|
const isSupportedVideo = Lbry.getMediaType(null, filePath) === 'video';
|
||||||
|
|
||||||
let thumbnailSrc;
|
let thumbnailSrc;
|
||||||
if (!thumbnail) {
|
if (!thumbnail) {
|
||||||
|
|
300
src/ui/component/viewers/audioViewer.jsx
Normal file
300
src/ui/component/viewers/audioViewer.jsx
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
import { stopContextMenu } from 'util/context-menu';
|
||||||
|
import butterchurn from 'butterchurn';
|
||||||
|
import detectButterchurnSupport from 'butterchurn/lib/isSupported.min';
|
||||||
|
import butterchurnPresets from 'butterchurn-presets';
|
||||||
|
import jsmediatags from 'jsmediatags/dist/jsmediatags';
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
|
||||||
|
import styles from './audioViewer.module.scss';
|
||||||
|
|
||||||
|
const isButterchurnSupported = detectButterchurnSupport();
|
||||||
|
|
||||||
|
const EQ_BANDS_SIMPLE = [55, 150, 250, 400, 500, 1000, 2000, 4000, 8000, 16000];
|
||||||
|
/*
|
||||||
|
const EQ_LOWSHELF = EQ_BANDS_SIMPLE.shift();
|
||||||
|
const EQ_HIGHSHELF = EQ_BANDS_SIMPLE.pop();
|
||||||
|
|
||||||
|
const eqFilters = EQ.map(function(band) {
|
||||||
|
var filter = wavesurfer.backend.ac.createBiquadFilter();
|
||||||
|
filter.type = 'peaking';
|
||||||
|
filter.gain.value = 0;
|
||||||
|
filter.Q.value = 1;
|
||||||
|
filter.frequency.value = band.f;
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
source: {
|
||||||
|
url: string,
|
||||||
|
stream: string => void,
|
||||||
|
downloadCompleted: string,
|
||||||
|
downloadPath: string,
|
||||||
|
status: string,
|
||||||
|
},
|
||||||
|
contentType: string,
|
||||||
|
poster?: string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
};
|
||||||
|
|
||||||
|
const presets = [
|
||||||
|
require('butterchurn-presets/presets/converted/Flexi - when monopolies were the future [simple warp + non-reactive moebius].json'),
|
||||||
|
require('butterchurn-presets/presets/converted/Rovastar & Loadus - FractalDrop (Active Sparks Mix).json'),
|
||||||
|
require('butterchurn-presets/presets/converted/shifter - tumbling cubes (ripples).json'),
|
||||||
|
require('butterchurn-presets/presets/converted/ORB - Blue Emotion.json'),
|
||||||
|
require('butterchurn-presets/presets/converted/shifter - urchin mod.json'),
|
||||||
|
require('butterchurn-presets/presets/converted/Stahlregen & fishbrain + flexi + geiss - The Machine that conquered the Aether.json'),
|
||||||
|
require('butterchurn-presets/presets/converted/Zylot - Crosshair Dimension (Light of Ages).json'),
|
||||||
|
];
|
||||||
|
|
||||||
|
class AudioVideoViewer extends React.PureComponent {
|
||||||
|
// audioNode: ?HTMLAudioElement;
|
||||||
|
// player: ?{ dispose: () => void };
|
||||||
|
|
||||||
|
state = {
|
||||||
|
playing: false,
|
||||||
|
enableMilkdrop: isButterchurnSupported,
|
||||||
|
showEqualizer: false,
|
||||||
|
showSongDetails: true,
|
||||||
|
enableArt: true,
|
||||||
|
artLoaded: false,
|
||||||
|
artist: null,
|
||||||
|
title: null,
|
||||||
|
album: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const me = this;
|
||||||
|
const { contentType, poster, claim, source } = me.props;
|
||||||
|
|
||||||
|
const path = source.downloadCompleted ? source.downloadPath : source.url;
|
||||||
|
const sources = [
|
||||||
|
{
|
||||||
|
src: path,
|
||||||
|
type: contentType,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const audioNode = this.audioNode;
|
||||||
|
|
||||||
|
audioNode.crossOrigin = 'anonymous';
|
||||||
|
audioNode.autostart = true;
|
||||||
|
|
||||||
|
const canvasHeight = me.canvasNode.offsetHeight;
|
||||||
|
const canvasWidth = me.canvasNode.offsetWidth;
|
||||||
|
|
||||||
|
// Required for canvas, nuance of rendering
|
||||||
|
me.canvasNode.height = canvasHeight;
|
||||||
|
me.canvasNode.width = canvasWidth;
|
||||||
|
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
|
||||||
|
const audioSource = audioContext.createMediaElementSource(audioNode);
|
||||||
|
audioSource.connect(audioContext.destination);
|
||||||
|
|
||||||
|
if (isButterchurnSupported) {
|
||||||
|
const visualizer = (me.visualizer = butterchurn.createVisualizer(audioContext, me.canvasNode, {
|
||||||
|
height: canvasHeight,
|
||||||
|
width: canvasWidth,
|
||||||
|
pixelRatio: window.devicePixelRatio || 1,
|
||||||
|
textureRatio: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
visualizer.connectAudio(audioSource);
|
||||||
|
visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
|
||||||
|
|
||||||
|
me._frameCycle = () => {
|
||||||
|
requestAnimationFrame(me._frameCycle);
|
||||||
|
|
||||||
|
if (me.state.enableMilkdrop === true) {
|
||||||
|
visualizer.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
me._frameCycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
const wavesurfer = WaveSurfer.create({
|
||||||
|
barWidth: 3,
|
||||||
|
container: this.waveNode,
|
||||||
|
waveColor: '#000',
|
||||||
|
progressColor: '#fff',
|
||||||
|
mediaControls: true,
|
||||||
|
responsive: true,
|
||||||
|
normalize: true,
|
||||||
|
backend: 'MediaElement',
|
||||||
|
minPxPerSec: 100,
|
||||||
|
height: this.waveNode.offsetHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurfer.load(audioNode);
|
||||||
|
|
||||||
|
jsmediatags.Config.setDisallowedXhrHeaders(['If-Modified-Since', 'Range']);
|
||||||
|
jsmediatags.read(path, {
|
||||||
|
onSuccess: function(result) {
|
||||||
|
const { album, artist, title, picture } = result.tags;
|
||||||
|
|
||||||
|
if (picture) {
|
||||||
|
const byteArray = new Uint8Array(picture.data);
|
||||||
|
const blob = new Blob([byteArray], { type: picture.type });
|
||||||
|
const albumArtUrl = URL.createObjectURL(blob);
|
||||||
|
me.artNode.src = albumArtUrl;
|
||||||
|
|
||||||
|
me.setState({ artLoaded: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
me.setState({
|
||||||
|
album,
|
||||||
|
artist,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: function(error) {
|
||||||
|
console.log(':(', error.type, error.info);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the render loop
|
||||||
|
this._frameCycle = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const me = this;
|
||||||
|
const { contentType, poster, claim, source } = me.props;
|
||||||
|
const {
|
||||||
|
album,
|
||||||
|
artist,
|
||||||
|
title,
|
||||||
|
enableMilkdrop,
|
||||||
|
showEqualizer,
|
||||||
|
showSongDetails,
|
||||||
|
enableArt,
|
||||||
|
artLoaded,
|
||||||
|
playing,
|
||||||
|
userActive,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const renderArt = enableArt && artLoaded;
|
||||||
|
|
||||||
|
const path = source.downloadCompleted ? source.downloadPath : source.url;
|
||||||
|
|
||||||
|
const playButton = (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
const audioNode = this.audioNode;
|
||||||
|
if (audioNode.paused) {
|
||||||
|
audioNode.play();
|
||||||
|
} else {
|
||||||
|
audioNode.pause();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={playing ? styles.playButtonPause : styles.playButtonPlay}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={userActive ? styles.userActive : styles.wrapper}
|
||||||
|
onMouseEnter={() => me.setState({ userActive: true })}
|
||||||
|
onMouseLeave={() => me.setState({ userActive: false })}
|
||||||
|
onContextMenu={stopContextMenu}
|
||||||
|
>
|
||||||
|
<div className={enableMilkdrop ? styles.containerWithMilkdrop : styles.container}>
|
||||||
|
<div style={{ position: 'absolute', top: 0, right: 0 }}>
|
||||||
|
<Tooltip onComponent body={__('Toggle Visualizer')}>
|
||||||
|
<Button
|
||||||
|
icon={enableMilkdrop ? ICONS.VISUALIZER_ON : ICONS.VISUALIZER_OFF}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isButterchurnSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new preset
|
||||||
|
this.visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
|
||||||
|
|
||||||
|
this.setState({ enableMilkdrop: !enableMilkdrop });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip onComponent body={__('Toggle Album Art')}>
|
||||||
|
<Button
|
||||||
|
icon={enableArt ? ICONS.MUSIC_ART_ON : ICONS.MUSIC_ART_OFF}
|
||||||
|
onClick={() => this.setState({ enableArt: !enableArt })}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip onComponent body={__('Toggle Details')}>
|
||||||
|
<Button
|
||||||
|
icon={showSongDetails ? ICONS.MUSIC_DETAILS_ON : ICONS.MUSIC_DETAILS_OFF}
|
||||||
|
onClick={() => this.setState({ showSongDetails: !showSongDetails })}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip onComponent body={__('Equalizer')}>
|
||||||
|
<Button icon={ICONS.MUSIC_EQUALIZER} onClick={() => this.setState({ showEqualizer: !showEqualizer })} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div ref={node => (this.waveNode = node)} className={styles.wave} />
|
||||||
|
<div className={styles.infoContainer}>
|
||||||
|
<div className={renderArt ? styles.infoArtContainer : styles.infoArtContainerHidden}>
|
||||||
|
<img className={styles.infoArtImage} ref={node => (this.artNode = node)} />
|
||||||
|
{renderArt && playButton}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
showSongDetails
|
||||||
|
? renderArt
|
||||||
|
? styles.songDetailsContainer
|
||||||
|
: styles.songDetailsContainerNoArt
|
||||||
|
: styles.songDetailsContainerHidden
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={renderArt ? styles.songDetails : styles.songDetailsNoArt}>
|
||||||
|
{artist && (
|
||||||
|
<div className={styles.detailsLineArtist}>
|
||||||
|
<Button icon={ICONS.MUSIC_ARTIST} className={styles.detailsIconArtist} />
|
||||||
|
{artist}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<div className={styles.detailsLineSong}>
|
||||||
|
<Button icon={ICONS.MUSIC_SONG} className={styles.detailsIconSong} />
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{album && (
|
||||||
|
<div className={styles.detailsLineAlbum}>
|
||||||
|
<Button icon={ICONS.MUSIC_ALBUM} className={styles.detailsIconAlbum} />
|
||||||
|
{album}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!renderArt && <div className={styles.playButtonDetachedContainer}>{playButton}</div>}
|
||||||
|
</div>
|
||||||
|
<canvas
|
||||||
|
ref={node => (this.canvasNode = node)}
|
||||||
|
className={enableMilkdrop ? styles.milkdrop : styles.milkdropDisabled}
|
||||||
|
/>
|
||||||
|
<audio
|
||||||
|
ref={node => (this.audioNode = node)}
|
||||||
|
src={path}
|
||||||
|
style={{ position: 'absolute', top: '-100px' }}
|
||||||
|
onPlay={() => this.setState({ playing: true })}
|
||||||
|
onPause={() => this.setState({ playing: false })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioVideoViewer;
|
193
src/ui/component/viewers/audioViewer.module.scss
Normal file
193
src/ui/component/viewers/audioViewer.module.scss
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
.wrapper {
|
||||||
|
composes: 'file-render__viewer' from global;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userActive {
|
||||||
|
composes: wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #212529;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerWithMilkdrop {
|
||||||
|
composes: container;
|
||||||
|
|
||||||
|
background: rgba(50, 50, 55, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20%;
|
||||||
|
height: 40%;
|
||||||
|
opacity: 0.5;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoContainer {
|
||||||
|
padding: 0 20%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 42%;
|
||||||
|
align-self: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: -10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoArtContainer {
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 40%;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoArtContainerHidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoArtImage {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.7s;
|
||||||
|
|
||||||
|
.userActive & {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.songDetailsContainer {
|
||||||
|
text-align: left;
|
||||||
|
padding: 3%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songDetailsContainerHidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songDetailsContainerNoArt {
|
||||||
|
composes: songDetailsContainer;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songDetails {
|
||||||
|
width: 150%;
|
||||||
|
text-shadow: 2px 2px 3px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songDetailsNoArt {
|
||||||
|
composes: songDetails;
|
||||||
|
|
||||||
|
width: 200%;
|
||||||
|
margin-left: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsIcon {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
top: -3px;
|
||||||
|
padding-right: 10px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsIconArtist {
|
||||||
|
composes: detailsIcon;
|
||||||
|
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsIconSong {
|
||||||
|
composes: detailsIcon;
|
||||||
|
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsIconAlbum {
|
||||||
|
composes: detailsIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsLineArtist {
|
||||||
|
font-size: 26px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsLineSong {
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsLineAlbum {
|
||||||
|
font-size: 20px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playButton {
|
||||||
|
position: absolute;
|
||||||
|
border: 5px solid #fff;
|
||||||
|
border-radius: 45px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: arial;
|
||||||
|
font-size: 60px;
|
||||||
|
left: 50%;
|
||||||
|
line-height: 80px;
|
||||||
|
margin-left: -45px;
|
||||||
|
padding-left: 20px;
|
||||||
|
bottom: 50%;
|
||||||
|
margin-bottom: -45px;
|
||||||
|
height: 90px;
|
||||||
|
width: 90px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.7s;
|
||||||
|
|
||||||
|
.userActive & {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playButtonPlay {
|
||||||
|
composes: playButton;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
content: '▶';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playButtonPause {
|
||||||
|
composes: playButton;
|
||||||
|
|
||||||
|
font-size: 50px;
|
||||||
|
line-height: 75px;
|
||||||
|
padding-left: 20px;
|
||||||
|
letter-spacing: -24px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
content: '▎▎';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playButtonDetachedContainer {
|
||||||
|
bottom: 35%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milkdrop {
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milkdropDisabled {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -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;
|
|
23
src/ui/component/viewers/videoViewer/index.js
Normal file
23
src/ui/component/viewers/videoViewer/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { connect } from 'react-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),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
changeVolume: volume => dispatch(doChangeVolume(volume)),
|
||||||
|
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
|
||||||
|
changeMute: muted => dispatch(doChangeMute(muted)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(VideoViewer);
|
81
src/ui/component/viewers/videoViewer/view.jsx
Normal file
81
src/ui/component/viewers/videoViewer/view.jsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// @flow
|
||||||
|
import React, { createRef, useEffect } 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.5, 1, 1.25, 1.5, 2],
|
||||||
|
fluid: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
source: string,
|
||||||
|
contentType: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function VideoViewer(props: Props) {
|
||||||
|
const { contentType, source } = props;
|
||||||
|
const videoRef = createRef();
|
||||||
|
|
||||||
|
// Handle any other effects separately to avoid re-mounting the video player when props change
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef && source && contentType) {
|
||||||
|
const videoNode = videoRef.current;
|
||||||
|
const videoJsOptions = {
|
||||||
|
...VIDEO_JS_OPTIONS,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: source,
|
||||||
|
type: contentType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const player = videojs(videoNode, videoJsOptions);
|
||||||
|
return () => player.dispose();
|
||||||
|
}
|
||||||
|
}, [videoRef, source, contentType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
const videoNode = videoRef && videoRef.current;
|
||||||
|
if (!videoNode) return;
|
||||||
|
|
||||||
|
// This should be done in a reusable way
|
||||||
|
// maybe a custom useKeyboardListener hook?
|
||||||
|
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const isPaused = videoNode.paused;
|
||||||
|
if (isPaused) {
|
||||||
|
videoNode.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoNode.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [videoRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
|
||||||
|
<div data-vjs-player>
|
||||||
|
<video ref={videoRef} className="video-js" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoViewer;
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doLoadVideo, doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { doHideModal } from 'redux/actions/app';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { makeSelectMetadataForUri } from 'lbry-redux';
|
import { makeSelectMetadataForUri } from 'lbry-redux';
|
||||||
import ModalAffirmPurchase from './view';
|
import ModalAffirmPurchase from './view';
|
||||||
|
@ -14,7 +14,9 @@ const perform = dispatch => ({
|
||||||
dispatch(doHideModal());
|
dispatch(doHideModal());
|
||||||
},
|
},
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
loadVideo: uri => dispatch(doLoadVideo(uri)),
|
loadVideo: uri => {
|
||||||
|
throw Error('sean you need to fix this');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -32,11 +32,11 @@ function ModalRemoveFile(props: Props) {
|
||||||
</section>
|
</section>
|
||||||
<Form onSubmit={() => deleteFile(outpoint || '', deleteChecked, abandonChecked)}>
|
<Form onSubmit={() => deleteFile(outpoint || '', deleteChecked, abandonChecked)}>
|
||||||
<FormField
|
<FormField
|
||||||
name="claim_abandon"
|
name="file_delete"
|
||||||
label={__('Abandon the claim for this URI')}
|
label={__('Also delete this file from my computer')}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={abandonChecked}
|
checked={deleteChecked}
|
||||||
onChange={() => setAbandonChecked(!abandonChecked)}
|
onChange={() => setDeleteChecked(!deleteChecked)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
doPrepareEdit,
|
doPrepareEdit,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
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 { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import FilePage from './view';
|
import FilePage from './view';
|
||||||
|
@ -29,7 +29,7 @@ const select = (state, props) => ({
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
obscureNsfw: !selectShowMatureContent(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { buildURI, normalizeURI } from 'lbry-redux';
|
import { Lbry, buildURI, normalizeURI } from 'lbry-redux';
|
||||||
import FileViewer from 'component/fileViewer';
|
import FileViewer from 'component/fileViewer';
|
||||||
import Thumbnail from 'component/common/thumbnail';
|
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import FileDetails from 'component/fileDetails';
|
import FileDetails from 'component/fileDetails';
|
||||||
import FileActions from 'component/fileActions';
|
import FileActions from 'component/fileActions';
|
||||||
|
@ -13,8 +12,6 @@ import DateTime from 'component/dateTime';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import classnames from 'classnames';
|
|
||||||
import getMediaType from 'util/get-media-type';
|
|
||||||
import RecommendedContent from 'component/recommendedContent';
|
import RecommendedContent from 'component/recommendedContent';
|
||||||
import ClaimTags from 'component/claimTags';
|
import ClaimTags from 'component/claimTags';
|
||||||
import CommentsList from 'component/commentsList';
|
import CommentsList from 'component/commentsList';
|
||||||
|
@ -28,7 +25,6 @@ type Props = {
|
||||||
contentType: string,
|
contentType: string,
|
||||||
uri: string,
|
uri: string,
|
||||||
rewardedContentClaimIds: Array<string>,
|
rewardedContentClaimIds: Array<string>,
|
||||||
obscureNsfw: boolean,
|
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
costInfo: ?{ cost: number },
|
costInfo: ?{ cost: number },
|
||||||
fetchFileInfo: string => void,
|
fetchFileInfo: string => void,
|
||||||
|
@ -44,25 +40,12 @@ type Props = {
|
||||||
fetchViewCount: string => void,
|
fetchViewCount: string => void,
|
||||||
balance: number,
|
balance: number,
|
||||||
title: string,
|
title: string,
|
||||||
thumbnail: ?string,
|
|
||||||
nsfw: boolean,
|
nsfw: boolean,
|
||||||
supportOption: boolean,
|
supportOption: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePage extends React.Component<Props> {
|
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'];
|
||||||
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) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -136,7 +119,6 @@ class FilePage extends React.Component<Props> {
|
||||||
contentType,
|
contentType,
|
||||||
uri,
|
uri,
|
||||||
rewardedContentClaimIds,
|
rewardedContentClaimIds,
|
||||||
obscureNsfw,
|
|
||||||
openModal,
|
openModal,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
prepareEdit,
|
prepareEdit,
|
||||||
|
@ -146,7 +128,6 @@ class FilePage extends React.Component<Props> {
|
||||||
viewCount,
|
viewCount,
|
||||||
balance,
|
balance,
|
||||||
title,
|
title,
|
||||||
thumbnail,
|
|
||||||
nsfw,
|
nsfw,
|
||||||
supportOption,
|
supportOption,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -154,14 +135,11 @@ class FilePage extends React.Component<Props> {
|
||||||
// File info
|
// File info
|
||||||
const { signing_channel: signingChannel } = claim;
|
const { signing_channel: signingChannel } = claim;
|
||||||
const channelName = signingChannel && signingChannel.name;
|
const channelName = signingChannel && signingChannel.name;
|
||||||
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
|
const { PREVIEW_MEDIA_TYPES } = FilePage;
|
||||||
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
|
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
|
||||||
const shouldObscureThumbnail = obscureNsfw && nsfw;
|
|
||||||
const fileName = fileInfo ? fileInfo.file_name : null;
|
const fileName = fileInfo ? fileInfo.file_name : null;
|
||||||
const mediaType = getMediaType(contentType, fileName);
|
const mediaType = Lbry.getMediaType(contentType, fileName);
|
||||||
const isPreviewType = PREVIEW_MEDIA_TYPES.includes(mediaType);
|
const isPreviewType = PREVIEW_MEDIA_TYPES.includes(mediaType);
|
||||||
const isPlayableType = PLAYABLE_MEDIA_TYPES.includes(mediaType);
|
|
||||||
const showFile = isPlayableType || isPreviewType;
|
|
||||||
|
|
||||||
const speechShareable =
|
const speechShareable =
|
||||||
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
|
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
|
||||||
|
@ -195,28 +173,7 @@ class FilePage extends React.Component<Props> {
|
||||||
{__('or send more LBC to your wallet.')}
|
{__('or send more LBC to your wallet.')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showFile && (
|
<FileViewer uri={uri} viewerContainer={this.viewerContainer} insufficientCredits={insufficientCredits} />
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
|
|
|
@ -28,6 +28,7 @@ const select = state => ({
|
||||||
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
|
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
|
||||||
userBlockedChannelsCount: selectBlockedChannelsCount(state),
|
userBlockedChannelsCount: selectBlockedChannelsCount(state),
|
||||||
hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state),
|
hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state),
|
||||||
|
maxConnections: makeSelectClientSetting(settings.MAX_CONNECTIONS)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -20,6 +20,9 @@ type DaemonSettings = {
|
||||||
download_dir: string,
|
download_dir: string,
|
||||||
share_usage_data: boolean,
|
share_usage_data: boolean,
|
||||||
max_key_fee?: Price,
|
max_key_fee?: Price,
|
||||||
|
max_connections_per_download?: number,
|
||||||
|
save_files: boolean,
|
||||||
|
save_blobs: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -47,6 +50,7 @@ type Props = {
|
||||||
supportOption: boolean,
|
supportOption: boolean,
|
||||||
userBlockedChannelsCount?: number,
|
userBlockedChannelsCount?: number,
|
||||||
hideBalance: boolean,
|
hideBalance: boolean,
|
||||||
|
maxConnections: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -62,6 +66,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
(this: any).onKeyFeeChange = this.onKeyFeeChange.bind(this);
|
(this: any).onKeyFeeChange = this.onKeyFeeChange.bind(this);
|
||||||
|
(this: any).onMaxConnectionsChange = this.onMaxConnectionsChange.bind(this);
|
||||||
(this: any).onKeyFeeDisableChange = this.onKeyFeeDisableChange.bind(this);
|
(this: any).onKeyFeeDisableChange = this.onKeyFeeDisableChange.bind(this);
|
||||||
(this: any).onInstantPurchaseMaxChange = this.onInstantPurchaseMaxChange.bind(this);
|
(this: any).onInstantPurchaseMaxChange = this.onInstantPurchaseMaxChange.bind(this);
|
||||||
(this: any).onThemeChange = this.onThemeChange.bind(this);
|
(this: any).onThemeChange = this.onThemeChange.bind(this);
|
||||||
|
@ -73,12 +78,21 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getThemes();
|
this.props.getThemes();
|
||||||
this.props.updateWalletStatus();
|
this.props.updateWalletStatus();
|
||||||
|
|
||||||
|
const { daemonSettings } = this.props;
|
||||||
|
this.props.setClientSetting(SETTINGS.MAX_CONNECTIONS, daemonSettings.max_connections_per_download);
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyFeeChange(newValue: Price) {
|
onKeyFeeChange(newValue: Price) {
|
||||||
this.setDaemonSetting('max_key_fee', newValue);
|
this.setDaemonSetting('max_key_fee', newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMaxConnectionsChange(event: SyntheticInputEvent<*>) {
|
||||||
|
const { value } = event.target;
|
||||||
|
this.setDaemonSetting('max_connections_per_download', value);
|
||||||
|
this.props.setClientSetting(SETTINGS.MAX_CONNECTIONS, value);
|
||||||
|
}
|
||||||
|
|
||||||
onKeyFeeDisableChange(isDisabled: boolean) {
|
onKeyFeeDisableChange(isDisabled: boolean) {
|
||||||
if (isDisabled) this.setDaemonSetting('max_key_fee');
|
if (isDisabled) this.setDaemonSetting('max_key_fee');
|
||||||
}
|
}
|
||||||
|
@ -156,12 +170,15 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
supportOption,
|
supportOption,
|
||||||
hideBalance,
|
hideBalance,
|
||||||
userBlockedChannelsCount,
|
userBlockedChannelsCount,
|
||||||
|
maxConnections,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
||||||
|
|
||||||
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
|
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
|
||||||
|
|
||||||
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
|
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
|
||||||
|
const connectionOptions = [1, 4, 6, 10, 20];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
@ -188,7 +205,41 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="card card--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={__(
|
||||||
|
'Enables saving of all viewed content to your downloads directory. Some file types are saved by default.'
|
||||||
|
)}
|
||||||
|
helper={__('This is not retroactive, only works from the time it was changed.')}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
<Form>
|
||||||
|
<FormField
|
||||||
|
type="checkbox"
|
||||||
|
name="save_blobs"
|
||||||
|
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
|
||||||
|
checked={daemonSettings.save_blobs}
|
||||||
|
label={
|
||||||
|
<React.Fragment>
|
||||||
|
{__('Enables saving of hosting data to help the LBRY network.')}{' '}
|
||||||
|
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />.
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
helper={__("If disabled, LBRY will be very sad and you won't be helping improve the network")}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card card--section">
|
||||||
|
<header className="card__header">
|
||||||
|
<h2 className="card__title">{__('Max Purchase Price')}</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -429,24 +480,42 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
<FormField
|
||||||
|
name="language_select"
|
||||||
|
type="select"
|
||||||
|
label={__('Language')}
|
||||||
|
onChange={this.onLanguageChange}
|
||||||
|
value={currentLanguage}
|
||||||
|
helper={__(
|
||||||
|
'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
|
<FormField
|
||||||
name="language_select"
|
name="max_connections"
|
||||||
type="select"
|
type="select"
|
||||||
label={__('Language')}
|
label={__('Max Connections')}
|
||||||
onChange={this.onLanguageChange}
|
helper={__('More connections, like, do stuff dude')}
|
||||||
value={currentLanguage}
|
min={1}
|
||||||
helper={__(
|
max={100}
|
||||||
'Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.'
|
onChange={this.onMaxConnectionsChange}
|
||||||
)}
|
value={maxConnections}
|
||||||
>
|
>
|
||||||
{Object.keys(languages).map(language => (
|
{connectionOptions.map(connectionOption => (
|
||||||
<option key={language} value={language}>
|
<option key={connectionOption} value={connectionOption}>
|
||||||
{languages[language]}
|
{connectionOption}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</FormField>
|
</FormField>
|
||||||
}
|
</fieldset-section>
|
||||||
</Form>
|
</Form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
commentReducer,
|
commentReducer,
|
||||||
blockedReducer,
|
blockedReducer,
|
||||||
publishReducer,
|
publishReducer,
|
||||||
|
fileReducer,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
userReducer,
|
userReducer,
|
||||||
|
@ -38,6 +39,7 @@ export default history =>
|
||||||
content: contentReducer,
|
content: contentReducer,
|
||||||
costInfo: costInfoReducer,
|
costInfo: costInfoReducer,
|
||||||
fileInfo: fileInfoReducer,
|
fileInfo: fileInfoReducer,
|
||||||
|
file: fileReducer, // Why is this not in `fileInfoReducer`?
|
||||||
homepage: homepageReducer,
|
homepage: homepageReducer,
|
||||||
notifications: notificationsReducer,
|
notifications: notificationsReducer,
|
||||||
publish: publishReducer,
|
publish: publishReducer,
|
||||||
|
|
|
@ -17,15 +17,13 @@ import {
|
||||||
buildURI,
|
buildURI,
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
selectFileInfosByOutpoint,
|
selectFileInfosByOutpoint,
|
||||||
selectDownloadingByOutpoint,
|
|
||||||
selectBalance,
|
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
parseURI,
|
parseURI,
|
||||||
doError,
|
doPurchaseUri,
|
||||||
|
makeSelectUriIsStreamable,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||||
import analytics from 'analytics';
|
|
||||||
import { formatLbryUriForWeb } from 'util/uri';
|
import { formatLbryUriForWeb } from 'util/uri';
|
||||||
|
|
||||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||||
|
@ -88,7 +86,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
||||||
dispatch(doUpdateUnreadSubscriptions(channelUri, null, NOTIFICATION_TYPES.DOWNLOADED));
|
dispatch(doUpdateUnreadSubscriptions(channelUri, null, NOTIFICATION_TYPES.DOWNLOADED));
|
||||||
} else {
|
} else {
|
||||||
// If notifications are disabled(false) just return
|
// 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,
|
body: fileInfo.metadata.title,
|
||||||
|
@ -123,39 +121,6 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
||||||
// @endif
|
// @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) {
|
export function doSetPlayingUri(uri: ?string) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -165,126 +130,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) {
|
export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize: number = PAGE_SIZE) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -334,12 +179,64 @@ export function doFetchClaimsByChannel(uri: string, page: number = 1, pageSize:
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doPlayUri(uri: string) {
|
export function doPurchaseUriWrapper(uri: string, costInfo: {}, saveFile: boolean) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
|
const state = getState();
|
||||||
|
const isUriStreamable = makeSelectUriIsStreamable(uri)(state);
|
||||||
|
|
||||||
|
function onSuccess(fileInfo) {
|
||||||
|
dispatch(doUpdateLoadStatus(uri, fileInfo.outpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only pass the sucess callback for non streamable files because we don't show the download percentage while streaming
|
||||||
|
const successCallBack = isUriStreamable ? undefined : onSuccess;
|
||||||
|
dispatch(doPurchaseUri(uri, costInfo, saveFile, successCallBack));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doPlayUri(uri: string, saveFile: boolean) {
|
||||||
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
|
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(doPurchaseUriWrapper(uri, { costInfo: cost }, saveFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the active playing uri so we can avoid showing error notifications if a previously started download fails
|
||||||
dispatch(doSetPlayingUri(uri));
|
dispatch(doSetPlayingUri(uri));
|
||||||
// @if TARGET='app'
|
|
||||||
dispatch(doPurchaseUri(uri));
|
const state = getState();
|
||||||
// @endif
|
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||||
|
const { cost } = makeSelectCostInfoForUri(uri)(state);
|
||||||
|
|
||||||
|
// we already fully downloaded the file.
|
||||||
|
if (fileInfo && fileInfo.completed && (fileInfo.status === 'stopped' || fileInfo.status === 'finished')) {
|
||||||
|
// 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
|
||||||
|
// doPurchaseUri action to reconstruct the file from the blobs
|
||||||
|
if (!fileInfo.download_path || !fileInfo.written_bytes) {
|
||||||
|
dispatch(doPurchaseUriWrapper(uri, { costInfo: cost }, saveFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,7 @@ import * as NOTIFICATION_TYPES from 'constants/subscriptions';
|
||||||
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
|
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
|
||||||
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { Lbry, buildURI, parseURI, doResolveUris } from 'lbry-redux';
|
import { Lbry, buildURI, parseURI, doResolveUris, doPurchaseUri } from 'lbry-redux';
|
||||||
import { doPurchaseUri } from 'redux/actions/content';
|
|
||||||
|
|
||||||
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
|
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
|
||||||
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
||||||
|
@ -194,15 +193,12 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
getState: GetState
|
getState: GetState
|
||||||
) => {
|
) => {
|
||||||
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
|
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
|
const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
|
||||||
const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri);
|
const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri);
|
||||||
|
|
||||||
if (!savedSubscription) {
|
if (!savedSubscription) {
|
||||||
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
|
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
|
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
|
||||||
Lbry.claim_search({
|
Lbry.claim_search({
|
||||||
channel: subscriptionUri,
|
channel: subscriptionUri,
|
||||||
|
@ -212,42 +208,34 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
}).then(claimListByChannel => {
|
}).then(claimListByChannel => {
|
||||||
const { items: claimsInChannel } = claimListByChannel;
|
const { items: claimsInChannel } = claimListByChannel;
|
||||||
|
|
||||||
// may happen if subscribed to an abandoned channel or an empty channel
|
// may happen if subscribed to an abandoned channel or an empty channel
|
||||||
if (!claimsInChannel || !claimsInChannel.length) {
|
if (!claimsInChannel || !claimsInChannel.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if the latest subscription currently saved is actually the latest subscription
|
// Determine if the latest subscription currently saved is actually the latest subscription
|
||||||
const latestIndex = claimsInChannel.findIndex(
|
const latestIndex = claimsInChannel.findIndex(
|
||||||
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
|
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
|
||||||
);
|
);
|
||||||
|
|
||||||
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
|
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
|
||||||
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
|
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
|
||||||
|
|
||||||
// If latest is 0, nothing has changed
|
// If latest is 0, nothing has changed
|
||||||
// Do not download/notify about new content, it would download/notify 10 claims per channel
|
// Do not download/notify about new content, it would download/notify 10 claims per channel
|
||||||
if (latestIndex !== 0 && savedSubscription.latest) {
|
if (latestIndex !== 0 && savedSubscription.latest) {
|
||||||
let downloadCount = 0;
|
let downloadCount = 0;
|
||||||
|
|
||||||
const newUnread = [];
|
const newUnread = [];
|
||||||
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
|
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
|
||||||
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
|
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
|
||||||
const shouldDownload =
|
const shouldDownload =
|
||||||
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
|
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
|
||||||
|
|
||||||
// Add the new content to the list of "un-read" subscriptions
|
// Add the new content to the list of "un-read" subscriptions
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
newUnread.push(uri);
|
newUnread.push(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDownload) {
|
if (shouldDownload) {
|
||||||
downloadCount += 1;
|
downloadCount += 1;
|
||||||
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
|
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
doUpdateUnreadSubscriptions(
|
doUpdateUnreadSubscriptions(
|
||||||
subscriptionUri,
|
subscriptionUri,
|
||||||
|
@ -256,7 +244,6 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the latest piece of content for a channel
|
// Set the latest piece of content for a channel
|
||||||
// This allows the app to know if there has been new content since it was last set
|
// This allows the app to know if there has been new content since it was last set
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -274,7 +261,6 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
|
||||||
buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false)
|
buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
|
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
|
||||||
// means it will delete a non-existant fetchingChannelClaims[uri]
|
// means it will delete a non-existant fetchingChannelClaims[uri]
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
|
@ -208,7 +208,7 @@ reducers[ACTIONS.WINDOW_FOCUSED] = state =>
|
||||||
|
|
||||||
reducers[ACTIONS.VOLUME_CHANGED] = (state, action) =>
|
reducers[ACTIONS.VOLUME_CHANGED] = (state, action) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
volume: action.data.volume,
|
muted: action.data.volume,
|
||||||
});
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.VOLUME_MUTED] = (state, action) =>
|
reducers[ACTIONS.VOLUME_MUTED] = (state, action) =>
|
||||||
|
|
|
@ -100,7 +100,7 @@ export const selectVolume = createSelector(
|
||||||
state => state.volume
|
state => state.volume
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selecetMute = createSelector(
|
export const selectMute = createSelector(
|
||||||
selectState,
|
selectState,
|
||||||
state => state.muted
|
state => state.muted
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { createSelector } from 'reselect';
|
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 RECENT_HISTORY_AMOUNT = 10;
|
||||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
const HISTORY_ITEMS_PER_PAGE = 50;
|
||||||
|
@ -12,6 +18,12 @@ export const selectPlayingUri = createSelector(
|
||||||
state => state.playingUri
|
state => state.playingUri
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeSelectIsPlaying = (uri: string) =>
|
||||||
|
createSelector(
|
||||||
|
selectPlayingUri,
|
||||||
|
playingUri => playingUri === uri
|
||||||
|
);
|
||||||
|
|
||||||
export const selectRewardContentClaimIds = createSelector(
|
export const selectRewardContentClaimIds = createSelector(
|
||||||
selectState,
|
selectState,
|
||||||
state => state.rewardedContentClaimIds
|
state => state.rewardedContentClaimIds
|
||||||
|
@ -85,3 +97,12 @@ export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeSelectShouldObscurePreview = (uri: string) =>
|
||||||
|
createSelector(
|
||||||
|
selectShowMatureContent,
|
||||||
|
makeSelectClaimIsNsfw(uri),
|
||||||
|
(showMatureContent, isClaimMature) => {
|
||||||
|
return isClaimMature && !showMatureContent;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const makeSelectClientSetting = setting =>
|
||||||
);
|
);
|
||||||
|
|
||||||
// refactor me
|
// refactor me
|
||||||
export const selectShowNsfw = makeSelectClientSetting(SETTINGS.SHOW_NSFW);
|
export const selectShowMatureContent = makeSelectClientSetting(SETTINGS.SHOW_NSFW);
|
||||||
|
|
||||||
export const selectLanguages = createSelector(
|
export const selectLanguages = createSelector(
|
||||||
selectState,
|
selectState,
|
||||||
|
|
|
@ -71,11 +71,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__loading {
|
.content__loading {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
align-items: center;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 var(--spacing-large);
|
padding: 0 var(--spacing-large);
|
||||||
|
@ -85,32 +87,3 @@
|
||||||
.content__loading-text {
|
.content__loading-text {
|
||||||
color: $lbry-white;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -69,6 +69,11 @@ ul {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
padding: 0.8rem;
|
padding: 0.8rem;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// A simple function to detect if a user is typing:
|
// A simple function to detect if a user is typing:
|
||||||
// useful when hanlding shorcut keys.
|
// useful when hanlding shorcut keys.
|
||||||
|
|
||||||
export default function detectTyping() {
|
export default function isUserTyping() {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
|
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
|
|
|
@ -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';
|
|
||||||
}
|
|
108
yarn.lock
108
yarn.lock
|
@ -1850,11 +1850,6 @@ binary-extensions@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
||||||
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
|
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:
|
binary@~0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
|
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"
|
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||||
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
||||||
|
|
||||||
buffer-alloc@^1.1.0, buffer-alloc@^1.2.0:
|
buffer-alloc@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
||||||
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
|
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"
|
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||||
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
|
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
|
||||||
|
|
||||||
buffer-from@^1.0.0, buffer-from@^1.1.0:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
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"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
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:
|
is-binary-path@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
|
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"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#8f12baa88f6f057eb3b7d0cf04d6e4bb0eb11763:
|
lbry-redux@lbryio/lbry-redux#1b7bb1cc9f2cb6a8efcce1869031d4da8ddbf4ca:
|
||||||
version "0.0.1"
|
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/1b7bb1cc9f2cb6a8efcce1869031d4da8ddbf4ca"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
mime "^2.4.4"
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^3.3.2"
|
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"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
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:
|
mem@^4.0.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
|
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"
|
rimraf "^2.5.4"
|
||||||
run-queue "^1.0.3"
|
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:
|
mpd-parser@0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.7.0.tgz#d36e3322579fce23d657f71a3c2f3e6cc5ce4002"
|
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"
|
dns-packet "^1.3.1"
|
||||||
thunky "^1.0.2"
|
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:
|
mute-stream@0.0.7:
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
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"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
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:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
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"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
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:
|
raw-body@2.4.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||||
|
@ -9825,7 +9767,7 @@ read-pkg@^2.0.0:
|
||||||
normalize-package-data "^2.3.2"
|
normalize-package-data "^2.3.2"
|
||||||
path-type "^2.0.0"
|
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"
|
version "2.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||||
|
@ -9838,7 +9780,7 @@ read-pkg@^2.0.0:
|
||||||
string_decoder "~1.1.1"
|
string_decoder "~1.1.1"
|
||||||
util-deprecate "~1.0.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"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9"
|
||||||
integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==
|
integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==
|
||||||
|
@ -10107,17 +10049,6 @@ remove-trailing-separator@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||||
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
|
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:
|
renderkid@^2.0.1:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
|
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
|
||||||
|
@ -10988,7 +10919,7 @@ stream-shift@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
|
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
|
||||||
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
|
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"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
|
resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
|
||||||
integrity sha512-DKJPEmCmIZoBfGVle9IhSfERiWaN5cuOtmfPxP2dZbLDRZxkBWZ4QbYxEJOSALk1Kf+WjBgedAMO6qkkf7Lmrg==
|
integrity sha512-DKJPEmCmIZoBfGVle9IhSfERiWaN5cuOtmfPxP2dZbLDRZxkBWZ4QbYxEJOSALk1Kf+WjBgedAMO6qkkf7Lmrg==
|
||||||
|
@ -11455,7 +11386,7 @@ tmp@^0.0.33:
|
||||||
dependencies:
|
dependencies:
|
||||||
os-tmpdir "~1.0.2"
|
os-tmpdir "~1.0.2"
|
||||||
|
|
||||||
to-arraybuffer@^1.0.0, to-arraybuffer@^1.0.1:
|
to-arraybuffer@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
|
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
|
||||||
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
|
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
|
||||||
|
@ -11672,13 +11603,6 @@ uglify-js@3.4.x:
|
||||||
commander "~2.19.0"
|
commander "~2.19.0"
|
||||||
source-map "~0.6.1"
|
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:
|
unbzip2-stream@^1.0.9:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
|
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
|
||||||
|
@ -12120,20 +12044,6 @@ videojs-vtt.js@0.14.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
global "^4.3.1"
|
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:
|
villain@btzr-io/Villain:
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
resolved "https://codeload.github.com/btzr-io/Villain/tar.gz/1f39a679cd78b08f8acc0b36615550eb91f6ee03"
|
resolved "https://codeload.github.com/btzr-io/Villain/tar.gz/1f39a679cd78b08f8acc0b36615550eb91f6ee03"
|
||||||
|
|
Loading…
Reference in a new issue