feat: additional file types on lbry.tv

Should support markdown, PDF, and anything in the code viewer. Tested that it's working on web and app.
This commit is contained in:
Thomas Zarebczan 2019-11-26 14:08:34 -05:00 committed by Sean Yesmunt
parent 66ccbb468b
commit 722c0b978c
11 changed files with 81 additions and 23 deletions

View file

@ -4,7 +4,6 @@ module.exports = api => {
return { return {
presets: ['@babel/env', '@babel/react', '@babel/flow'], presets: ['@babel/env', '@babel/react', '@babel/flow'],
plugins: [ plugins: [
'@babel/plugin-syntax-dynamic-import',
'import-glob', 'import-glob',
'@babel/plugin-transform-runtime', '@babel/plugin-transform-runtime',
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],

View file

@ -16,11 +16,11 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
// @if TARGET='app'
import DocumentViewer from 'component/viewers/documentViewer'; import DocumentViewer from 'component/viewers/documentViewer';
import DocxViewer from 'component/viewers/docxViewer';
import HtmlViewer from 'component/viewers/htmlViewer';
import PdfViewer from 'component/viewers/pdfViewer'; import PdfViewer from 'component/viewers/pdfViewer';
import HtmlViewer from 'component/viewers/htmlViewer';
// @if TARGET='app'
import DocxViewer from 'component/viewers/docxViewer';
import ComicBookViewer from 'component/viewers/comicBookViewer'; import ComicBookViewer from 'component/viewers/comicBookViewer';
import ThreeViewer from 'component/viewers/threeViewer'; import ThreeViewer from 'component/viewers/threeViewer';
// @endif // @endif
@ -102,26 +102,33 @@ class FileRender extends React.PureComponent<Props> {
// Add routes to viewer... // Add routes to viewer...
}; };
// Supported contentTypes
const contentTypes = {
'application/pdf': <PdfViewer source={downloadPath || source} />,
'text/html': <HtmlViewer source={downloadPath || source} />,
'text/htm': <HtmlViewer source={downloadPath || source} />,
};
// Supported fileType // Supported fileType
const fileTypes = { const fileTypes = {
// @if TARGET='app' // @if TARGET='app'
pdf: <PdfViewer source={downloadPath} />,
docx: <DocxViewer source={downloadPath} />, docx: <DocxViewer source={downloadPath} />,
html: <HtmlViewer source={downloadPath} />,
htm: <HtmlViewer source={downloadPath} />,
// @endif // @endif
// Add routes to viewer... // Add routes to viewer...
}; };
// Check for a valid fileType or mediaType // Check for a valid fileType, mediaType, or contentType
let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType]; let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType] || contentTypes[contentType];
// Check for Human-readable files // Check for Human-readable files
if (!viewer && readableFiles.includes(mediaType)) { if (!viewer && readableFiles.includes(mediaType)) {
viewer = ( viewer = (
<DocumentViewer <DocumentViewer
source={{ source={{
stream: options => fs.createReadStream(downloadPath, options), // @if TARGET='app'
file: options => fs.createReadStream(downloadPath, options),
// @endif
stream: source,
fileType, fileType,
contentType, contentType,
}} }}

View file

@ -5,6 +5,7 @@ import {
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri, makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri, makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable, makeSelectUriIsStreamable,
makeSelectTitleForUri, makeSelectTitleForUri,
} from 'lbry-redux'; } from 'lbry-redux';
@ -23,6 +24,7 @@ const select = (state, props) => {
title: makeSelectTitleForUri(uri)(state), title: makeSelectTitleForUri(uri)(state),
thumbnail: makeSelectThumbnailForUri(uri)(state), thumbnail: makeSelectThumbnailForUri(uri)(state),
mediaType: makeSelectMediaTypeForUri(uri)(state), mediaType: makeSelectMediaTypeForUri(uri)(state),
contentType: makeSelectContentTypeForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state), fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: makeSelectShouldObscurePreview(uri)(state), obscurePreview: makeSelectShouldObscurePreview(uri)(state),
isPlaying: makeSelectIsPlaying(uri)(state), isPlaying: makeSelectIsPlaying(uri)(state),

View file

@ -15,6 +15,7 @@ import { onFullscreenChange } from 'util/full-screen';
type Props = { type Props = {
mediaType: string, mediaType: string,
contentType: string,
isLoading: boolean, isLoading: boolean,
isPlaying: boolean, isPlaying: boolean,
fileInfo: FileListItem, fileInfo: FileListItem,
@ -47,6 +48,7 @@ export default function FileViewer(props: Props) {
triggerAnalyticsView, triggerAnalyticsView,
claimRewards, claimRewards,
mediaType, mediaType,
contentType,
} = props; } = props;
const [playTime, setPlayTime] = useState(); const [playTime, setPlayTime] = useState();
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect'); const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
@ -56,7 +58,9 @@ export default function FileViewer(props: Props) {
}); });
const inline = pageUri === uri; const inline = pageUri === uri;
const isReadyToPlay = (IS_WEB && isStreamable) || (isStreamable && streamingUrl) || (fileInfo && fileInfo.completed); const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const isReadyToPlay =
(IS_WEB && (isStreamable || webStreamOnly)) || (isStreamable && streamingUrl) || (fileInfo && fileInfo.completed);
const loadingMessage = const loadingMessage =
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes) !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.") ? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")

View file

@ -6,6 +6,7 @@ import {
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri, makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri, makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable, makeSelectUriIsStreamable,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc'; import { makeSelectCostInfoForUri } from 'lbryinc';
@ -16,6 +17,7 @@ import FileViewer from './view';
const select = (state, props) => ({ const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state), mediaType: makeSelectMediaTypeForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state), obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state), isPlaying: makeSelectIsPlaying(props.uri)(state),

View file

@ -14,6 +14,7 @@ const SPACE_BAR_KEYCODE = 32;
type Props = { type Props = {
play: string => void, play: string => void,
mediaType: string, mediaType: string,
contentType: string,
isLoading: boolean, isLoading: boolean,
isPlaying: boolean, isPlaying: boolean,
fileInfo: FileListItem, fileInfo: FileListItem,
@ -31,6 +32,7 @@ export default function FileViewer(props: Props) {
const { const {
play, play,
mediaType, mediaType,
contentType,
isPlaying, isPlaying,
fileInfo, fileInfo,
uri, uri,
@ -45,7 +47,8 @@ export default function FileViewer(props: Props) {
const cost = costInfo && costInfo.cost; const cost = costInfo && costInfo.cost;
const isPlayable = ['audio', 'video'].includes(mediaType); const isPlayable = ['audio', 'video'].includes(mediaType);
const fileStatus = fileInfo && fileInfo.status; const fileStatus = fileInfo && fileInfo.status;
const supported = (IS_WEB && isStreamable) || !IS_WEB; const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const supported = (IS_WEB && (isStreamable || webStreamOnly)) || !IS_WEB;
// Wrap this in useCallback because we need to use it to the keyboard effect // Wrap this in useCallback because we need to use it to the keyboard effect
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render // If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render

View file

@ -1,14 +1,16 @@
// @flow // @flow
import React, { Suspense } from 'react'; import React 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';
import CodeViewer from 'component/viewers/codeViewer'; import CodeViewer from 'component/viewers/codeViewer';
import * as https from 'https';
type Props = { type Props = {
theme: string, theme: string,
source: { source: {
stream: string => any, file: (?string) => any,
stream: string,
fileType: string, fileType: string,
contentType: string, contentType: string,
}, },
@ -32,9 +34,9 @@ class DocumentViewer extends React.PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
const { source } = this.props; const { source } = this.props;
// @if TARGET='app'
if (source && source.stream) { if (source && source.file) {
const stream = source.stream('utf8'); const stream = source.file('utf8');
let data = ''; let data = '';
@ -50,6 +52,32 @@ class DocumentViewer extends React.PureComponent<Props, State> {
this.setState({ error: true, loading: false }); this.setState({ error: true, loading: false });
}); });
} }
// @endif
// @if TARGET='web'
if (source && source.stream) {
https.get(
source.stream,
function(response) {
if (response.statusCode === 200) {
let body = '';
let i = 0;
response.on('data', function(chunk) {
i = i + 1;
body += chunk;
});
response.on(
'end',
function() {
this.setState({ content: body, loading: false });
}.bind(this)
);
} else {
this.setState({ error: true, loading: false });
}
}.bind(this)
);
}
// @endif
} }
renderDocument() { renderDocument() {
@ -58,8 +86,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
const { source, theme } = this.props; const { source, theme } = this.props;
const { fileType, contentType } = source; const { fileType, contentType } = source;
const markdownType = ['md', 'markdown']; const markdownType = ['md', 'markdown'];
if (markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md') {
if (markdownType.includes(fileType)) {
// Render markdown // Render markdown
viewer = <MarkdownPreview content={content} promptLinks />; viewer = <MarkdownPreview content={content} promptLinks />;
} else { } else {

View file

@ -11,7 +11,12 @@ class HtmlViewer extends React.PureComponent<Props> {
const { source } = this.props; const { source } = this.props;
return ( return (
<div className="file-render__viewer" onContextMenu={stopContextMenu}> <div className="file-render__viewer" onContextMenu={stopContextMenu}>
{/* @if TARGET='app' */}
<iframe sandbox="" title={__('File preview')} src={`file://${source}`} /> <iframe sandbox="" title={__('File preview')} src={`file://${source}`} />
{/* @endif */}
{/* @if TARGET='web' */}
<iframe sandbox="" title={__('File preview')} src={source} />
{/* @endif */}
</div> </div>
); );
} }

View file

@ -27,21 +27,27 @@ class PdfViewer extends React.PureComponent<Props> {
// @if TARGET='app' // @if TARGET='app'
shell.openExternal(path); shell.openExternal(path);
// @endif // @endif
// @if TARGET='web'
console.error('provide stub for shell.openExternal'); // eslint-disable-line
// @endif
} }
render() { render() {
// We used to be able to just render a webview and display the pdf inside the app // We used to be able to just render a webview and display the pdf inside the app
// This was disabled on electron@3 // This was disabled on electron@3
// https://github.com/electron/electron/issues/12337 // https://github.com/electron/electron/issues/12337
const { source } = this.props;
return ( return (
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}> <div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
{/* @if TARGET='app' */}
<p> <p>
{__('PDF opened externally.')} <Button button="link" label={__('Click here')} onClick={this.openFile} />{' '} {__('PDF opened externally.')} <Button button="link" label={__('Click here')} onClick={this.openFile} />{' '}
{__('to open it again.')} {__('to open it again.')}
</p> </p>
{/* @endif */}
{/* @if TARGET='web' */}
<div className="file-render__viewer">
<iframe title={__('File preview')} src={source} />
</div>
{/* @endif */}
</div> </div>
); );
} }

View file

@ -34,6 +34,9 @@ let baseConfig = {
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
loader: 'babel-loader', loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-syntax-dynamic-import'],
},
}, },
{ {
test: /\.s?css$/, test: /\.s?css$/,