Merge pull request #2406 from lbryio/audio-player

New Audio Player & Code Splitting
This commit is contained in:
Shawn K 2019-04-09 09:24:20 -04:00 committed by GitHub
commit 627acbd885
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 2113 additions and 859 deletions

View file

@ -1,6 +1,8 @@
{ {
"presets": ["@babel/react", "@babel/flow"], "presets": ["@babel/react", "@babel/flow"],
"plugins": [ "plugins": [
"import-glob",
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-dynamic-import",
"react-hot-loader/babel", "react-hot-loader/babel",
["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }], ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }],

View file

@ -25,7 +25,7 @@
"compile": "cross-env NODE_ENV=production yarn compile:electron && cross-env NODE_ENV=production yarn compile:web", "compile": "cross-env NODE_ENV=production yarn compile:electron && cross-env NODE_ENV=production yarn compile:web",
"dev": "yarn dev:electron", "dev": "yarn dev:electron",
"dev:electron": "cross-env NODE_ENV=development node ./src/platforms/electron/devServer.js", "dev:electron": "cross-env NODE_ENV=development node ./src/platforms/electron/devServer.js",
"dev:web": "NODE_ENV=development webpack-dev-server --open --hot --progress --config webpack.web.config.js", "dev:web": "cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --config webpack.web.config.js",
"dev:internal-apis": "LBRY_API_URL='http://localhost:9090' yarn dev:electron", "dev:internal-apis": "LBRY_API_URL='http://localhost:9090' yarn dev:electron",
"run:web": "cross-env NODE_ENV=production yarn compile:web && node ./dist/web/server.js", "run:web": "cross-env NODE_ENV=production yarn compile:web && node ./dist/web/server.js",
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
@ -52,6 +52,7 @@
"@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-flow-strip-types": "^7.2.3", "@babel/plugin-transform-flow-strip-types": "^7.2.3",
"@babel/plugin-transform-runtime": "^7.4.3",
"@babel/polyfill": "^7.2.5", "@babel/polyfill": "^7.2.5",
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
@ -64,8 +65,11 @@
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"babel-plugin-add-module-exports": "^1.0.0", "babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-import-glob": "^2.0.0",
"babel-plugin-transform-imports": "^1.5.1", "babel-plugin-transform-imports": "^1.5.1",
"bluebird": "^3.5.1", "bluebird": "^3.5.1",
"butterchurn": "^2.6.7",
"butterchurn-presets": "^2.4.7",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.39.2", "codemirror": "^5.39.2",
@ -74,6 +78,7 @@
"country-data": "^0.0.31", "country-data": "^0.0.31",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"cssnano": "^4.1.10",
"dat.gui": "^0.7.2", "dat.gui": "^0.7.2",
"decompress": "^4.2.0", "decompress": "^4.2.0",
"del": "^3.0.0", "del": "^3.0.0",
@ -106,12 +111,14 @@
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"history": "^4.9.0", "history": "^4.9.0",
"husky": "^0.14.3", "husky": "^0.14.3",
"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#86f1340f834d0f5cd5365492a8ff15d4b213a050", "lbry-redux": "lbryio/lbry-redux#d4c7dea65f7179974e9b96c863022fe7b049ff7d",
"lbryinc": "lbryio/lbryinc#d9f9035113c8b9ab3b0ee7ffbd38f910086a665e", "lbryinc": "lbryio/lbryinc#4f2d4a50986bffab0b05d9f6cd7c2f0a856a0e02",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"lodash-es": "^4.17.11",
"make-runnable": "^1.3.6", "make-runnable": "^1.3.6",
"mammoth": "^1.4.6", "mammoth": "^1.4.6",
"mime": "^2.3.1", "mime": "^2.3.1",
@ -121,6 +128,8 @@
"node-libs-browser": "^2.1.0", "node-libs-browser": "^2.1.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"preprocess-loader": "^0.3.0", "preprocess-loader": "^0.3.0",
"prettier": "^1.11.1", "prettier": "^1.11.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -156,9 +165,10 @@
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.2.3", "terser-webpack-plugin": "^1.2.3",
"three": "^0.93.0", "three": "^0.93.0",
"three-full": "^11.3.2", "three-full": "^17.1.0",
"tree-kill": "^1.1.0", "tree-kill": "^1.1.0",
"video.js": "^7.2.2", "video.js": "^7.2.2",
"wavesurfer.js": "^2.2.1",
"webpack": "^4.28.4", "webpack": "^4.28.4",
"webpack-bundle-analyzer": "^3.1.0", "webpack-bundle-analyzer": "^3.1.0",
"webpack-config-utils": "^2.3.1", "webpack-config-utils": "^2.3.1",

16
postcss.config.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = ({ file, options, env }) => {
env = env || {};
file = file || {};
options = options || {};
options.cssnext = options.cssnext || null;
options.autoprefixer = options.autoprefixer || null;
options.cssnano = options.cssnano || null;
return {
parser: file.extname === '.sss' ? 'sugarss' : false,
plugins: {
'postcss-import': { root: file.dirname },
'cssnano': env === 'production' ? options.cssnano : false
}
};
};

View file

@ -40,12 +40,8 @@ app.setName('LBRY');
app.setAppUserModelId('io.lbry.LBRY'); app.setAppUserModelId('io.lbry.LBRY');
if (isDev) { if (isDev) {
// Enable WEBGL // Disable security warnings in dev mode:
app.commandLine.appendSwitch('ignore-gpu-blacklist'); // https://github.com/electron/electron/blob/master/docs/tutorial/security.md#electron-security-warnings
app.commandLine.appendSwitch('--disable-gpu-process-crash-limit');
app.disableDomainBlockingFor3DAPIs();
// Disable security warnings in dev mode - https://github.com/electron/electron/blob/master/docs/tutorial/security.md#electron-security-warnings
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
} }

View file

@ -1,12 +1,16 @@
// @flow // @flow
import * as React from 'react'; import React, { Suspense } from 'react';
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from 'react-dom/server';
import MarkdownPreview from 'component/common/markdown-preview'; import MarkdownPreview from 'component/common/markdown-preview';
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import { openEditorMenu, stopContextMenu } from 'util/context-menu'; import { openEditorMenu, stopContextMenu } from 'util/context-menu';
const SimpleMDE = React.lazy(() => import(
/* webpackChunkName: "SimpleMDE" */
'react-simplemde-editor'
));
type Props = { type Props = {
name: string, name: string,
label?: string, label?: string,
@ -132,6 +136,7 @@ export class FormField extends React.PureComponent<Props> {
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}> <div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section> <fieldset-section>
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
<Suspense fallback={<div></div>}>
<SimpleMDE <SimpleMDE
{...inputProps} {...inputProps}
id={name} id={name}
@ -145,6 +150,7 @@ export class FormField extends React.PureComponent<Props> {
}, },
}} }}
/> />
</Suspense>
</fieldset-section> </fieldset-section>
</div> </div>
); );

View file

@ -0,0 +1,55 @@
// @flow
import * as React from 'react';
import remark from 'remark';
import reactRenderer from 'remark-react';
import remarkEmoji from 'remark-emoji';
import ExternalLink from 'component/externalLink';
import defaultSchema from 'hast-util-sanitize/lib/github.json';
type MarkdownProps = {
content: ?string,
promptLinks?: boolean,
};
type SimpleLinkProps = {
href?: string,
title?: string,
children?: React.Node,
};
const SimpleLink = (props: SimpleLinkProps) => {
const { href, title, children } = props;
return (
<a href={href} title={title}>
{children}
</a>
);
};
// Use github sanitation schema
const schema = { ...defaultSchema };
// Extend sanitation schema to support lbry protocol
schema.protocols.href[3] = 'lbry';
const MarkdownPreview = (props: MarkdownProps) => {
const { content, promptLinks } = props;
const remarkOptions = {
sanitize: schema,
remarkReactComponents: {
a: promptLinks ? ExternalLink : SimpleLink,
},
};
return (
<div className="markdown-preview">
{
remark()
.use(remarkEmoji)
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</div>
);
};
export default MarkdownPreview;

View file

@ -1,54 +1,16 @@
// @flow import React, { Suspense } from 'react';
import * as React from 'react';
import remark from 'remark';
import reactRenderer from 'remark-react';
import remarkEmoji from 'remark-emoji';
import ExternalLink from 'component/externalLink';
import defaultSchema from 'hast-util-sanitize/lib/github.json';
type MarkdownProps = { const MarkdownPreviewInternal = React.lazy(() => import(
content: ?string, /* webpackChunkName: "markdownPreview" */
promptLinks?: boolean, /* webpackPrefetch: true */
}; './markdown-preview-internal'
));
type SimpleLinkProps = { const MarkdownPreview = (props) => {
href?: string,
title?: string,
children?: React.Node,
};
const SimpleLink = (props: SimpleLinkProps) => {
const { href, title, children } = props;
return ( return (
<a href={href} title={title}> <Suspense fallback={<div className="markdown-preview"></div>}>
{children} <MarkdownPreviewInternal {...props} />
</a> </Suspense>
);
};
// Use github sanitation schema
const schema = { ...defaultSchema };
// Extend sanitation schema to support lbry protocol
schema.protocols.href[3] = 'lbry';
const MarkdownPreview = (props: MarkdownProps) => {
const { content, promptLinks } = props;
const remarkOptions = {
sanitize: schema,
remarkReactComponents: {
a: promptLinks ? ExternalLink : SimpleLink,
},
};
return (
<div className="markdown-preview">
{
remark()
.use(remarkEmoji)
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</div>
); );
}; };

View file

@ -1,8 +1,12 @@
// @flow // @flow
import React from 'react'; import React, { Suspense } from 'react';
import QRCodeElement from 'qrcode.react';
import classnames from 'classnames'; import classnames from 'classnames';
const LazyQRCodeElement = React.lazy(() => import(
/* webpackChunkName: "qrCode" */
'qrcode.react'
));
type Props = { type Props = {
value: string, value: string,
paddingRight?: boolean, paddingRight?: boolean,
@ -24,7 +28,9 @@ class QRCode extends React.Component<Props> {
'qr-code--top-padding': paddingTop, 'qr-code--top-padding': paddingTop,
})} })}
> >
<QRCodeElement value={value} /> <Suspense fallback={<div></div>}>
<LazyQRCodeElement value={value} />
</Suspense>
</div> </div>
); );
} }

View file

@ -1,13 +1,35 @@
// @flow // @flow
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import { remote } from 'electron'; import { remote } from 'electron';
import React from 'react'; import React, { Suspense } from 'react';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import PdfViewer from 'component/viewers/pdfViewer'; import VideoViewer from 'component/viewers/videoViewer';
import DocumentViewer from 'component/viewers/documentViewer';
import DocxViewer from 'component/viewers/docxViewer'; const AudioViewer = React.lazy(() => import(
import HtmlViewer from 'component/viewers/htmlViewer'; /* webpackChunkName: "audioViewer" */
import AudioVideoViewer from 'component/viewers/audioVideoViewer'; 'component/viewers/audioViewer'
));
const DocumentViewer = React.lazy(() => import(
/* webpackChunkName: "documentViewer" */
'component/viewers/documentViewer'
));
const DocxViewer = React.lazy(() => import(
/* webpackChunkName: "docxViewer" */
'component/viewers/docxViewer'
));
const HtmlViewer = React.lazy(() => import(
/* webpackChunkName: "htmlViewer" */
'component/viewers/htmlViewer'
));
const PdfViewer = React.lazy(() => import(
/* webpackChunkName: "pdfViewer" */
'component/viewers/pdfViewer'
));
// @if TARGET='app' // @if TARGET='app'
const ThreeViewer = React.lazy(() => import( const ThreeViewer = React.lazy(() => import(
/* webpackChunkName: "threeViewer" */ /* webpackChunkName: "threeViewer" */
@ -97,6 +119,8 @@ class FileRender extends React.PureComponent<Props> {
renderViewer() { renderViewer() {
const { source, mediaType, currentTheme, poster, claim } = this.props; const { source, mediaType, currentTheme, poster, claim } = this.props;
console.log('mediaType', mediaType);
// Extract relevant data to render file // Extract relevant data to render file
const { stream, fileType, contentType, downloadPath, fileName } = source; const { stream, fileType, contentType, downloadPath, fileName } = source;
@ -123,7 +147,7 @@ class FileRender extends React.PureComponent<Props> {
/> />
), ),
video: ( video: (
<AudioVideoViewer <VideoViewer
claim={claim} claim={claim}
source={{ downloadPath, fileName }} source={{ downloadPath, fileName }}
contentType={contentType} contentType={contentType}
@ -131,7 +155,7 @@ class FileRender extends React.PureComponent<Props> {
/> />
), ),
audio: ( audio: (
<AudioVideoViewer <AudioViewer
claim={claim} claim={claim}
source={{ downloadPath, fileName }} source={{ downloadPath, fileName }}
contentType={contentType} contentType={contentType}
@ -176,6 +200,7 @@ class FileRender extends React.PureComponent<Props> {
} }
render() { render() {
console.log('RENDER')
return ( return (
<div className="file-render"> <div className="file-render">
<React.Suspense fallback={<div></div>}> <React.Suspense fallback={<div></div>}>

View file

@ -47,11 +47,8 @@ class MediaPlayer extends React.PureComponent<Props, State> {
'comic-book', 'comic-book',
'document', 'document',
'3D-file', '3D-file',
// The web can use the new video player, which has it's own file renderer
// @if TARGET='web'
'video', 'video',
'audio', 'audio',
// @endif
]; ];
static SANDBOX_SET_BASE_URL = 'http://localhost:5278/set/'; static SANDBOX_SET_BASE_URL = 'http://localhost:5278/set/';
static SANDBOX_CONTENT_BASE_URL = 'http://localhost:5278'; static SANDBOX_CONTENT_BASE_URL = 'http://localhost:5278';
@ -266,6 +263,11 @@ class MediaPlayer extends React.PureComponent<Props, State> {
// This files are supported using a custom viewer // This files are supported using a custom viewer
const { mediaType, contentType } = this.props; const { mediaType, contentType } = this.props;
console.log({
mediaType,
contentType
})
return ( return (
MediaPlayer.FILE_MEDIA_TYPES.indexOf(mediaType) > -1 || MediaPlayer.FILE_MEDIA_TYPES.indexOf(mediaType) > -1 ||
MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1 MediaPlayer.SANDBOX_TYPES.indexOf(contentType) > -1
@ -360,6 +362,13 @@ class MediaPlayer extends React.PureComponent<Props, State> {
const isPlayableType = this.playableType(); const isPlayableType = this.playableType();
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType); const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);
console.log({
mediaType,
fileSource,
isFileReady,
isFileType
})
return ( return (
<React.Fragment> <React.Fragment>
{loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />} {loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />}

View file

@ -1,13 +1,17 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React from 'react'; import React, { Suspense } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import analytics from 'analytics'; import analytics from 'analytics';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import Player from './internal/player';
import PlayButton from './internal/play-button'; import PlayButton from './internal/play-button';
const Player = React.lazy(() => import(
/* webpackChunkName: "player-legacy" */
'./internal/player'
));
const SPACE_BAR_KEYCODE = 32; const SPACE_BAR_KEYCODE = 32;
type Props = { type Props = {
@ -269,6 +273,7 @@ class FileViewer extends React.PureComponent<Props> {
<LoadingScreen status={loadStatusMessage} /> <LoadingScreen status={loadStatusMessage} />
</div> </div>
) : ( ) : (
<Suspense fallback={<div></div>}>
<Player <Player
fileName={fileInfo.file_name} fileName={fileInfo.file_name}
poster={poster} poster={poster}
@ -288,6 +293,7 @@ class FileViewer extends React.PureComponent<Props> {
onFinishCb={this.onFileFinishCb} onFinishCb={this.onFileFinishCb}
playingUri={playingUri} playingUri={playingUri}
/> />
</Suspense>
)} )}
</div> </div>
)} )}

View file

@ -0,0 +1,277 @@
// @flow
import type { Claim } from 'types/claim';
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: {
downloadPath: string,
fileName: string,
},
contentType: string,
poster?: string,
claim: Claim,
};
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<Props> {
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 } = me.props;
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
const sources = [
{
src: path,
type: contentType,
},
];
const audioNode = this.audioNode;
audioNode.crossOrigin = 'anonymous';
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 } = me.props;
const {
album,
artist,
title,
enableMilkdrop,
showEqualizer,
showSongDetails,
enableArt,
artLoaded,
playing,
userActive,
} = this.state;
const renderArt = enableArt && artLoaded;
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
const playButton = (
<div onClick={()=>{
const audioNode = this.audioNode;
if (audioNode.paused) {
audioNode.play();
} else {
audioNode.pause();
}
}} className={playing ? styles.playButtonPause : styles.playButtonPlay}></div>
);
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>
<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;

View 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, .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, .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 .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;
}

View file

@ -36,7 +36,7 @@ class CodeViewer extends React.PureComponent<Props> {
const { theme, contentType } = me.props; const { theme, contentType } = me.props;
// Init CodeMirror // Init CodeMirror
import( import(
/* webpackChunkName: "codeViewer" */ /* webpackChunkName: "codemirror" */
'codemirror/lib/codemirror' 'codemirror/lib/codemirror'
).then((CodeMirror) => { ).then((CodeMirror) => {
me.codeMirror = CodeMirror.fromTextArea(me.textarea, { me.codeMirror = CodeMirror.fromTextArea(me.textarea, {

View file

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React, { Suspense } from 'react';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import MarkdownPreview from 'component/common/markdown-preview'; import MarkdownPreview from 'component/common/markdown-preview';
@ -84,7 +84,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
<div className="file-render__viewer document-viewer"> <div className="file-render__viewer document-viewer">
{loading && !error && <LoadingScreen status={loadingMessage} spinner />} {loading && !error && <LoadingScreen status={loadingMessage} spinner />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />} {error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && this.renderDocument()} {isReady && <Suspense fallback={<div></div>}>{this.renderDocument()}</Suspense>}
</div> </div>
); );
} }

View file

@ -1,10 +1,12 @@
// @flow // @flow
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import React from 'react'; import React, { Suspense } from 'react';
import { stopContextMenu } from 'util/context-menu'; import { stopContextMenu } from 'util/context-menu';
import(/* webpackChunkName: "videojs" */ import(
/* webpackChunkName: "videojs" */
/* webpackPreload: true */ /* webpackPreload: true */
'video.js/dist/video-js.css'); 'video.js/dist/video-js.css'
);
type Props = { type Props = {
source: { source: {
@ -39,11 +41,16 @@ class AudioVideoViewer extends React.PureComponent<Props> {
sources, sources,
}; };
import(/* webpackChunkName: "videojs" */ import(
/* webpackChunkName: "videojs" */
/* webpackMode: "lazy" */ /* webpackMode: "lazy" */
/* webpackPreload: true */ /* webpackPreload: true */
'video.js').then(videojs => { 'video.js'
this.player = videojs.default(this.videoNode, videoJsOptions, () => {}); ).then(videojs => {
if (videojs.__esModule) {
videojs = videojs.default;
}
this.player = videojs(this.videoNode, videoJsOptions, () => {});
}); });
} }

View file

@ -53,3 +53,13 @@ export const TRANSACTIONS = 'FileText';
export const LBRY = 'Lbry'; export const LBRY = 'Lbry';
export const SEND = 'Send'; export const SEND = 'Send';
export const DISCOVER = 'Compass'; export const DISCOVER = 'Compass';
export const VISUALIZER_ON = 'Eye';
export const VISUALIZER_OFF = 'EyeOff';
export const MUSIC_DETAILS_ON = 'AlignLeft';
export const MUSIC_DETAILS_OFF = 'AlignLeft';
export const MUSIC_ART_ON = 'Image';
export const MUSIC_ART_OFF = 'Image';
export const MUSIC_ALBUM = 'Disc';
export const MUSIC_ARTIST = 'Mic';
export const MUSIC_SONG = 'Music';
export const MUSIC_EQUALIZER = 'Sliders';

View file

@ -21,7 +21,11 @@ import {
import { Lbry, doToast, isURIValid, setSearchApi } from 'lbry-redux'; import { Lbry, doToast, isURIValid, setSearchApi } from 'lbry-redux';
import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings'; import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
import { doAuthenticate, Lbryio, rewards, doBlackListedOutpointsSubscribe } from 'lbryinc'; import { doAuthenticate, Lbryio, rewards, doBlackListedOutpointsSubscribe } from 'lbryinc';
import 'scss/all.scss'; import(
/* webpackChunkName: "styles" */
/* webpackPrefetch: true */
'scss/all.scss'
);
import { store, history } from 'store'; import { store, history } from 'store';
import pjson from 'package.json'; import pjson from 'package.json';
import app from './app'; import app from './app';

View file

@ -1,11 +1,15 @@
// @flow // @flow
import React from 'react'; import React, { Suspense } from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import Button from 'component/button'; import Button from 'component/button';
import UserPhoneNew from 'component/userPhoneNew';
import UserPhoneVerify from 'component/userPhoneVerify'; import UserPhoneVerify from 'component/userPhoneVerify';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
const LazyUserPhoneNew = React.lazy(() => import(
/* webpackChunkName: "userPhoneNew" */
'component/userPhoneNew'
));
type Props = { type Props = {
phone: ?number, phone: ?number,
user: { user: {
@ -31,7 +35,11 @@ class ModalPhoneCollection extends React.PureComponent<Props> {
const cancelButton = <Button button="link" onClick={closeModal} label={__('Not Now')} />; const cancelButton = <Button button="link" onClick={closeModal} label={__('Not Now')} />;
if (!user.phone_number && !phone) { if (!user.phone_number && !phone) {
return <UserPhoneNew cancelButton={cancelButton} />; return (
<Suspense fallback={<div></div>}>
<UserPhoneNew cancelButton={cancelButton} />
</Suspense>
);
} else if (!user.phone_number) { } else if (!user.phone_number) {
return <UserPhoneVerify cancelButton={cancelButton} />; return <UserPhoneVerify cancelButton={cancelButton} />;
} }

View file

@ -147,6 +147,11 @@ class FilePage extends React.Component<Props> {
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw; const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
const fileName = fileInfo ? fileInfo.file_name : null; const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = getMediaType(contentType, fileName); const mediaType = getMediaType(contentType, fileName);
console.log({
mediaType,
contentType,
fileName,
})
const showFile = const showFile =
PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType); PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);

View file

@ -24,8 +24,10 @@ import {
parseURI, parseURI,
creditsToString, creditsToString,
doError, doError,
makeSelectCostInfoForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import {
makeSelectCostInfoForUri,
} from 'lbryinc';
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
import analytics from 'analytics'; import analytics from 'analytics';
import { formatLbryUriForWeb } from 'util/uri'; import { formatLbryUriForWeb } from 'util/uri';

View file

@ -16,7 +16,6 @@ import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subs
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 } from 'lbry-redux';
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content'; import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';
import Promise from 'bluebird';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000; const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;

View file

@ -3,7 +3,6 @@ const webpack = require('webpack');
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const { DefinePlugin, ProvidePlugin } = require('webpack'); const { DefinePlugin, ProvidePlugin } = require('webpack');
const { getIfUtils, removeEmpty } = require('webpack-config-utils'); const { getIfUtils, removeEmpty } = require('webpack-config-utils');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const NODE_ENV = process.env.NODE_ENV || 'development'; const NODE_ENV = process.env.NODE_ENV || 'development';
@ -41,11 +40,27 @@ let baseConfig = {
loader: 'babel-loader', loader: 'babel-loader',
}, },
{ {
test: /\.s?css$/, test: /\.module.scss$/,
use: [ use: [
'style-loader', // creates style nodes from JS strings 'style-loader',
'css-loader', // translates CSS into CommonJS {
'sass-loader', // compiles Sass to CSS, using Node Sass by default loader: 'css-loader',
options: {
modules: true,
},
},
'postcss-loader',
'sass-loader',
],
},
{
test: /\.s?css$/,
exclude: /\.module.scss$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
], ],
}, },
{ {
@ -79,12 +94,24 @@ let baseConfig = {
resolve: { resolve: {
modules: [UI_ROOT, 'node_modules', __dirname], modules: [UI_ROOT, 'node_modules', __dirname],
extensions: ['.js', '.jsx', '.json', '.scss'], extensions: ['.js', '.jsx', '.json', '.scss'],
alias: {
'lbry-redux$': 'lbry-redux/dist/bundle.es.js',
// Build optimizations for 'redux-persist-transform-filter'
'redux-persist-transform-filter': 'redux-persist-transform-filter/index.js',
'lodash.get': 'lodash-es/get',
'lodash.set': 'lodash-es/set',
'lodash.unset': 'lodash-es/unset',
'lodash.pickby': 'lodash-es/pickBy',
'lodash.isempty': 'lodash-es/isEmpty',
'lodash.forin': 'lodash-es/forIn',
'lodash.clonedeep': 'lodash-es/cloneDeep',
},
}, },
plugins: [ plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.EnvironmentPlugin(['NODE_ENV']), new webpack.EnvironmentPlugin(['NODE_ENV']),
// new BundleAnalyzerPlugin(),
new ProvidePlugin({ new ProvidePlugin({
i18n: ['i18n', 'default'], i18n: ['i18n', 'default'],
__: ['i18n/__', 'default'], __: ['i18n/__', 'default'],

View file

@ -5,6 +5,7 @@ const baseConfig = require('./webpack.base.config.js');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const { DefinePlugin } = require('webpack'); const { DefinePlugin } = require('webpack');
const { getIfUtils, removeEmpty } = require('webpack-config-utils'); const { getIfUtils, removeEmpty } = require('webpack-config-utils');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const STATIC_ROOT = path.resolve(__dirname, 'static/'); const STATIC_ROOT = path.resolve(__dirname, 'static/');
const DIST_ROOT = path.resolve(__dirname, 'dist/'); const DIST_ROOT = path.resolve(__dirname, 'dist/');
@ -108,6 +109,7 @@ const renderConfig = {
], ],
}, },
plugins: [ plugins: [
// new BundleAnalyzerPlugin(),
new DefinePlugin({ new DefinePlugin({
IS_WEB: JSON.stringify(false), IS_WEB: JSON.stringify(false),
}), }),

2081
yarn.lock

File diff suppressed because it is too large Load diff