File downloads and refactoring #3918
61 changed files with 1018 additions and 1118 deletions
|
@ -11,6 +11,8 @@ type Props = {
|
|||
export default function NagDegradedPerformance(props: Props) {
|
||||
const { onClose } = props;
|
||||
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Nag
|
||||
type="error"
|
||||
|
|
|
@ -84,6 +84,8 @@ function OpenInAppLink(props: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Nag
|
||||
type="helpful"
|
||||
|
|
|
@ -335,7 +335,6 @@
|
|||
"credits": "credits",
|
||||
"No channel name after @.": "No channel name after @.",
|
||||
"View channel": "View channel",
|
||||
"Add to your library": "Add to your library",
|
||||
"Web link": "Web link",
|
||||
"Facebook": "Facebook",
|
||||
"Twitter": "Twitter",
|
||||
|
@ -842,7 +841,6 @@
|
|||
"Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.": "Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.",
|
||||
"Loading 3D model.": "Loading 3D model.",
|
||||
"Click here": "Click here",
|
||||
"PDF opened externally. %click_here% to open it again.": "PDF opened externally. %click_here% to open it again.",
|
||||
"Wallet Server": "Wallet Server",
|
||||
"lbry.tv wallet servers": "lbry.tv wallet servers",
|
||||
"Custom wallet servers": "Custom wallet servers",
|
||||
|
|
8
ui/component/IframeReact/index.js
Normal file
8
ui/component/IframeReact/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { connect } from 'react-redux';
|
||||
import IframeReact from './view';
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = () => ({});
|
||||
|
||||
export default connect(select, perform)(IframeReact);
|
35
ui/component/IframeReact/view.jsx
Normal file
35
ui/component/IframeReact/view.jsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
fullHeight: boolean,
|
||||
src: string,
|
||||
title: string,
|
||||
};
|
||||
|
||||
export default function I18nMessage(props: Props) {
|
||||
|
||||
const { src, title } = props;
|
||||
|
||||
// const iframeRef = useRef();
|
||||
|
||||
// const [iframeHeight, setIframeHeight] = useState('80vh');
|
||||
|
||||
function onLoad() {
|
||||
/*
|
||||
|
||||
iframe domain restrictions prevent naive design :-(
|
||||
|
||||
const obj = iframeRef.current;
|
||||
if (obj) {
|
||||
setIframeHeight(obj.contentWindow.document.body.scrollHeight + 'px');
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
return (
|
||||
// style={{height: iframeHeight}}
|
||||
// ref={iframeRef}
|
||||
<iframe src={src} title={title} onLoad={onLoad} />
|
||||
);
|
||||
}
|
|
@ -10,7 +10,7 @@ import ReactModal from 'react-modal';
|
|||
import { openContextMenu } from 'util/context-menu';
|
||||
import useKonamiListener from 'util/enhanced-layout';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import FloatingViewer from 'component/floatingViewer';
|
||||
import FileRenderFloating from 'component/fileRenderFloating';
|
||||
import { withRouter } from 'react-router';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
import Nag from 'component/common/nag';
|
||||
|
@ -286,7 +286,7 @@ function App(props: Props) {
|
|||
<React.Fragment>
|
||||
<Router />
|
||||
<ModalRouter />
|
||||
<FloatingViewer pageUri={uri} />
|
||||
<FileRenderFloating pageUri={uri} />
|
||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
|
|
9
ui/component/claimInsufficientCredits/index.js
Normal file
9
ui/component/claimInsufficientCredits/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
|
||||
import ClaimInsufficientCredits from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select)(ClaimInsufficientCredits);
|
33
ui/component/claimInsufficientCredits/view.jsx
Normal file
33
ui/component/claimInsufficientCredits/view.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
fileInfo: FileListItem,
|
||||
isInsufficientCredits: boolean,
|
||||
};
|
||||
|
||||
function ClaimInsufficientCredits(props: Props) {
|
||||
const { isInsufficientCredits, fileInfo } = props;
|
||||
|
||||
if (fileInfo || !isInsufficientCredits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media__insufficient-credits help--warning">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
|
||||
}}
|
||||
>
|
||||
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it. Check
|
||||
out %reward_link% for free LBC or send more LBC to your wallet.
|
||||
</I18nMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimInsufficientCredits;
|
|
@ -6,6 +6,7 @@ import classnames from 'classnames';
|
|||
import ClaimPreview from 'component/claimPreview';
|
||||
import Spinner from 'component/spinner';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Card from 'component/common/card';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
const SORT_NEW = 'new';
|
||||
|
@ -161,9 +162,9 @@ export default function ClaimList(props: Props) {
|
|||
</ul>
|
||||
)}
|
||||
{!timedOut && urisLength === 0 && !loading && (
|
||||
<div className="card--section main--empty empty">{empty || __('No results')}</div>
|
||||
<div className="empty empty--centered">{empty || __('No results')}</div>
|
||||
)}
|
||||
{timedOut && timedOutMessage && <div className="card--section main--empty empty">{timedOutMessage}</div>}
|
||||
{timedOut && timedOutMessage && <div className="empty empty--centered">{timedOutMessage}</div>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,11 +11,10 @@ import {
|
|||
selectBlockedChannels,
|
||||
selectChannelIsBlocked,
|
||||
doFileGet,
|
||||
makeSelectStreamingUrlForUri,
|
||||
} from 'lbry-redux';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||
import { makeSelectHasVisitedUri, makeSelectStreamingUrlForUriWebProxy } from 'redux/selectors/content';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import ClaimPreview from './view';
|
||||
|
||||
|
@ -34,7 +33,7 @@ const select = (state, props) => ({
|
|||
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
||||
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
||||
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
|
||||
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -42,7 +41,4 @@ const perform = dispatch => ({
|
|||
getFile: uri => dispatch(doFileGet(uri, false)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ClaimPreview);
|
||||
export default connect(select, perform)(ClaimPreview);
|
||||
|
|
|
@ -11,26 +11,47 @@ type Props = {
|
|||
actions?: string | Node,
|
||||
icon?: string,
|
||||
className?: string,
|
||||
isPageTitle?: boolean,
|
||||
isBodyTable?: boolean,
|
||||
actionIconPadding?: boolean,
|
||||
};
|
||||
|
||||
export default function Card(props: Props) {
|
||||
const { title, subtitle, body, actions, icon, className, actionIconPadding = true } = props;
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
body,
|
||||
actions,
|
||||
icon,
|
||||
className,
|
||||
isPageTitle = false,
|
||||
isBodyTable = false,
|
||||
For cards that should have title as an h1 For cards that should have title as an h1
|
||||
actionIconPadding = true,
|
||||
New option for cards where the entire body is a table (there was one on Help) New option for cards where the entire body is a table (there was one on Help)
|
||||
} = props;
|
||||
return (
|
||||
<section className={classnames(className, 'card')}>
|
||||
{(title || subtitle) && (
|
||||
<div className="card__header">
|
||||
<div className="section__flex">
|
||||
{icon && <Icon sectionIcon icon={icon} />}
|
||||
<div>
|
||||
<h2 className="section__title">{title}</h2>
|
||||
{subtitle && <div className="section__subtitle">{subtitle}</div>}
|
||||
</div>
|
||||
{isPageTitle && <h1 className="card__title">{title}</h1>}
|
||||
{!isPageTitle && <h2 className="card__title">{title}</h2>}
|
||||
{subtitle && <div className="card__subtitle">{subtitle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>}
|
||||
{body && (
|
||||
<div
|
||||
className={classnames('card__body', {
|
||||
'card__body--with-icon': icon,
|
||||
'card__body--no-title': !title && !subtitle,
|
||||
'card__body--table': isBodyTable,
|
||||
})}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
)}
|
||||
{actions && (
|
||||
<div
|
||||
add support cards without titles add support cards without titles
|
||||
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}
|
||||
|
|
|
@ -10,17 +10,24 @@ type Props = {
|
|||
actionText: string,
|
||||
href?: string,
|
||||
type?: string,
|
||||
inline?: boolean,
|
||||
onClick?: () => void,
|
||||
onClose?: () => void,
|
||||
};
|
||||
|
||||
export default function Nag(props: Props) {
|
||||
const { message, actionText, href, onClick, onClose, type } = props;
|
||||
const { message, actionText, href, onClick, onClose, type, inline } = props;
|
||||
|
||||
const buttonProps = onClick ? { onClick } : { href };
|
||||
|
||||
return (
|
||||
<div className={classnames('nag', { 'nag--helpful': type === 'helpful', 'nag--error': type === 'error' })}>
|
||||
<div
|
||||
className={classnames('nag', {
|
||||
'nag--helpful': type === 'helpful',
|
||||
'nag--error': type === 'error',
|
||||
'nag--inline': inline,
|
||||
})}
|
||||
>
|
||||
Add support for inline nags (used on the player). Inline is possibly a bad term as I review this, should possibly be absolute? Add support for inline nags (used on the player). Inline is possibly a bad term as I review this, should possibly be absolute?
|
||||
<div className="nag__message">{message}</div>
|
||||
<Button
|
||||
className={classnames('nag__button', {
|
||||
|
|
|
@ -73,7 +73,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
|||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="main main--empty">
|
||||
<div className="main main--full-width main--empty">
|
||||
<Yrbl
|
||||
type="sad"
|
||||
title={__('Aw shucks!')}
|
||||
|
|
|
@ -60,7 +60,7 @@ function FileActions(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="media__actions">
|
||||
<div className="section__actions">
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
button="alt"
|
||||
icon={ICONS.SHARE}
|
||||
|
@ -97,7 +97,7 @@ function FileActions(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="section__actions">
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<FileDownloadLink uri={uri} />
|
||||
|
||||
{claimIsMine && (
|
||||
|
|
|
@ -5,6 +5,7 @@ import Button from 'component/button';
|
|||
import Expandable from 'component/expandable';
|
||||
import path from 'path';
|
||||
import ClaimTags from 'component/claimTags';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -42,6 +43,9 @@ class FileDetails extends PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card
|
||||
title={__('Details')}
|
||||
body={
|
||||
<Expandable>
|
||||
{description && (
|
||||
<div className="media__info-text">
|
||||
|
@ -109,6 +113,8 @@ class FileDetails extends PureComponent<Props> {
|
|||
</tbody>
|
||||
</table>
|
||||
</Expandable>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,13 +11,14 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
downloading: boolean,
|
||||
loading: boolean,
|
||||
isStreamable: boolean,
|
||||
fileInfo: ?FileListItem,
|
||||
openModal: (id: string, { path: string }) => void,
|
||||
pause: () => void,
|
||||
download: string => void,
|
||||
triggerViewEvent: string => void,
|
||||
costInfo: ?{ cost: string },
|
||||
buttonType: ?string,
|
||||
showLabel: ?boolean,
|
||||
hideOpenButton: boolean,
|
||||
hideDownloadStatus: boolean,
|
||||
};
|
||||
|
@ -35,6 +36,8 @@ function FileDownloadLink(props: Props) {
|
|||
claim,
|
||||
triggerViewEvent,
|
||||
costInfo,
|
||||
buttonType = 'alt',
|
||||
showLabel = false,
|
||||
hideOpenButton = false,
|
||||
hideDownloadStatus = false,
|
||||
} = props;
|
||||
|
@ -73,10 +76,12 @@ function FileDownloadLink(props: Props) {
|
|||
}
|
||||
|
||||
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
||||
const openLabel = __('Open file');
|
||||
return hideOpenButton ? null : (
|
||||
<Button
|
||||
button="alt"
|
||||
title={__('Open file')}
|
||||
button={buttonType}
|
||||
title={openLabel}
|
||||
label={showLabel ? openLabel : null}
|
||||
icon={ICONS.EXTERNAL}
|
||||
onClick={() => {
|
||||
pause();
|
||||
|
@ -86,11 +91,14 @@ function FileDownloadLink(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const label = IS_WEB ? __('Download') : __('Download to your Library');
|
||||
|
||||
return (
|
||||
<Button
|
||||
button="alt"
|
||||
title={IS_WEB ? __('Download') : __('Add to your library')}
|
||||
button={buttonType}
|
||||
title={label}
|
||||
icon={ICONS.DOWNLOAD}
|
||||
label={showLabel ? label : null}
|
||||
onClick={handleDownload}
|
||||
// @if TARGET='web'
|
||||
download={fileName}
|
||||
|
|
|
@ -3,14 +3,16 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectDownloadPathForUri,
|
||||
makeSelectFileNameForUri,
|
||||
} from 'lbry-redux';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectIsText } from 'redux/selectors/content';
|
||||
import {
|
||||
makeSelectFileRenderModeForUri,
|
||||
makeSelectFileExtensionForUri,
|
||||
makeSelectStreamingUrlForUriWebProxy,
|
||||
} from 'redux/selectors/content';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import FileRender from './view';
|
||||
|
||||
|
@ -23,10 +25,10 @@ const select = (state, props) => {
|
|||
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),
|
||||
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
autoplay: autoplay,
|
||||
isText: makeSelectIsText(props.uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -34,7 +36,4 @@ const perform = dispatch => ({
|
|||
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileRender);
|
||||
export default connect(select, perform)(FileRender);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import { URL } from 'config';
|
||||
import { remote } from 'electron';
|
||||
import React, { Suspense, Fragment } from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import VideoViewer from 'component/viewers/videoViewer';
|
||||
import ImageViewer from 'component/viewers/imageViewer';
|
||||
import AppViewer from 'component/viewers/appViewer';
|
||||
|
@ -11,18 +11,14 @@ import Button from 'component/button';
|
|||
import { withRouter } from 'react-router-dom';
|
||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
// @if TARGET='web'
|
||||
import { generateStreamUrl } from 'util/lbrytv';
|
||||
// @endif
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
import DocumentViewer from 'component/viewers/documentViewer';
|
||||
import PdfViewer from 'component/viewers/pdfViewer';
|
||||
moved into proxied selector, this way we can assume when selecting a streamUrl it will just work moved into proxied selector, this way we can assume when selecting a streamUrl it will just work
|
||||
import HtmlViewer from 'component/viewers/htmlViewer';
|
||||
|
||||
// @if TARGET='app'
|
||||
// should match
|
||||
import DocxViewer from 'component/viewers/docxViewer';
|
||||
import ComicBookViewer from 'component/viewers/comicBookViewer';
|
||||
import ThreeViewer from 'component/viewers/threeViewer';
|
||||
|
@ -31,17 +27,17 @@ import ThreeViewer from 'component/viewers/threeViewer';
|
|||
type Props = {
|
||||
uri: string,
|
||||
mediaType: string,
|
||||
isText: true,
|
||||
streamingUrl: string,
|
||||
embedded?: boolean,
|
||||
contentType: string,
|
||||
claim: StreamClaim,
|
||||
currentTheme: string,
|
||||
downloadPath: string,
|
||||
fileName: string,
|
||||
fileExtension: string,
|
||||
autoplay: boolean,
|
||||
setPlayingUri: (string | null) => void,
|
||||
currentlyFloating: boolean,
|
||||
renderMode: string,
|
||||
thumbnail: string,
|
||||
};
|
||||
|
||||
|
@ -113,114 +109,50 @@ class FileRender extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
renderViewer() {
|
||||
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
|
||||
const fileType = fileName && path.extname(fileName).substring(1);
|
||||
const { currentTheme, contentType, downloadPath, fileExtension, streamingUrl, uri, renderMode } = this.props;
|
||||
const source = streamingUrl;
|
||||
|
||||
// Ideally the lbrytv api server would just replace the streaming_url returned by the sdk so we don't need this check
|
||||
// https://github.com/lbryio/lbrytv/issues/51
|
||||
const source = IS_WEB ? generateStreamUrl(claim.name, claim.claim_id) : streamingUrl;
|
||||
|
||||
// Human-readable files (scripts and plain-text files)
|
||||
const readableFiles = ['text', 'document', 'script'];
|
||||
|
||||
// Supported mediaTypes
|
||||
const mediaTypes = {
|
||||
// @if TARGET='app'
|
||||
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
||||
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
||||
application: <AppViewer uri={uri} />,
|
||||
// @endif
|
||||
|
||||
video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
|
||||
audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
|
||||
image: <ImageViewer uri={uri} source={source} />,
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
// Supported contentTypes
|
||||
const contentTypes = {
|
||||
'application/x-ext-mkv': (
|
||||
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
|
||||
),
|
||||
'video/x-matroska': (
|
||||
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
|
||||
),
|
||||
'application/pdf': <PdfViewer source={downloadPath || source} />,
|
||||
'text/html': <HtmlViewer source={downloadPath || source} />,
|
||||
'text/htm': <HtmlViewer source={downloadPath || source} />,
|
||||
};
|
||||
|
||||
// Supported fileType
|
||||
const fileTypes = {
|
||||
// @if TARGET='app'
|
||||
docx: <DocxViewer source={downloadPath} />,
|
||||
// @endif
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
// Check for a valid fileType, mediaType, or contentType
|
||||
let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType] || contentTypes[contentType];
|
||||
|
||||
// Check for Human-readable files
|
||||
if (!viewer && readableFiles.includes(mediaType)) {
|
||||
viewer = (
|
||||
switch (renderMode) {
|
||||
case RENDER_MODES.AUDIO:
|
||||
case RENDER_MODES.VIDEO:
|
||||
return <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />;
|
||||
case RENDER_MODES.IMAGE:
|
||||
return <ImageViewer uri={uri} source={source} />;
|
||||
case RENDER_MODES.HTML:
|
||||
return <HtmlViewer source={downloadPath || source} />;
|
||||
case RENDER_MODES.DOCUMENT:
|
||||
case RENDER_MODES.MARKDOWN:
|
||||
return (
|
||||
<DocumentViewer
|
||||
source={{
|
||||
// @if TARGET='app'
|
||||
file: options => fs.createReadStream(downloadPath, options),
|
||||
// @endif
|
||||
stream: source,
|
||||
fileType,
|
||||
fileExtension,
|
||||
contentType,
|
||||
}}
|
||||
renderMode={renderMode}
|
||||
theme={currentTheme}
|
||||
/>
|
||||
);
|
||||
case RENDER_MODES.DOCX:
|
||||
return <DocxViewer source={downloadPath} />;
|
||||
case RENDER_MODES.PDF:
|
||||
return <PdfViewer source={downloadPath || source} />;
|
||||
case RENDER_MODES.CAD:
|
||||
return <ThreeViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
|
||||
case RENDER_MODES.COMIC:
|
||||
return <ComicBookViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
|
||||
case RENDER_MODES.APPLICATION:
|
||||
return <AppViewer uri={uri} />;
|
||||
}
|
||||
|
||||
// @if TARGET='web'
|
||||
// temp workaround to disabled paid content on web
|
||||
if (claim && claim.value.fee && Number(claim.value.fee.amount) > 0) {
|
||||
const paidMessage = __(
|
||||
'Currently, only free content is available on lbry.tv. Try viewing it in the desktop app.'
|
||||
);
|
||||
const paid = <LoadingScreen status={paidMessage} spinner={false} />;
|
||||
return paid;
|
||||
return null;
|
||||
}
|
||||
// @endif
|
||||
|
||||
const unsupported = IS_WEB ? (
|
||||
<div className={'content__cover--disabled'}>
|
||||
<Yrbl
|
||||
className={'content__cover--disabled'}
|
||||
title={'Not available on lbry.tv'}
|
||||
subtitle={
|
||||
<Fragment>
|
||||
<p>
|
||||
{__('Good news, though! You can')}{' '}
|
||||
<Button button="link" label={__('Download the desktop app')} href="https://lbry.com/get" />{' '}
|
||||
{'and have access to all file types.'}
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
uri={uri}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={'content__cover--disabled'}>
|
||||
<Yrbl
|
||||
title={'Content Downloaded'}
|
||||
subtitle={'This file is unsupported here, but you can view the content in an application of your choice'}
|
||||
uri={uri}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Return viewer
|
||||
return viewer || unsupported;
|
||||
}
|
||||
render() {
|
||||
const { isText, uri, currentlyFloating, embedded } = this.props;
|
||||
const { uri, currentlyFloating, embedded, renderMode } = this.props;
|
||||
const { showAutoplayCountdown, showEmbededMessage } = this.state;
|
||||
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
|
||||
|
||||
|
@ -228,7 +160,7 @@ class FileRender extends React.PureComponent<Props, State> {
|
|||
<div
|
||||
className={classnames({
|
||||
'file-render': !embedded,
|
||||
'file-render--document': isText && !embedded,
|
||||
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
||||
'file-render__embed': embedded,
|
||||
})}
|
||||
>
|
||||
|
|
10
ui/component/fileRenderDownload/index.js
Normal file
10
ui/component/fileRenderDownload/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import FileRenderDownload from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select)(FileRenderDownload));
|
43
ui/component/fileRenderDownload/view.jsx
Normal file
43
ui/component/fileRenderDownload/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
New view for downloadable content. Download only content does not go through New view for downloadable content. Download only content does not go through `<FileRenderInitiator>` (previous ViewerInitiator)
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import Card from 'component/common/card';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
isFree: boolean,
|
||||
renderMode: string,
|
||||
};
|
||||
|
||||
export default function FileRenderDownload(props: Props) {
|
||||
const { uri, renderMode, isFree } = props;
|
||||
|
||||
// @if TARGET='web'
|
||||
if (RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode)) {
|
||||
return (
|
||||
<Card
|
||||
title={isFree ? __('Download or Get the App') : __('Get the App')}
|
||||
subtitle={
|
||||
<p>
|
||||
{isFree
|
||||
? __(
|
||||
'This content can be downloaded from lbry.tv, but not displayed. It will display in LBRY Desktop, an app for desktop computers.'
|
||||
)
|
||||
: __('Paid content requires a full LBRY app.')}
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isFree && <FileDownloadLink uri={uri} buttonType="primary" showLabel />}
|
||||
<Button button={!isFree ? 'primary' : 'link'} label={__('Get the App')} href="https://lbry.com/get" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// @endif
|
||||
|
||||
return <Card title={__('Download')} actions={<FileDownloadLink uri={uri} buttonType="primary" showLabel />} />;
|
||||
}
|
|
@ -1,42 +1,29 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
makeSelectTitleForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
|
||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||
import {
|
||||
makeSelectIsPlaying,
|
||||
makeSelectShouldObscurePreview,
|
||||
selectPlayingUri,
|
||||
makeSelectIsText,
|
||||
makeSelectFileRenderModeForUri,
|
||||
makeSelectStreamingUrlForUriWebProxy,
|
||||
} from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doAnalyticsView } from 'redux/actions/app';
|
||||
import FileViewer from './view';
|
||||
import FileRenderFloating from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const uri = selectPlayingUri(state);
|
||||
return {
|
||||
uri,
|
||||
title: makeSelectTitleForUri(uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||
mediaType: makeSelectMediaTypeForUri(uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
|
||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
isText: makeSelectIsText(uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -46,9 +33,4 @@ const perform = dispatch => ({
|
|||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
});
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer)
|
||||
);
|
||||
export default withRouter(connect(select, perform)(FileRenderFloating));
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
|
@ -8,24 +9,17 @@ import FileRender from 'component/fileRender';
|
|||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
import { FILE_WRAPPER_CLASS } from 'component/layoutWrapperFile/view';
|
||||
import { FILE_WRAPPER_CLASS } from 'page/file/view';
|
||||
import Draggable from 'react-draggable';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import { onFullscreenChange } from 'util/full-screen';
|
||||
import useIsMobile from 'effects/use-is-mobile';
|
||||
|
||||
type Props = {
|
||||
mediaType: string,
|
||||
contentType: string,
|
||||
isText: boolean,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
obscurePreview: boolean,
|
||||
insufficientCredits: boolean,
|
||||
isStreamable: boolean,
|
||||
thumbnail?: string,
|
||||
streamingUrl?: string,
|
||||
floatingPlayer: boolean,
|
||||
pageUri: ?string,
|
||||
|
@ -33,25 +27,23 @@ type Props = {
|
|||
floatingPlayerEnabled: boolean,
|
||||
clearPlayingUri: () => void,
|
||||
triggerAnalyticsView: (string, number) => Promise<any>,
|
||||
renderMode: string,
|
||||
claimRewards: () => void,
|
||||
};
|
||||
|
||||
export default function FileViewer(props: Props) {
|
||||
export default function FloatingViewer(props: Props) {
|
||||
const {
|
||||
isPlaying,
|
||||
fileInfo,
|
||||
uri,
|
||||
streamingUrl,
|
||||
isStreamable,
|
||||
pageUri,
|
||||
title,
|
||||
clearPlayingUri,
|
||||
floatingPlayerEnabled,
|
||||
triggerAnalyticsView,
|
||||
claimRewards,
|
||||
mediaType,
|
||||
contentType,
|
||||
isText,
|
||||
renderMode,
|
||||
} = props;
|
||||
const isMobile = useIsMobile();
|
||||
const [playTime, setPlayTime] = useState();
|
||||
|
@ -60,40 +52,17 @@ export default function FileViewer(props: Props) {
|
|||
x: -25,
|
||||
y: window.innerHeight - 400,
|
||||
});
|
||||
|
||||
const inline = pageUri === uri;
|
||||
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
|
||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
||||
const isReadyToPlay =
|
||||
(IS_WEB && (isStreamable || webStreamOnly || forceVideo)) ||
|
||||
((isStreamable || forceVideo) && streamingUrl) ||
|
||||
(fileInfo && fileInfo.completed);
|
||||
const isPlayable = RENDER_MODES.PLAYABLE_MODES.includes(renderMode);
|
||||
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||
const loadingMessage =
|
||||
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
|
||||
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');
|
||||
|
||||
const previousUri = usePrevious(uri);
|
||||
const isNewView = uri && previousUri !== uri && isPlaying;
|
||||
const [hasRecordedView, setHasRecordedView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewView) {
|
||||
setPlayTime(Date.now());
|
||||
}
|
||||
}, [isNewView, uri]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playTime && isReadyToPlay && !hasRecordedView) {
|
||||
const timeToStart = Date.now() - playTime;
|
||||
triggerAnalyticsView(uri, timeToStart).then(() => {
|
||||
claimRewards();
|
||||
setHasRecordedView(false); // This is a terrible variable name, rename this
|
||||
setPlayTime(null);
|
||||
});
|
||||
}
|
||||
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
||||
|
@ -115,6 +84,27 @@ export default function FileViewer(props: Props) {
|
|||
};
|
||||
}, [setFileViewerRect, inline]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewView) {
|
||||
setPlayTime(Date.now());
|
||||
}
|
||||
}, [isNewView, uri]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playTime && isReadyToPlay && !hasRecordedView) {
|
||||
const timeToStart = Date.now() - playTime;
|
||||
triggerAnalyticsView(uri, timeToStart).then(() => {
|
||||
claimRewards();
|
||||
setHasRecordedView(false); // This is a terrible variable name, rename this
|
||||
setPlayTime(null);
|
||||
});
|
||||
}
|
||||
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
|
||||
|
||||
if (!isPlayable || !isPlaying || !uri || (!inline && (isMobile || !floatingPlayerEnabled))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleDrag(e, ui) {
|
||||
const { x, y } = position;
|
||||
const newX = x + ui.deltaX;
|
||||
|
@ -125,16 +115,6 @@ export default function FileViewer(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
const hidePlayer =
|
||||
isText ||
|
||||
!isPlaying ||
|
||||
!uri ||
|
||||
(!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
|
||||
|
||||
if (hidePlayer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
onDrag={handleDrag}
|
|
@ -1,41 +1,32 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
makeSelectClaimForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
makeSelectIsPlaying,
|
||||
makeSelectShouldObscurePreview,
|
||||
selectPlayingUri,
|
||||
makeSelectCanAutoplay,
|
||||
makeSelectIsText,
|
||||
makeSelectInsufficientCreditsForUri,
|
||||
makeSelectStreamingUrlForUriWebProxy,
|
||||
makeSelectFileRenderModeForUri,
|
||||
} from 'redux/selectors/content';
|
||||
import FileViewer from './view';
|
||||
import FileRenderInitiator from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
isAutoPlayable: makeSelectCanAutoplay(props.uri)(state),
|
||||
isText: makeSelectIsText(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
|
@ -48,7 +39,4 @@ const perform = dispatch => ({
|
|||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer);
|
||||
export default withRouter(connect(select, perform)(FileRenderInitiator));
|
|
@ -5,78 +5,52 @@
|
|||
// while a file is currently being viewed
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import Button from 'component/button';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { generateDownloadUrl } from 'util/lbrytv';
|
||||
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
||||
import Nag from 'component/common/nag';
|
||||
|
||||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
||||
type Props = {
|
||||
play: string => void,
|
||||
mediaType: string,
|
||||
isText: boolean,
|
||||
contentType: string,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
history: { push: string => void },
|
||||
obscurePreview: boolean,
|
||||
insufficientCredits: boolean,
|
||||
isStreamable: boolean,
|
||||
thumbnail?: string,
|
||||
autoplay: boolean,
|
||||
hasCostInfo: boolean,
|
||||
costInfo: any,
|
||||
isAutoPlayable: boolean,
|
||||
inline: boolean,
|
||||
renderMode: string,
|
||||
claim: StreamClaim,
|
||||
};
|
||||
|
||||
export default function FileViewerInitiator(props: Props) {
|
||||
export default function FileRenderInitiator(props: Props) {
|
||||
const {
|
||||
play,
|
||||
mediaType,
|
||||
isText,
|
||||
contentType,
|
||||
isPlaying,
|
||||
fileInfo,
|
||||
uri,
|
||||
obscurePreview,
|
||||
insufficientCredits,
|
||||
history,
|
||||
thumbnail,
|
||||
autoplay,
|
||||
isStreamable,
|
||||
renderMode,
|
||||
hasCostInfo,
|
||||
costInfo,
|
||||
isAutoPlayable,
|
||||
claim,
|
||||
} = props;
|
||||
|
||||
const cost = costInfo && costInfo.cost;
|
||||
const forceVideo = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
|
||||
const isImage = mediaType === 'image';
|
||||
const isFree = hasCostInfo && cost === 0;
|
||||
const fileStatus = fileInfo && fileInfo.status;
|
||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
||||
const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true;
|
||||
const { name, claim_id: claimId, value } = claim;
|
||||
const fileName = value && value.source && value.source.name;
|
||||
const downloadUrl = generateDownloadUrl(name, claimId);
|
||||
|
||||
function getTitle() {
|
||||
let message = __('Unsupported File');
|
||||
// @if TARGET='web'
|
||||
if (cost) {
|
||||
message = __('Paid Content Not Supported on lbry.tv');
|
||||
} else {
|
||||
message = __("We're not quite ready to display this file on lbry.tv yet");
|
||||
}
|
||||
// @endif
|
||||
|
||||
return message;
|
||||
}
|
||||
const isPlayable = RENDER_MODES.PLAYABLE_MODES.includes(renderMode);
|
||||
|
||||
// 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
|
||||
|
@ -112,45 +86,55 @@ export default function FileViewerInitiator(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
const videoOnPage = document.querySelector('video');
|
||||
if (((autoplay && !videoOnPage && isAutoPlayable) || isText || isImage) && hasCostInfo && cost === 0) {
|
||||
if (
|
||||
isFree &&
|
||||
((autoplay && !videoOnPage && RENDER_MODES.PLAYABLE_MODES.includes(renderMode)) ||
|
||||
RENDER_MODES.AUTO_RENDER_MODES.includes(renderMode))
|
||||
) {
|
||||
viewFile();
|
||||
}
|
||||
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost, isText, isImage]);
|
||||
}, [autoplay, viewFile, isFree, renderMode]);
|
||||
|
||||
/*
|
||||
once content is playing, let the appropriate <FileRender> take care of it...
|
||||
but for playables, always render so area can be used to fill with floating player
|
||||
*/
|
||||
if (isPlaying && !isPlayable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showAppNag = IS_WEB && (!isFree || RENDER_MODES.UNSUPPORTED_ON_WEB.includes(renderMode));
|
||||
|
||||
const disabled = showAppNag || (!fileInfo && insufficientCredits);
|
||||
|
||||
return (
|
||||
<div
|
||||
disabled={!hasCostInfo}
|
||||
style={!obscurePreview && supported && thumbnail && !isPlaying ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
onClick={supported ? viewFile : undefined}
|
||||
className={classnames({
|
||||
content__cover: supported,
|
||||
'content__cover--disabled': !supported,
|
||||
'content__cover--hidden-for-text': isText,
|
||||
onClick={disabled ? undefined : viewFile}
|
||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
className={classnames('content__cover', {
|
||||
'content__cover--disabled': disabled,
|
||||
'card__media--nsfw': obscurePreview,
|
||||
'card__media--disabled': supported && !fileInfo && insufficientCredits,
|
||||
})}
|
||||
>
|
||||
{!supported && (
|
||||
<Yrbl
|
||||
type="happy"
|
||||
title={getTitle()}
|
||||
subtitle={
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
download_the_app: <Button button="link" label={__('download the app')} href="https://lbry.com/get" />,
|
||||
download_this_file: (
|
||||
<Button button="link" label={__('download this file')} download={fileName} href={downloadUrl} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
Good news, though! You can %download_the_app% and gain access to everything, or %download_this_file% and
|
||||
view it on your device.
|
||||
</I18nMessage>
|
||||
}
|
||||
{showAppNag && (
|
||||
<Nag
|
||||
type="helpful"
|
||||
inline
|
||||
message={__('This content requires LBRY Desktop to display.')}
|
||||
actionText={__('Get the App')}
|
||||
href="https://lbry.com/get"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isPlaying && supported && (
|
||||
{insufficientCredits && !showAppNag && (
|
||||
<Nag
|
||||
type="helpful"
|
||||
inline
|
||||
message={__('You need more credits to purchase this.')}
|
||||
actionText={__('Open Rewards')}
|
||||
onClick={() => history.push(`/$/${PAGES.REWARDS}`)}
|
||||
/>
|
||||
)}
|
||||
{!disabled && (
|
||||
<Button
|
||||
onClick={viewFile}
|
||||
iconSize={30}
|
|
@ -1,24 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
"TextViewer" is now FileRenderInline This is used for all content types that are renderable (i.e. not downloadable) but not "playable" (go into floating player) "TextViewer" is now FileRenderInline
This is used for all content types that are renderable (i.e. not downloadable) but not "playable" (go into floating player)
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectMediaTypeForUri, makeSelectContentTypeForUri } from 'lbry-redux';
|
||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||
import { makeSelectIsPlaying } from 'redux/selectors/content';
|
||||
import {
|
||||
makeSelectFileRenderModeForUri,
|
||||
makeSelectIsPlaying,
|
||||
makeSelectStreamingUrlForUriWebProxy,
|
||||
} from 'redux/selectors/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doAnalyticsView } from 'redux/actions/app';
|
||||
import FileViewer from './view';
|
||||
import FileRenderInline from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -26,9 +24,4 @@ const perform = dispatch => ({
|
|||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
});
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer)
|
||||
);
|
||||
export default withRouter(connect(select, perform)(FileRenderInline));
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import FileRender from 'component/fileRender';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
|
||||
type Props = {
|
||||
mediaType: string,
|
||||
|
@ -10,30 +10,20 @@ type Props = {
|
|||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
isStreamable: boolean,
|
||||
renderMode: string,
|
||||
streamingUrl?: string,
|
||||
triggerAnalyticsView: (string, number) => Promise<any>,
|
||||
claimRewards: () => void,
|
||||
};
|
||||
|
||||
export default function TextViewer(props: Props) {
|
||||
const {
|
||||
isPlaying,
|
||||
fileInfo,
|
||||
uri,
|
||||
streamingUrl,
|
||||
isStreamable,
|
||||
triggerAnalyticsView,
|
||||
claimRewards,
|
||||
mediaType,
|
||||
contentType,
|
||||
} = props;
|
||||
export default function FileRenderInline(props: Props) {
|
||||
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, claimRewards } = props;
|
||||
|
||||
const [playTime, setPlayTime] = useState();
|
||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
||||
const previousUri = usePrevious(uri);
|
||||
const isNewView = uri && previousUri !== uri && isPlaying;
|
||||
const [hasRecordedView, setHasRecordedView] = useState(false);
|
||||
const isReadyToPlay = (IS_WEB && (isStreamable || streamingUrl || webStreamOnly)) || (fileInfo && fileInfo.completed);
|
||||
const isReadyToPlay = streamingUrl || (fileInfo && fileInfo.completed);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewView) {
|
||||
|
@ -52,9 +42,9 @@ export default function TextViewer(props: Props) {
|
|||
}
|
||||
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
|
||||
|
||||
return (
|
||||
<div className={classnames('content__viewersss')}>
|
||||
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
|
||||
</div>
|
||||
);
|
||||
if (!isPlaying) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={__('Preparing your content')} />;
|
||||
}
|
11
ui/component/fileTitle/index.js
Normal file
11
ui/component/fileTitle/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectTitleForUri } from 'lbry-redux';
|
||||
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
|
||||
import FileTitle from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select)(FileTitle);
|
48
ui/component/fileTitle/view.jsx
Normal file
48
ui/component/fileTitle/view.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import ClaimInsufficientCredits from 'component/claimInsufficientCredits';
|
||||
import FileSubtitle from 'component/fileSubtitle';
|
||||
import FileAuthor from 'component/fileAuthor';
|
||||
import FileActions from 'component/fileActions';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
title: string,
|
||||
nsfw: boolean,
|
||||
};
|
||||
|
||||
function FileTitle(props: Props) {
|
||||
const { title, uri, nsfw } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
isPageTitle
|
||||
title={
|
||||
<React.Fragment>
|
||||
{title}
|
||||
<FilePrice badge uri={normalizeURI(uri)} />
|
||||
{nsfw && (
|
||||
<span className="media__title-badge">
|
||||
<span className="badge badge--tag-mature">{__('Mature')}</span>
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
{/* @if TARGET='app' */}
|
||||
<ClaimInsufficientCredits uri={uri} />
|
||||
{/* @endif */}
|
||||
<FileSubtitle uri={uri} />
|
||||
<FileAuthor uri={uri} />
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={<FileActions uri={uri} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileTitle;
|
|
@ -1,9 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectViewCountForUri } from 'lbryinc';
|
||||
import { doFetchViewCount, makeSelectViewCountForUri } from 'lbryinc';
|
||||
import FileViewCount from './view';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
viewCount: makeSelectViewCountForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select)(FileViewCount);
|
||||
const perform = dispatch => ({
|
||||
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileViewCount);
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import HelpLink from 'component/common/help-link';
|
||||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
fetchViewCount: string => void,
|
||||
uri: string,
|
||||
viewCount: string,
|
||||
};
|
||||
|
||||
function FileViewCount(props: Props) {
|
||||
const { viewCount } = props;
|
||||
const { claim, uri, fetchViewCount, viewCount } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (claim && claim.claim_id) {
|
||||
fetchViewCount(claim.claim_id);
|
||||
}
|
||||
}, [fetchViewCount, uri, claim]);
|
||||
|
||||
return (
|
||||
<span>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import {
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
doPrepareEdit,
|
||||
makeSelectTitleForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import fs from 'fs';
|
||||
import FilePage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FilePage);
|
|
@ -1,84 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import FileViewerInitiator from 'component/fileViewerInitiator';
|
||||
import FileSubtitle from 'component/fileSubtitle';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import FileAuthor from 'component/fileAuthor';
|
||||
import FileActions from 'component/fileActions';
|
||||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import ClaimUri from 'component/claimUri';
|
||||
|
||||
export const FILE_WRAPPER_CLASS = 'grid-area--content';
|
||||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
costInfo: ?{ cost: number },
|
||||
balance: number,
|
||||
title: string,
|
||||
nsfw: boolean,
|
||||
};
|
||||
|
||||
function LayoutWrapperFile(props: Props) {
|
||||
const { claim, uri, claimIsMine, costInfo, balance, title, nsfw } = props;
|
||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ClaimUri uri={uri} />
|
||||
<div className={`card ${FILE_WRAPPER_CLASS}`}>
|
||||
<FileViewerInitiator uri={uri} insufficientCredits={insufficientCredits} />
|
||||
</div>
|
||||
|
||||
<div className="media__title">
|
||||
<span className="media__title-badge">
|
||||
{nsfw && <span className="badge badge--tag-mature">{__('Mature')}</span>}
|
||||
</span>
|
||||
<span className="media__title-badge">
|
||||
<FilePrice badge uri={normalizeURI(uri)} />
|
||||
</span>
|
||||
<h1 className="media__title-text">{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className="grid-area--info">
|
||||
<FileSubtitle uri={uri} />
|
||||
<FileActions uri={uri} />
|
||||
|
||||
<div className="section__divider">
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<FileAuthor uri={uri} />
|
||||
|
||||
<div className="section">
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
|
||||
<div className="section__divider">
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className="section__title--small">{__('Comments')}</div>
|
||||
<section className="section">
|
||||
<CommentCreate uri={uri} />
|
||||
</section>
|
||||
<section className="section">
|
||||
<CommentsList uri={uri} />
|
||||
</section>
|
||||
</div>
|
||||
<div className="grid-area--related">
|
||||
<RecommendedContent uri={uri} claimId={claim.claim_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutWrapperFile;
|
|
@ -1,35 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import {
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
doPrepareEdit,
|
||||
makeSelectTitleForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import fs from 'fs';
|
||||
import LayoutWrapperNonDocument from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(LayoutWrapperNonDocument);
|
|
@ -1,90 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import classNames from 'classnames';
|
||||
import FileSubtitle from 'component/fileSubtitle';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import FileAuthor from 'component/fileAuthor';
|
||||
import FileActions from 'component/fileActions';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import TextViewer from 'component/textViewer';
|
||||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import ClaimUri from 'component/claimUri';
|
||||
import FileViewerInitiator from 'component/fileViewerInitiator';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
title: string,
|
||||
nsfw: boolean,
|
||||
claim: StreamClaim,
|
||||
thumbnail: ?string,
|
||||
contentType: string,
|
||||
fileType: string,
|
||||
};
|
||||
|
||||
function LayoutWrapperText(props: Props) {
|
||||
const { uri, claim, title, nsfw, contentType, fileType } = props;
|
||||
|
||||
const markdownType = ['md', 'markdown'];
|
||||
const isMarkdown = markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classNames('main__document-wrapper', { 'main__document-wrapper--markdown': isMarkdown })}>
|
||||
<ClaimUri uri={uri} />
|
||||
|
||||
<div className="media__title">
|
||||
<span className="media__title-badge">
|
||||
{nsfw && <span className="badge badge--tag-mature">{__('Mature')}</span>}
|
||||
</span>
|
||||
<span className="media__title-badge">
|
||||
<FilePrice badge uri={normalizeURI(uri)} />
|
||||
</span>
|
||||
<h1 className="media__title-text">{title}</h1>
|
||||
</div>
|
||||
|
||||
<FileSubtitle uri={uri} />
|
||||
|
||||
<div className="section">
|
||||
<FileAuthor uri={uri} />
|
||||
</div>
|
||||
|
||||
<div className="section__divider">
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
{/* Render the initiator to trigger the view of the file */}
|
||||
<FileViewerInitiator uri={uri} />
|
||||
<TextViewer uri={uri} />
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div>
|
||||
<FileActions uri={uri} />
|
||||
|
||||
<div className="section__divider">
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<FileAuthor uri={uri} />
|
||||
|
||||
<div className="section">
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
<div className="section__title--small">{__('Comments')}</div>
|
||||
<section className="section">
|
||||
<CommentCreate uri={uri} />
|
||||
</section>
|
||||
<section className="section">
|
||||
<CommentsList uri={uri} />
|
||||
</section>
|
||||
</div>
|
||||
<RecommendedContent uri={uri} claimId={claim.claim_id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutWrapperText;
|
|
@ -7,7 +7,7 @@
|
|||
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
|
||||
File upload is carried out in the background by that function.
|
||||
*/
|
||||
import React, { useEffect, Fragment } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||
import Button from 'component/button';
|
||||
|
@ -141,7 +141,7 @@ function PublishForm(props: Props) {
|
|||
}, [name, channel, resolveUri, updatePublishForm]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="card-stack">
|
||||
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
|
||||
{!publishing && (
|
||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||
|
@ -209,7 +209,7 @@ function PublishForm(props: Props) {
|
|||
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
||||
</p>
|
||||
</section>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ type Options = {
|
|||
type Props = {
|
||||
uri: string,
|
||||
claim: ?StreamClaim,
|
||||
claimId: string,
|
||||
recommendedContent: Array<string>,
|
||||
isSearching: boolean,
|
||||
search: (string, Options) => void,
|
||||
|
@ -43,10 +42,10 @@ export default class RecommendedContent extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
getRecommendedContent() {
|
||||
const { claim, search, mature, claimId } = this.props;
|
||||
const { claim, search, mature } = this.props;
|
||||
|
||||
if (claim && claim.value && claim.value) {
|
||||
const options: Options = { size: 20, related_to: claimId, isBackgroundSearch: true };
|
||||
if (claim && claim.value && claim.claim_id) {
|
||||
const options: Options = { size: 20, related_to: claim.claim_id, isBackgroundSearch: true };
|
||||
if (claim && !mature) {
|
||||
options['nsfw'] = false;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ function AppViewer(props: Props) {
|
|||
// }, [outpoint, contentType, setAppUrl, setLoading]);
|
||||
|
||||
return (
|
||||
<div className="content__cover--disabled">
|
||||
<div className="content__cover--none">
|
||||
<Yrbl
|
||||
title={__('Sorry')}
|
||||
subtitle={__('Games and apps are currently disabled due to potential security concerns.')}
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
import React from 'react';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import Card from 'component/common/card';
|
||||
import CodeViewer from 'component/viewers/codeViewer';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import * as https from 'https';
|
||||
|
||||
type Props = {
|
||||
theme: string,
|
||||
renderMode: string,
|
||||
source: {
|
||||
file: (?string) => any,
|
||||
stream: string,
|
||||
|
@ -79,20 +82,15 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
renderDocument() {
|
||||
let viewer = null;
|
||||
const { content } = this.state;
|
||||
const { source, theme } = this.props;
|
||||
const { fileType, contentType } = source;
|
||||
const markdownType = ['md', 'markdown'];
|
||||
if (markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md') {
|
||||
// Render markdown
|
||||
viewer = <MarkdownPreview content={content} />;
|
||||
} else {
|
||||
// Render plain text
|
||||
viewer = <CodeViewer value={content} contentType={contentType} theme={theme} />;
|
||||
}
|
||||
const { source, theme, renderMode } = this.props;
|
||||
const { contentType } = source;
|
||||
|
||||
return viewer;
|
||||
return renderMode === RENDER_MODES.MARKDOWN ? (
|
||||
<Card body={<MarkdownPreview content={content} />} />
|
||||
) : (
|
||||
<CodeViewer value={content} contentType={contentType} theme={theme} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -101,7 +99,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
|||
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||
|
||||
return (
|
||||
<div className="file-render__viewer--document">
|
||||
<div className="file-render__viewer file-render__viewer--document">
|
||||
{loading && !error && <div className="placeholder--text-document" />}
|
||||
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
|
||||
{isReady && this.renderDocument()}
|
||||
|
|
|
@ -58,7 +58,7 @@ class DocxViewer extends React.PureComponent<Props, State> {
|
|||
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||
|
||||
return (
|
||||
<div className="file-render__viewer--document">
|
||||
<div className="file-render__viewer file-render__viewer--document">
|
||||
{loading && <LoadingScreen status={loadingMessage} spinner />}
|
||||
{error && <LoadingScreen status={errorMessage} spinner={false} />}
|
||||
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}
|
||||
|
|
|
@ -35,7 +35,10 @@ class HtmlViewer extends React.PureComponent<Props, State> {
|
|||
const { source } = this.props;
|
||||
const { loading } = this.state;
|
||||
return (
|
||||
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
|
||||
<div
|
||||
className="file-render__viewer file-render__viewer--html file-render__viewer--iframe"
|
||||
onContextMenu={stopContextMenu}
|
||||
>
|
||||
{loading && <div className="placeholder--text-document" />}
|
||||
{/* @if TARGET='app' */}
|
||||
<iframe ref={this.iframe} hidden={loading} sandbox="" title={__('File preview')} src={`file://${source}`} />
|
||||
|
|
|
@ -1,57 +1,21 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { stopContextMenu } from 'util/context-menu';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
// @if TARGET='app'
|
||||
import { shell } from 'electron';
|
||||
// @endif
|
||||
import IframeReact from 'component/IframeReact';
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
};
|
||||
|
||||
class PdfViewer extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
(this: any).openFile = this.openFile.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.openFile();
|
||||
}
|
||||
|
||||
openFile() {
|
||||
const { source } = this.props;
|
||||
const path = `file://${source}`;
|
||||
// @if TARGET='app'
|
||||
shell.openExternal(path);
|
||||
// @endif
|
||||
}
|
||||
|
||||
render() {
|
||||
// We used to be able to just render a webview and display the pdf inside the app
|
||||
// This was disabled on electron@3
|
||||
// https://github.com/electron/electron/issues/12337
|
||||
const { source } = this.props;
|
||||
const src = IS_WEB ? source : `file://${source}`;
|
||||
return (
|
||||
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
|
||||
{/* @if TARGET='app' */}
|
||||
<p>
|
||||
<I18nMessage
|
||||
tokens={{ click_here: <Button button="link" label={__('Click here')} onClick={this.openFile} /> }}
|
||||
>
|
||||
PDF opened externally. %click_here% to open it again.
|
||||
</I18nMessage>
|
||||
</p>
|
||||
{/* @endif */}
|
||||
|
||||
{/* @if TARGET='web' */}
|
||||
<div className="file-render__viewer">
|
||||
<iframe title={__('File preview')} src={source} />
|
||||
<div className="file-render__viewer file-render__viewer--document" onContextMenu={stopContextMenu}>
|
||||
<div className="file-render__viewer file-render__viewer--iframe">
|
||||
<IframeReact title={__('File preview')} src={src} />
|
||||
</div>
|
||||
{/* @endif */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const WalletBalance = (props: Props) => {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<section className="section__flex-wrap">
|
||||
<section className="columns">
|
||||
<div>
|
||||
<h2 className="section__title">{__('Available Balance')}</h2>
|
||||
<span className="section__title--large">
|
||||
|
|
31
ui/constants/file_render_modes.js
Normal file
31
ui/constants/file_render_modes.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
export const VIDEO = 'video';
|
||||
export const AUDIO = 'audio';
|
||||
|
||||
export const PLAYABLE_MODES = [VIDEO, AUDIO]; // these types will show in floating player
|
||||
|
||||
export const PDF = 'pdf';
|
||||
export const DOCX = 'docx';
|
||||
export const HTML = 'html';
|
||||
export const MARKDOWN = 'md';
|
||||
export const DOCUMENT = 'document';
|
||||
export const PLAIN_TEXT = 'plain_text';
|
||||
|
||||
export const TEXT_MODES = [PDF, DOCUMENT, PLAIN_TEXT, DOCX, HTML, MARKDOWN]; // these types will use text/document layout
|
||||
|
||||
export const IMAGE = 'image';
|
||||
export const CAD = 'cad';
|
||||
export const COMIC = 'comic';
|
||||
|
||||
export const AUTO_RENDER_MODES = [IMAGE].concat(TEXT_MODES); // these types will render (and thus download) automatically (if free)
|
||||
|
||||
export const DOWNLOAD = 'download';
|
||||
export const APPLICATION = 'application';
|
||||
export const UNSUPPORTED = 'unsupported';
|
||||
|
||||
// PDFs disabled on desktop until we update Electron: https://github.com/electron/electron/issues/12337
|
||||
// Comics disabled because nothing is actually reporting as a comic type
|
||||
export const UNSUPPORTED_IN_THIS_APP = IS_WEB ? [CAD, COMIC, APPLICATION] : [CAD, COMIC, APPLICATION, PDF];
|
||||
|
||||
export const UNRENDERABLE_MODES = Array.from(
|
||||
new Set(UNSUPPORTED_IN_THIS_APP.concat([DOWNLOAD, APPLICATION, UNSUPPORTED]))
|
||||
);
|
|
@ -3,17 +3,15 @@ import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
|||
import { doSetContentHistoryItem } from 'redux/actions/content';
|
||||
import {
|
||||
doFetchFileInfo,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectChannelForClaimUri,
|
||||
selectBalance,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { makeSelectIsText } from 'redux/selectors/content';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import FilePage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -22,11 +20,9 @@ const select = (state, props) => ({
|
|||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowMatureContent(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||
balance: selectBalance(state),
|
||||
isText: makeSelectIsText(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -34,10 +30,6 @@ const perform = dispatch => ({
|
|||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FilePage);
|
||||
export default connect(select, perform)(FilePage);
|
||||
|
|
|
@ -1,41 +1,44 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Page from 'component/page';
|
||||
import I18nMessage from 'component/i18nMessage/view';
|
||||
import LayoutWrapperFile from 'component/layoutWrapperFile';
|
||||
import LayoutWrapperText from 'component/layoutWrapperText';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import ClaimUri from 'component/claimUri';
|
||||
import FileTitle from 'component/fileTitle';
|
||||
import FileRenderInitiator from 'component/fileRenderInitiator';
|
||||
import FileRenderInline from 'component/fileRenderInline';
|
||||
import FileRenderDownload from 'component/fileRenderDownload';
|
||||
import Card from 'component/common/card';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
|
||||
export const FILE_WRAPPER_CLASS = 'grid-area--content';
|
||||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
costInfo: ?{ cost: number },
|
||||
fetchFileInfo: string => void,
|
||||
fetchCostInfo: string => void,
|
||||
setViewed: string => void,
|
||||
isSubscribed: ?string,
|
||||
isSubscribed: boolean,
|
||||
channelUri: string,
|
||||
viewCount: number,
|
||||
renderMode: string,
|
||||
markSubscriptionRead: (string, string) => void,
|
||||
fetchViewCount: string => void,
|
||||
balance: number,
|
||||
isText: boolean,
|
||||
};
|
||||
|
||||
class FilePage extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { uri, claim, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed, fetchViewCount } = this.props;
|
||||
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
|
||||
|
||||
if (isSubscribed) {
|
||||
this.removeFromSubscriptionNotifications();
|
||||
}
|
||||
|
||||
fetchViewCount(claim.claim_id);
|
||||
|
||||
// always refresh file info when entering file page to see if we have the file
|
||||
// this could probably be refactored into more direct components now
|
||||
// @if TARGET='app'
|
||||
fetchFileInfo(uri);
|
||||
// @endif
|
||||
|
@ -46,16 +49,12 @@ class FilePage extends React.Component<Props> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { isSubscribed, claim, uri, fileInfo, setViewed, fetchViewCount, fetchFileInfo } = this.props;
|
||||
const { isSubscribed, uri, fileInfo, setViewed, fetchFileInfo } = this.props;
|
||||
|
||||
if (!prevProps.isSubscribed && isSubscribed) {
|
||||
this.removeFromSubscriptionNotifications();
|
||||
}
|
||||
|
||||
if (prevProps.uri !== uri) {
|
||||
fetchViewCount(claim.claim_id);
|
||||
}
|
||||
|
||||
if (prevProps.uri !== uri) {
|
||||
setViewed(uri);
|
||||
}
|
||||
|
@ -74,26 +73,67 @@ class FilePage extends React.Component<Props> {
|
|||
markSubscriptionRead(channelUri, uri);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { uri, claimIsMine, costInfo, fileInfo, balance, isText } = this.props;
|
||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
||||
renderFilePageLayout(uri, mode, cost) {
|
||||
if (RENDER_MODES.PLAYABLE_MODES.includes(mode)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ClaimUri uri={uri} />
|
||||
<div className={FILE_WRAPPER_CLASS}>
|
||||
<FileRenderInitiator uri={uri} />
|
||||
</div>
|
||||
{/* playables will be rendered and injected by <FileRenderFloating> */}
|
||||
<FileTitle uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (RENDER_MODES.UNRENDERABLE_MODES.includes(mode)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ClaimUri uri={uri} />
|
||||
<FileTitle uri={uri} />
|
||||
<FileRenderDownload uri={uri} isFree={cost === 0} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (RENDER_MODES.TEXT_MODES.includes(mode)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ClaimUri uri={uri} />
|
||||
<FileTitle uri={uri} />
|
||||
<FileRenderInitiator uri={uri} />
|
||||
<FileRenderInline uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page className="main--file-page">
|
||||
{!fileInfo && insufficientCredits && (
|
||||
<div className="media__insufficient-credits help--warning">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
|
||||
}}
|
||||
>
|
||||
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.
|
||||
Check out %reward_link% for free LBC or send more LBC to your wallet.
|
||||
</I18nMessage>
|
||||
</div>
|
||||
)}
|
||||
<React.Fragment>
|
||||
<ClaimUri uri={uri} />
|
||||
<FileRenderInitiator uri={uri} />
|
||||
<FileRenderInline uri={uri} />
|
||||
<FileTitle uri={uri} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
{isText ? <LayoutWrapperText uri={uri} /> : <LayoutWrapperFile uri={uri} />}
|
||||
render() {
|
||||
const { uri, renderMode, costInfo } = this.props;
|
||||
|
||||
return (
|
||||
<Page className="file-page">
|
||||
<div className="section card-stack">
|
||||
{this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)}
|
||||
</div>
|
||||
<div className="section columns">
|
||||
<div className="card-stack">
|
||||
<FileDetails uri={uri} />
|
||||
<Card title={__('Leave a Comment')} actions={<CommentCreate uri={uri} />} />
|
||||
<Card title={__('Comments')} body={<CommentsList uri={uri} />} />
|
||||
</div>
|
||||
<RecommendedContent uri={uri} />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ class HelpPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Page className="card-stack">
|
||||
<Card
|
||||
title={__('Read the FAQ')}
|
||||
subtitle={__('Our FAQ answers many common questions.')}
|
||||
|
@ -202,20 +202,18 @@ class HelpPage extends React.PureComponent<Props, State> {
|
|||
<WalletBackup />
|
||||
{/* @endif */}
|
||||
|
||||
<section className="card">
|
||||
<header className="table__header">
|
||||
<div className="table__header-text">
|
||||
<h2 className="section__title">{__('About')}</h2>
|
||||
|
||||
{this.state.upgradeAvailable !== null && this.state.upgradeAvailable && (
|
||||
<p className="section__subtitle">
|
||||
{__('A newer version of LBRY is available.')}{' '}
|
||||
<Card
|
||||
title={__('About')}
|
||||
subtitle={
|
||||
this.state.upgradeAvailable !== null && this.state.upgradeAvailable ? (
|
||||
<span>
|
||||
{__('A newer version of LBRY is available.')}
|
||||
<Button button="link" href={newVerLink} label={__('Download now!')} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
isBodyTable
|
||||
body={
|
||||
<div className="table__wrapper">
|
||||
<table className="table table--stretch">
|
||||
<tbody>
|
||||
|
@ -278,7 +276,8 @@ class HelpPage extends React.PureComponent<Props, State> {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -258,7 +258,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
const endHours = ['5', '6', '7', '8'];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Page className="card-stack">
|
||||
{!IS_WEB && noDaemonSettings ? (
|
||||
<section className="card card--section">
|
||||
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
|
||||
|
|
|
@ -5,76 +5,64 @@ import {
|
|||
selectClaimsByUri,
|
||||
makeSelectClaimsInChannelForCurrentPageState,
|
||||
makeSelectClaimIsNsfw,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectRecommendedContentForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
selectBalance,
|
||||
selectBlockedChannels,
|
||||
parseURI,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
makeSelectFileNameForUri,
|
||||
} from 'lbry-redux';
|
||||
import { selectAllCostInfoByUri } from 'lbryinc';
|
||||
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import path from 'path';
|
||||
import React from 'react';
|
||||
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
||||
// @if TARGET='web'
|
||||
import { generateStreamUrl } from 'util/lbrytv';
|
||||
// @endif
|
||||
|
||||
const RECENT_HISTORY_AMOUNT = 10;
|
||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
||||
|
||||
export const selectState = (state: any) => state.content || {};
|
||||
|
||||
export const selectPlayingUri = createSelector(
|
||||
selectState,
|
||||
state => state.playingUri
|
||||
);
|
||||
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||
|
||||
export const makeSelectIsPlaying = (uri: string) =>
|
||||
createSelector(
|
||||
selectPlayingUri,
|
||||
playingUri => playingUri === uri
|
||||
);
|
||||
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
|
||||
|
||||
export const makeSelectContentPositionForUri = (uri: string) =>
|
||||
createSelector(
|
||||
selectState,
|
||||
makeSelectClaimForUri(uri),
|
||||
(state, claim) => {
|
||||
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
|
||||
if (!claim) {
|
||||
return null;
|
||||
}
|
||||
const outpoint = `${claim.txid}:${claim.nout}`;
|
||||
const id = claim.claim_id;
|
||||
return state.positions[id] ? state.positions[id][outpoint] : null;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const selectHistory = createSelector(
|
||||
selectState,
|
||||
state => state.history || []
|
||||
);
|
||||
export const selectHistory = createSelector(selectState, state => state.history || []);
|
||||
|
||||
export const selectHistoryPageCount = createSelector(
|
||||
selectHistory,
|
||||
history => Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
|
||||
export const selectHistoryPageCount = createSelector(selectHistory, history =>
|
||||
Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
|
||||
);
|
||||
|
||||
export const makeSelectHistoryForPage = (page: number) =>
|
||||
createSelector(
|
||||
selectHistory,
|
||||
selectClaimsByUri,
|
||||
(history, claimsByUri) => {
|
||||
createSelector(selectHistory, selectClaimsByUri, (history, claimsByUri) => {
|
||||
const left = page * HISTORY_ITEMS_PER_PAGE;
|
||||
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
|
||||
return historyItemsForPage;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const makeSelectHistoryForUri = (uri: string) =>
|
||||
createSelector(
|
||||
selectHistory,
|
||||
history => history.find(i => i.uri === uri)
|
||||
);
|
||||
createSelector(selectHistory, history => history.find(i => i.uri === uri));
|
||||
|
||||
export const makeSelectHasVisitedUri = (uri: string) =>
|
||||
createSelector(
|
||||
makeSelectHistoryForUri(uri),
|
||||
history => Boolean(history)
|
||||
);
|
||||
createSelector(makeSelectHistoryForUri(uri), history => Boolean(history));
|
||||
|
||||
export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
||||
createSelector(
|
||||
|
@ -132,17 +120,12 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
|||
}
|
||||
);
|
||||
|
||||
export const selectRecentHistory = createSelector(
|
||||
selectHistory,
|
||||
history => {
|
||||
export const selectRecentHistory = createSelector(selectHistory, history => {
|
||||
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
|
||||
createSelector(
|
||||
makeSelectClaimsInChannelForCurrentPageState(channel),
|
||||
channelClaims => {
|
||||
createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), channelClaims => {
|
||||
if (uris) return uris;
|
||||
|
||||
if (channelClaims) {
|
||||
|
@ -151,32 +134,90 @@ export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const makeSelectShouldObscurePreview = (uri: string) =>
|
||||
createSelector(
|
||||
selectShowMatureContent,
|
||||
makeSelectClaimIsNsfw(uri),
|
||||
(showMatureContent, isClaimMature) => {
|
||||
createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => {
|
||||
return isClaimMature && !showMatureContent;
|
||||
});
|
||||
|
||||
// should probably be in lbry-redux, yarn link was fighting me
|
||||
export const makeSelectFileExtensionForUri = (uri: string) =>
|
||||
createSelector(makeSelectFileNameForUri(uri), fileName => {
|
||||
return fileName && path.extname(fileName).substring(1);
|
||||
});
|
||||
|
||||
// @if TARGET='web'
|
||||
export const makeSelectStreamingUrlForUriWebProxy = (uri: string) =>
|
||||
createSelector(makeSelectClaimForUri(uri), claim => (claim ? generateStreamUrl(claim.name, claim.claim_id) : null));
|
||||
// @endif
|
||||
// @if TARGET='app'
|
||||
export const makeSelectStreamingUrlForUriWebProxy = (uri: string) =>
|
||||
createSelector(makeSelectStreamingUrlForUri, url => url);
|
||||
// @endif
|
||||
|
||||
export const makeSelectFileRenderModeForUri = (uri: string) =>
|
||||
createSelector(
|
||||
makeSelectContentTypeForUri(uri),
|
||||
makeSelectMediaTypeForUri(uri),
|
||||
makeSelectFileExtensionForUri(uri),
|
||||
(contentType, mediaType, extension) => {
|
||||
if (mediaType === 'video' || FORCE_CONTENT_TYPE_PLAYER.includes(contentType)) {
|
||||
return RENDER_MODES.VIDEO;
|
||||
}
|
||||
if (mediaType === 'image') {
|
||||
return RENDER_MODES.IMAGE;
|
||||
}
|
||||
if (['md', 'markdown'].includes(extension) || ['text/md', 'text/markdown'].includes(contentType)) {
|
||||
return RENDER_MODES.MARKDOWN;
|
||||
}
|
||||
if (contentType === 'application/pdf') {
|
||||
return RENDER_MODES.PDF;
|
||||
}
|
||||
if (['text/htm', 'text/html'].includes(contentType)) {
|
||||
return RENDER_MODES.HTML;
|
||||
}
|
||||
if (['text', 'document', 'script'].includes(mediaType)) {
|
||||
return RENDER_MODES.DOCUMENT;
|
||||
}
|
||||
if (extension === 'docx') {
|
||||
return RENDER_MODES.DOCX;
|
||||
}
|
||||
|
||||
// when writing this my local copy of Lbry.getMediaType had '3D-file', but I was receiving model...'
|
||||
if (['3D-file', 'model'].includes(mediaType)) {
|
||||
return RENDER_MODES.CAD;
|
||||
}
|
||||
if (mediaType === 'comic-book') {
|
||||
return RENDER_MODES.COMIC;
|
||||
}
|
||||
if (
|
||||
[
|
||||
'application/zip',
|
||||
'application/x-gzip',
|
||||
'application/x-gtar',
|
||||
'application/x-tgz',
|
||||
'application/vnd.rar',
|
||||
'application/x-7z-compressed',
|
||||
].includes(contentType)
|
||||
) {
|
||||
return RENDER_MODES.DOWNLOAD;
|
||||
}
|
||||
|
||||
if (mediaType === 'application') {
|
||||
return RENDER_MODES.APPLICATION;
|
||||
}
|
||||
|
||||
return RENDER_MODES.UNSUPPORTED;
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectCanAutoplay = (uri: string) =>
|
||||
export const makeSelectInsufficientCreditsForUri = (uri: string) =>
|
||||
createSelector(
|
||||
makeSelectMediaTypeForUri(uri),
|
||||
mediaType => {
|
||||
const canAutoPlay = ['audio', 'video', 'image', 'text', 'document'].includes(mediaType);
|
||||
return canAutoPlay;
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectIsText = (uri: string) =>
|
||||
createSelector(
|
||||
makeSelectMediaTypeForUri(uri),
|
||||
mediaType => {
|
||||
const isText = ['text', 'document', 'script'].includes(mediaType);
|
||||
return isText;
|
||||
makeSelectClaimIsMine(uri),
|
||||
makeSelectCostInfoForUri(uri),
|
||||
selectBalance,
|
||||
(isMine, costInfo, balance) => {
|
||||
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.card {
|
||||
background-color: var(--color-card-background);
|
||||
margin-bottom: var(--spacing-large);
|
||||
position: relative;
|
||||
border-radius: var(--card-radius);
|
||||
overflow: hidden;
|
||||
|
@ -79,6 +78,12 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-stack {
|
||||
.card:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
|
||||
.card__list {
|
||||
column-count: 2;
|
||||
column-gap: var(--spacing-large);
|
||||
|
@ -86,7 +91,7 @@
|
|||
|
||||
.card {
|
||||
display: inline-block;
|
||||
margin: 0 0 var(--spacing-large);
|
||||
margin-bottom: var(--spacing-large);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
|
@ -139,23 +144,49 @@
|
|||
background-color: black;
|
||||
}
|
||||
|
||||
.card__media--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card__header {
|
||||
margin: var(--spacing-medium) var(--spacing-large);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.section__subtitle {
|
||||
margin-bottom: 0;
|
||||
> * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon__wrapper {
|
||||
margin-right: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
|
||||
.card__title {
|
||||
font-size: var(--font-title);
|
||||
font-weight: var(--font-weight-light);
|
||||
display: block;
|
||||
|
||||
/* .badge rule inherited from file page prices, should be refactored */
|
||||
.badge {
|
||||
float: right;
|
||||
margin-left: var(--spacing-small);
|
||||
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtitle {
|
||||
color: var(--color-text-subtitle);
|
||||
margin: var(--spacing-small) 0;
|
||||
font-size: var(--font-body);
|
||||
}
|
||||
|
||||
.card__body {
|
||||
padding: var(--spacing-large);
|
||||
&:not(.card__body--no-title) {
|
||||
padding-top: 0;
|
||||
}
|
||||
&.card__body--table {
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.card__main-actions {
|
||||
padding: var(--spacing-large);
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
.comment {
|
||||
padding: var(--spacing-small) 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: var(--font-body);
|
||||
padding: var(--spacing-medium) 0;
|
||||
margin: 0;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--spacing-medium) 0;
|
||||
}
|
||||
&:last-of-type {
|
||||
padding-top: var(--spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,8 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--card-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
justify-content: center;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -110,7 +112,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content__cover--disabled {
|
||||
.content__cover--none {
|
||||
@include thumbnail;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
|
@ -128,8 +130,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content__cover--hidden-for-text {
|
||||
display: none;
|
||||
.content__cover--disabled {
|
||||
pointer-events: none;
|
||||
.nag {
|
||||
/* boo fire Jeremy */
|
||||
pointer-events: auto;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.content__loading {
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
.file-page {
|
||||
.grid-area--content + .card,
|
||||
.file-render + .card,
|
||||
.content__cover + .card,
|
||||
.card + .file-render,
|
||||
.card + .grid-area--content,
|
||||
.card + .content__cover {
|
||||
margin-top: var(--spacing-large);
|
||||
}
|
||||
|
||||
.card + .file-render {
|
||||
margin-top: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
|
||||
.file-render {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
max-height: var(--inline-player-max-height);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
// margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-render--document {
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
|
||||
.content__loading {
|
||||
background-color: transparent;
|
||||
|
@ -22,6 +34,21 @@
|
|||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
width: 40em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: unset;
|
||||
min-width: unset;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 100%;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__viewer {
|
||||
|
@ -45,27 +72,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-render__viewer--document {
|
||||
@extend .file-render__viewer;
|
||||
overflow: auto;
|
||||
|
||||
.markdown-preview {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-small);
|
||||
.file-render__viewer--iframe {
|
||||
display: flex; /*this eliminates extra height from whitespace, if someone edits this with a better technique, tell Jeremy*/
|
||||
/*
|
||||
ideally iframes would dynamiclly grow, see <IframeReact> for a start at this
|
||||
for now, since we don't know size, let's make as large as we can without being larger than available area
|
||||
*/
|
||||
iframe {
|
||||
height: calc(100vh - var(--header-height) - var(--spacing-medium) * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__viewer--pdf {
|
||||
@extend .file-render__viewer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.file-render__content {
|
||||
width: 100%;
|
||||
|
@ -138,6 +154,8 @@
|
|||
}
|
||||
|
||||
.file-render {
|
||||
border-radius: var(--card-radius);
|
||||
|
||||
.video-js {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -33,33 +33,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.main--file-page {
|
||||
position: relative;
|
||||
|
||||
.grid-area--content {
|
||||
max-height: var(--inline-player-max-height);
|
||||
}
|
||||
|
||||
.grid-area--info {
|
||||
margin-right: var(--spacing-large);
|
||||
width: 52.5%;
|
||||
}
|
||||
|
||||
.grid-area--related {
|
||||
width: calc(47.5% - var(--spacing-large));
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
overflow-x: hidden;
|
||||
|
||||
.grid-area--related,
|
||||
.grid-area--info {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main--auth-page {
|
||||
max-width: 60rem;
|
||||
margin-top: var(--spacing-main-padding);
|
||||
|
@ -75,6 +48,9 @@
|
|||
margin-top: 100px;
|
||||
margin-bottom: 100px;
|
||||
text-align: center;
|
||||
> .card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.main--launching {
|
||||
|
@ -100,26 +76,3 @@
|
|||
.main--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main__document-wrapper {
|
||||
max-width: 100%;
|
||||
min-width: 40em;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-bottom: var(--spacing-xlarge);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.main__document-wrapper--markdown {
|
||||
@extend .main__document-wrapper;
|
||||
width: 40em;
|
||||
max-width: unset;
|
||||
min-width: unset;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
.markdown-preview {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -15,25 +15,6 @@
|
|||
// M E D I A
|
||||
// T I T L E
|
||||
|
||||
.media__title {
|
||||
margin-bottom: var(--spacing-medium);
|
||||
}
|
||||
|
||||
.media__title-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: var(--font-weight-bold);
|
||||
white-space: normal;
|
||||
font-size: var(--font-title);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.media__title-badge {
|
||||
float: right;
|
||||
margin-left: var(--spacing-small);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.media__uri {
|
||||
position: absolute;
|
||||
transform: translateY(-130%);
|
||||
|
@ -65,14 +46,6 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
.media__uri--large {
|
||||
margin-bottom: var(--spacing-medium);
|
||||
}
|
||||
|
||||
.media__insufficient-credits {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
// M E D I A
|
||||
// S U B T I T L E
|
||||
|
||||
|
@ -90,19 +63,7 @@
|
|||
@extend .media__subtitle;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.media__subtitle--large {
|
||||
display: block;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.media__subtitle__channel {
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: var(--spacing-small) 0;
|
||||
margin-bottom: var(--spacing-small);
|
||||
}
|
||||
|
||||
.media__info-text {
|
||||
|
@ -118,13 +79,8 @@
|
|||
}
|
||||
|
||||
.media__actions {
|
||||
@extend .section__actions;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.media__document-thumbnail {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
margin-bottom: var(--spacing-xlarge);
|
||||
}
|
||||
|
||||
.yrbl {
|
||||
|
|
|
@ -13,7 +13,6 @@ $nag-error-z-index: 100001;
|
|||
color: var(--color-white);
|
||||
font-weight: var(--font-weight-bold);
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
|
||||
.button--link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
@ -26,6 +25,13 @@ $nag-error-z-index: 100001;
|
|||
}
|
||||
}
|
||||
|
||||
.nag--inline {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
z-index: 1 !important; /* booooooo */
|
||||
}
|
||||
|
||||
.nag--helpful {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-white);
|
||||
|
|
|
@ -22,21 +22,11 @@
|
|||
.section__flex {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
& > :first-child {
|
||||
> .icon__wrapper:first-child {
|
||||
margin-right: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
|
||||
.section__flex-wrap {
|
||||
@extend .section__flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-large);
|
||||
& > * {
|
||||
margin-bottom: var(--spacing-large);
|
||||
}
|
||||
}
|
||||
|
||||
.section__title {
|
||||
text-align: left;
|
||||
font-size: var(--font-title);
|
||||
|
@ -101,6 +91,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section__actions--no-margin {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.section__actions {
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -146,6 +146,7 @@ img {
|
|||
|
||||
& > * {
|
||||
margin: 0;
|
||||
margin-bottom: var(--spacing-medium);
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
|
||||
|
@ -216,12 +217,16 @@ img {
|
|||
.help--inline {
|
||||
@extend .help;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-text-empty);
|
||||
font-style: italic;
|
||||
}
|
||||
.empty--centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 134px;
|
||||
|
|
|
@ -19,9 +19,6 @@ $breakpoint-medium: 1150px;
|
|||
--spacing-large: 2rem;
|
||||
--spacing-xlarge: 3rem;
|
||||
--spacing-main-padding: var(--spacing-xlarge);
|
||||
--file-page-max-width: 1787px;
|
||||
--file-max-height: 788px;
|
||||
--file-max-width: 1400px;
|
||||
--floating-viewer-width: 32rem;
|
||||
--floating-viewer-height: 18rem; // 32 * 9/16
|
||||
--floating-viewer-info-height: 5rem;
|
||||
|
|
Loading…
Reference in a new issue
This doesn't do anything. If iframes are serving files from the same domain, growable iframes would work. Ironically we just changed this.