File downloads and refactoring #3918
|
@ -11,6 +11,8 @@ type Props = {
|
||||||
export default function NagDegradedPerformance(props: Props) {
|
export default function NagDegradedPerformance(props: Props) {
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nag
|
<Nag
|
||||||
type="error"
|
type="error"
|
||||||
|
|
|
@ -84,6 +84,8 @@ function OpenInAppLink(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nag
|
<Nag
|
||||||
type="helpful"
|
type="helpful"
|
||||||
|
|
|
@ -335,7 +335,6 @@
|
||||||
"credits": "credits",
|
"credits": "credits",
|
||||||
"No channel name after @.": "No channel name after @.",
|
"No channel name after @.": "No channel name after @.",
|
||||||
"View channel": "View channel",
|
"View channel": "View channel",
|
||||||
"Add to your library": "Add to your library",
|
|
||||||
"Web link": "Web link",
|
"Web link": "Web link",
|
||||||
"Facebook": "Facebook",
|
"Facebook": "Facebook",
|
||||||
"Twitter": "Twitter",
|
"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.",
|
"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.",
|
"Loading 3D model.": "Loading 3D model.",
|
||||||
"Click here": "Click here",
|
"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",
|
"Wallet Server": "Wallet Server",
|
||||||
"lbry.tv wallet servers": "lbry.tv wallet servers",
|
"lbry.tv wallet servers": "lbry.tv wallet servers",
|
||||||
"Custom wallet servers": "Custom wallet servers",
|
"Custom wallet servers": "Custom wallet servers",
|
||||||
|
|
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
|
@ -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 { openContextMenu } from 'util/context-menu';
|
||||||
import useKonamiListener from 'util/enhanced-layout';
|
import useKonamiListener from 'util/enhanced-layout';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
import FloatingViewer from 'component/floatingViewer';
|
import FileRenderFloating from 'component/fileRenderFloating';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import usePrevious from 'effects/use-previous';
|
import usePrevious from 'effects/use-previous';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
|
@ -286,7 +286,7 @@ function App(props: Props) {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Router />
|
<Router />
|
||||||
<ModalRouter />
|
<ModalRouter />
|
||||||
<FloatingViewer pageUri={uri} />
|
<FileRenderFloating pageUri={uri} />
|
||||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||||
|
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
|
|
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
|
@ -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 ClaimPreview from 'component/claimPreview';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
|
import Card from 'component/common/card';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
const SORT_NEW = 'new';
|
const SORT_NEW = 'new';
|
||||||
|
@ -161,9 +162,9 @@ export default function ClaimList(props: Props) {
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{!timedOut && urisLength === 0 && !loading && (
|
{!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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,10 @@ import {
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
selectChannelIsBlocked,
|
selectChannelIsBlocked,
|
||||||
doFileGet,
|
doFileGet,
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
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 { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ const select = (state, props) => ({
|
||||||
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
||||||
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
||||||
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(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 => ({
|
const perform = dispatch => ({
|
||||||
|
@ -42,7 +41,4 @@ const perform = dispatch => ({
|
||||||
getFile: uri => dispatch(doFileGet(uri, false)),
|
getFile: uri => dispatch(doFileGet(uri, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(ClaimPreview);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(ClaimPreview);
|
|
||||||
|
|
|
@ -11,26 +11,47 @@ type Props = {
|
||||||
actions?: string | Node,
|
actions?: string | Node,
|
||||||
icon?: string,
|
icon?: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
isPageTitle?: boolean,
|
||||||
|
isBodyTable?: boolean,
|
||||||
actionIconPadding?: boolean,
|
actionIconPadding?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Card(props: Props) {
|
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 (
|
return (
|
||||||
<section className={classnames(className, 'card')}>
|
<section className={classnames(className, 'card')}>
|
||||||
{(title || subtitle) && (
|
{(title || subtitle) && (
|
||||||
<div className="card__header">
|
<div className="card__header">
|
||||||
<div className="section__flex">
|
{icon && <Icon sectionIcon icon={icon} />}
|
||||||
{icon && <Icon sectionIcon icon={icon} />}
|
<div>
|
||||||
<div>
|
{isPageTitle && <h1 className="card__title">{title}</h1>}
|
||||||
<h2 className="section__title">{title}</h2>
|
{!isPageTitle && <h2 className="card__title">{title}</h2>}
|
||||||
{subtitle && <div className="section__subtitle">{subtitle}</div>}
|
{subtitle && <div className="card__subtitle">{subtitle}</div>}
|
||||||
</div>
|
|
||||||
</div>
|
</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 && (
|
{actions && (
|
||||||
<div
|
<div
|
||||||
add support cards without titles add support cards without titles
|
|||||||
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}
|
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}
|
||||||
|
|
|
@ -10,17 +10,24 @@ type Props = {
|
||||||
actionText: string,
|
actionText: string,
|
||||||
href?: string,
|
href?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
inline?: boolean,
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Nag(props: Props) {
|
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 };
|
const buttonProps = onClick ? { onClick } : { href };
|
||||||
|
|
||||||
return (
|
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>
|
<div className="nag__message">{message}</div>
|
||||||
<Button
|
<Button
|
||||||
className={classnames('nag__button', {
|
className={classnames('nag__button', {
|
||||||
|
|
|
@ -73,7 +73,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="main main--empty">
|
<div className="main main--full-width main--empty">
|
||||||
<Yrbl
|
<Yrbl
|
||||||
type="sad"
|
type="sad"
|
||||||
title={__('Aw shucks!')}
|
title={__('Aw shucks!')}
|
||||||
|
|
|
@ -60,7 +60,7 @@ function FileActions(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="media__actions">
|
<div className="media__actions">
|
||||||
<div className="section__actions">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
icon={ICONS.SHARE}
|
icon={ICONS.SHARE}
|
||||||
|
@ -97,7 +97,7 @@ function FileActions(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section__actions">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<FileDownloadLink uri={uri} />
|
<FileDownloadLink uri={uri} />
|
||||||
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Button from 'component/button';
|
||||||
import Expandable from 'component/expandable';
|
import Expandable from 'component/expandable';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import ClaimTags from 'component/claimTags';
|
import ClaimTags from 'component/claimTags';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -42,73 +43,78 @@ class FileDetails extends PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Expandable>
|
<Card
|
||||||
{description && (
|
title={__('Details')}
|
||||||
<div className="media__info-text">
|
body={
|
||||||
<MarkdownPreview content={description} />
|
<Expandable>
|
||||||
</div>
|
{description && (
|
||||||
)}
|
<div className="media__info-text">
|
||||||
<ClaimTags uri={uri} type="large" />
|
<MarkdownPreview content={description} />
|
||||||
<table className="table table--condensed table--fixed table--file-details">
|
</div>
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td> {__('Content Type')}</td>
|
|
||||||
<td>{mediaType}</td>
|
|
||||||
</tr>
|
|
||||||
{fileSize && (
|
|
||||||
<tr>
|
|
||||||
<td> {__('File Size')}</td>
|
|
||||||
<td>{fileSize}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
<tr>
|
<ClaimTags uri={uri} type="large" />
|
||||||
<td> {__('Bid Amount')}</td>
|
<table className="table table--condensed table--fixed table--file-details">
|
||||||
<td>{claim.amount} LBC</td>
|
<tbody>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td> {__('Content Type')}</td>
|
||||||
<td> {__('Effective Amount')}</td>
|
<td>{mediaType}</td>
|
||||||
<td>{claim.meta.effective_amount} LBC</td>
|
</tr>
|
||||||
</tr>
|
{fileSize && (
|
||||||
<tr>
|
<tr>
|
||||||
<td> {__('Is Controlling')}</td>
|
<td> {__('File Size')}</td>
|
||||||
<td>{claim.meta.is_controlling ? __('Yes') : __('No')}</td>
|
<td>{fileSize}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
)}
|
||||||
<td> {__('Claim ID')}</td>
|
<tr>
|
||||||
<td>{claim.claim_id}</td>
|
<td> {__('Bid Amount')}</td>
|
||||||
</tr>
|
<td>{claim.amount} LBC</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> {__('Effective Amount')}</td>
|
||||||
|
<td>{claim.meta.effective_amount} LBC</td>
|
||||||
|
</tr>
|
||||||
Expandable should become a card option. As should @jessopb's newest collapsible elements (they're collapsible cards). Expandable should become a card option. As should @jessopb's newest collapsible elements (they're collapsible cards).
|
|||||||
|
<tr>
|
||||||
|
<td> {__('Is Controlling')}</td>
|
||||||
|
<td>{claim.meta.is_controlling ? __('Yes') : __('No')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> {__('Claim ID')}</td>
|
||||||
|
<td>{claim.claim_id}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
{languages && (
|
{languages && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{__('Languages')}</td>
|
<td>{__('Languages')}</td>
|
||||||
<td>{languages.join(' ')}</td>
|
<td>{languages.join(' ')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{__('License')}</td>
|
<td>{__('License')}</td>
|
||||||
<td>{license}</td>
|
<td>{license}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{downloadPath && (
|
{downloadPath && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{__('Downloaded to')}</td>
|
<td>{__('Downloaded to')}</td>
|
||||||
<td>
|
<td>
|
||||||
{/* {downloadPath.replace(/(.{10})/g, '$1\u200b')} */}
|
{/* {downloadPath.replace(/(.{10})/g, '$1\u200b')} */}
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
className="button--download-link"
|
className="button--download-link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (downloadPath) {
|
if (downloadPath) {
|
||||||
openFolder(downloadPath);
|
openFolder(downloadPath);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
label={downloadNote || downloadPath.replace(/(.{10})/g, '$1\u200b')}
|
label={downloadNote || downloadPath.replace(/(.{10})/g, '$1\u200b')}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Expandable>
|
</Expandable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,14 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
downloading: boolean,
|
downloading: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
isStreamable: boolean,
|
|
||||||
fileInfo: ?FileListItem,
|
fileInfo: ?FileListItem,
|
||||||
openModal: (id: string, { path: string }) => void,
|
openModal: (id: string, { path: string }) => void,
|
||||||
pause: () => void,
|
pause: () => void,
|
||||||
download: string => void,
|
download: string => void,
|
||||||
triggerViewEvent: string => void,
|
triggerViewEvent: string => void,
|
||||||
costInfo: ?{ cost: string },
|
costInfo: ?{ cost: string },
|
||||||
|
buttonType: ?string,
|
||||||
|
showLabel: ?boolean,
|
||||||
hideOpenButton: boolean,
|
hideOpenButton: boolean,
|
||||||
hideDownloadStatus: boolean,
|
hideDownloadStatus: boolean,
|
||||||
};
|
};
|
||||||
|
@ -35,6 +36,8 @@ function FileDownloadLink(props: Props) {
|
||||||
claim,
|
claim,
|
||||||
triggerViewEvent,
|
triggerViewEvent,
|
||||||
costInfo,
|
costInfo,
|
||||||
|
buttonType = 'alt',
|
||||||
|
showLabel = false,
|
||||||
hideOpenButton = false,
|
hideOpenButton = false,
|
||||||
hideDownloadStatus = false,
|
hideDownloadStatus = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -73,10 +76,12 @@ function FileDownloadLink(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
||||||
|
const openLabel = __('Open file');
|
||||||
return hideOpenButton ? null : (
|
return hideOpenButton ? null : (
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button={buttonType}
|
||||||
title={__('Open file')}
|
title={openLabel}
|
||||||
|
label={showLabel ? openLabel : null}
|
||||||
icon={ICONS.EXTERNAL}
|
icon={ICONS.EXTERNAL}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pause();
|
pause();
|
||||||
|
@ -86,11 +91,14 @@ function FileDownloadLink(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const label = IS_WEB ? __('Download') : __('Download to your Library');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button={buttonType}
|
||||||
title={IS_WEB ? __('Download') : __('Add to your library')}
|
title={label}
|
||||||
icon={ICONS.DOWNLOAD}
|
icon={ICONS.DOWNLOAD}
|
||||||
|
label={showLabel ? label : null}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
download={fileName}
|
download={fileName}
|
||||||
|
|
|
@ -3,14 +3,16 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
makeSelectDownloadPathForUri,
|
makeSelectDownloadPathForUri,
|
||||||
makeSelectFileNameForUri,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/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 { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import FileRender from './view';
|
import FileRender from './view';
|
||||||
|
|
||||||
|
@ -23,10 +25,10 @@ const select = (state, props) => {
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
|
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
|
||||||
fileName: makeSelectFileNameForUri(props.uri)(state),
|
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||||
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
isText: makeSelectIsText(props.uri)(state),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,7 +36,4 @@ const perform = dispatch => ({
|
||||||
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
|
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(FileRender);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileRender);
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { URL } from 'config';
|
import { URL } from 'config';
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
import React, { Suspense, Fragment } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import classnames from 'classnames';
|
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 VideoViewer from 'component/viewers/videoViewer';
|
||||||
import ImageViewer from 'component/viewers/imageViewer';
|
import ImageViewer from 'component/viewers/imageViewer';
|
||||||
import AppViewer from 'component/viewers/appViewer';
|
import AppViewer from 'component/viewers/appViewer';
|
||||||
|
@ -11,18 +11,14 @@ import Button from 'component/button';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
// @if TARGET='web'
|
|
||||||
import { generateStreamUrl } from 'util/lbrytv';
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import Yrbl from 'component/yrbl';
|
|
||||||
|
|
||||||
import DocumentViewer from 'component/viewers/documentViewer';
|
import DocumentViewer from 'component/viewers/documentViewer';
|
||||||
import PdfViewer from 'component/viewers/pdfViewer';
|
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';
|
import HtmlViewer from 'component/viewers/htmlViewer';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
// should match
|
||||||
import DocxViewer from 'component/viewers/docxViewer';
|
import DocxViewer from 'component/viewers/docxViewer';
|
||||||
import ComicBookViewer from 'component/viewers/comicBookViewer';
|
import ComicBookViewer from 'component/viewers/comicBookViewer';
|
||||||
import ThreeViewer from 'component/viewers/threeViewer';
|
import ThreeViewer from 'component/viewers/threeViewer';
|
||||||
|
@ -31,17 +27,17 @@ import ThreeViewer from 'component/viewers/threeViewer';
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
mediaType: string,
|
mediaType: string,
|
||||||
isText: true,
|
|
||||||
streamingUrl: string,
|
streamingUrl: string,
|
||||||
embedded?: boolean,
|
embedded?: boolean,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
downloadPath: string,
|
downloadPath: string,
|
||||||
fileName: string,
|
fileExtension: string,
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
setPlayingUri: (string | null) => void,
|
setPlayingUri: (string | null) => void,
|
||||||
currentlyFloating: boolean,
|
currentlyFloating: boolean,
|
||||||
|
renderMode: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,114 +109,50 @@ class FileRender extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderViewer() {
|
renderViewer() {
|
||||||
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
|
const { currentTheme, contentType, downloadPath, fileExtension, streamingUrl, uri, renderMode } = this.props;
|
||||||
const fileType = fileName && path.extname(fileName).substring(1);
|
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
|
switch (renderMode) {
|
||||||
// https://github.com/lbryio/lbrytv/issues/51
|
case RENDER_MODES.AUDIO:
|
||||||
const source = IS_WEB ? generateStreamUrl(claim.name, claim.claim_id) : streamingUrl;
|
case RENDER_MODES.VIDEO:
|
||||||
|
return <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />;
|
||||||
// Human-readable files (scripts and plain-text files)
|
case RENDER_MODES.IMAGE:
|
||||||
const readableFiles = ['text', 'document', 'script'];
|
return <ImageViewer uri={uri} source={source} />;
|
||||||
|
case RENDER_MODES.HTML:
|
||||||
// Supported mediaTypes
|
return <HtmlViewer source={downloadPath || source} />;
|
||||||
const mediaTypes = {
|
case RENDER_MODES.DOCUMENT:
|
||||||
// @if TARGET='app'
|
case RENDER_MODES.MARKDOWN:
|
||||||
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
return (
|
||||||
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
|
<DocumentViewer
|
||||||
application: <AppViewer uri={uri} />,
|
source={{
|
||||||
// @endif
|
// @if TARGET='app'
|
||||||
|
file: options => fs.createReadStream(downloadPath, options),
|
||||||
video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
|
// @endif
|
||||||
audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
|
stream: source,
|
||||||
image: <ImageViewer uri={uri} source={source} />,
|
fileExtension,
|
||||||
// Add routes to viewer...
|
contentType,
|
||||||
};
|
}}
|
||||||
|
renderMode={renderMode}
|
||||||
// Supported contentTypes
|
theme={currentTheme}
|
||||||
const contentTypes = {
|
/>
|
||||||
'application/x-ext-mkv': (
|
);
|
||||||
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
|
case RENDER_MODES.DOCX:
|
||||||
),
|
return <DocxViewer source={downloadPath} />;
|
||||||
'video/x-matroska': (
|
case RENDER_MODES.PDF:
|
||||||
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
|
return <PdfViewer source={downloadPath || source} />;
|
||||||
),
|
case RENDER_MODES.CAD:
|
||||||
'application/pdf': <PdfViewer source={downloadPath || source} />,
|
return <ThreeViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
|
||||||
'text/html': <HtmlViewer source={downloadPath || source} />,
|
case RENDER_MODES.COMIC:
|
||||||
'text/htm': <HtmlViewer source={downloadPath || source} />,
|
return <ComicBookViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
|
||||||
};
|
case RENDER_MODES.APPLICATION:
|
||||||
|
return <AppViewer uri={uri} />;
|
||||||
// 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 = (
|
|
||||||
<DocumentViewer
|
|
||||||
source={{
|
|
||||||
// @if TARGET='app'
|
|
||||||
file: options => fs.createReadStream(downloadPath, options),
|
|
||||||
// @endif
|
|
||||||
stream: source,
|
|
||||||
fileType,
|
|
||||||
contentType,
|
|
||||||
}}
|
|
||||||
theme={currentTheme}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @if TARGET='web'
|
return null;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
// @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() {
|
render() {
|
||||||
const { isText, uri, currentlyFloating, embedded } = this.props;
|
const { uri, currentlyFloating, embedded, renderMode } = this.props;
|
||||||
const { showAutoplayCountdown, showEmbededMessage } = this.state;
|
const { showAutoplayCountdown, showEmbededMessage } = this.state;
|
||||||
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
|
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
|
||||||
|
|
||||||
|
@ -228,7 +160,7 @@ class FileRender extends React.PureComponent<Props, State> {
|
||||||
<div
|
<div
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'file-render': !embedded,
|
'file-render': !embedded,
|
||||||
'file-render--document': isText && !embedded,
|
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
||||||
'file-render__embed': embedded,
|
'file-render__embed': embedded,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
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
|
@ -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 * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectThumbnailForUri,
|
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
makeSelectUriIsStreamable,
|
|
||||||
makeSelectTitleForUri,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||||
import {
|
import {
|
||||||
makeSelectIsPlaying,
|
makeSelectIsPlaying,
|
||||||
makeSelectShouldObscurePreview,
|
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectIsText,
|
makeSelectFileRenderModeForUri,
|
||||||
|
makeSelectStreamingUrlForUriWebProxy,
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
import FileViewer from './view';
|
import FileRenderFloating from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const uri = selectPlayingUri(state);
|
const uri = selectPlayingUri(state);
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
title: makeSelectTitleForUri(uri)(state),
|
title: makeSelectTitleForUri(uri)(state),
|
||||||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
|
||||||
mediaType: makeSelectMediaTypeForUri(uri)(state),
|
|
||||||
contentType: makeSelectContentTypeForUri(uri)(state),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||||
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
|
|
||||||
isPlaying: makeSelectIsPlaying(uri)(state),
|
isPlaying: makeSelectIsPlaying(uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
|
||||||
isStreamable: makeSelectUriIsStreamable(uri)(state),
|
|
||||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(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()),
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(connect(select, perform)(FileRenderFloating));
|
||||||
connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileViewer)
|
|
||||||
);
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -8,24 +9,17 @@ import FileRender from 'component/fileRender';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import usePrevious from 'effects/use-previous';
|
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 Draggable from 'react-draggable';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import useIsMobile from 'effects/use-is-mobile';
|
import useIsMobile from 'effects/use-is-mobile';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mediaType: string,
|
|
||||||
contentType: string,
|
|
||||||
isText: boolean,
|
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
uri: string,
|
uri: string,
|
||||||
obscurePreview: boolean,
|
|
||||||
insufficientCredits: boolean,
|
|
||||||
isStreamable: boolean,
|
|
||||||
thumbnail?: string,
|
|
||||||
streamingUrl?: string,
|
streamingUrl?: string,
|
||||||
floatingPlayer: boolean,
|
floatingPlayer: boolean,
|
||||||
pageUri: ?string,
|
pageUri: ?string,
|
||||||
|
@ -33,25 +27,23 @@ type Props = {
|
||||||
floatingPlayerEnabled: boolean,
|
floatingPlayerEnabled: boolean,
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
triggerAnalyticsView: (string, number) => Promise<any>,
|
triggerAnalyticsView: (string, number) => Promise<any>,
|
||||||
|
renderMode: string,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileViewer(props: Props) {
|
export default function FloatingViewer(props: Props) {
|
||||||
const {
|
const {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
fileInfo,
|
fileInfo,
|
||||||
uri,
|
uri,
|
||||||
streamingUrl,
|
streamingUrl,
|
||||||
isStreamable,
|
|
||||||
pageUri,
|
pageUri,
|
||||||
title,
|
title,
|
||||||
clearPlayingUri,
|
clearPlayingUri,
|
||||||
floatingPlayerEnabled,
|
floatingPlayerEnabled,
|
||||||
triggerAnalyticsView,
|
triggerAnalyticsView,
|
||||||
claimRewards,
|
claimRewards,
|
||||||
mediaType,
|
renderMode,
|
||||||
contentType,
|
|
||||||
isText,
|
|
||||||
} = props;
|
} = props;
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [playTime, setPlayTime] = useState();
|
const [playTime, setPlayTime] = useState();
|
||||||
|
@ -60,40 +52,17 @@ export default function FileViewer(props: Props) {
|
||||||
x: -25,
|
x: -25,
|
||||||
y: window.innerHeight - 400,
|
y: window.innerHeight - 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inline = pageUri === uri;
|
const inline = pageUri === uri;
|
||||||
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
|
const isPlayable = RENDER_MODES.PLAYABLE_MODES.includes(renderMode);
|
||||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||||
const isReadyToPlay =
|
|
||||||
(IS_WEB && (isStreamable || webStreamOnly || forceVideo)) ||
|
|
||||||
((isStreamable || forceVideo) && streamingUrl) ||
|
|
||||||
(fileInfo && fileInfo.completed);
|
|
||||||
const loadingMessage =
|
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.")
|
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
|
||||||
: __('Loading');
|
: __('Loading');
|
||||||
|
|
||||||
const previousUri = usePrevious(uri);
|
const previousUri = usePrevious(uri);
|
||||||
const isNewView = uri && previousUri !== uri && isPlaying;
|
const isNewView = uri && previousUri !== uri && isPlaying;
|
||||||
const [hasRecordedView, setHasRecordedView] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
||||||
|
@ -115,6 +84,27 @@ export default function FileViewer(props: Props) {
|
||||||
};
|
};
|
||||||
}, [setFileViewerRect, inline]);
|
}, [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) {
|
function handleDrag(e, ui) {
|
||||||
const { x, y } = position;
|
const { x, y } = position;
|
||||||
const newX = x + ui.deltaX;
|
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 (
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
onDrag={handleDrag}
|
onDrag={handleDrag}
|
|
@ -1,41 +1,32 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import {
|
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectThumbnailForUri,
|
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
makeSelectUriIsStreamable,
|
|
||||||
makeSelectClaimForUri,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
import {
|
import {
|
||||||
makeSelectIsPlaying,
|
makeSelectIsPlaying,
|
||||||
makeSelectShouldObscurePreview,
|
makeSelectShouldObscurePreview,
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectCanAutoplay,
|
makeSelectInsufficientCreditsForUri,
|
||||||
makeSelectIsText,
|
makeSelectStreamingUrlForUriWebProxy,
|
||||||
|
makeSelectFileRenderModeForUri,
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import FileViewer from './view';
|
import FileRenderInitiator from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
isAutoPlayable: makeSelectCanAutoplay(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
isText: makeSelectIsText(props.uri)(state),
|
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -48,7 +39,4 @@ const perform = dispatch => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default withRouter(connect(select, perform)(FileRenderInitiator));
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileViewer);
|
|
|
@ -5,78 +5,52 @@
|
||||||
// while a file is currently being viewed
|
// while a file is currently being viewed
|
||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import classnames from 'classnames';
|
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 Button from 'component/button';
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import Yrbl from 'component/yrbl';
|
import Nag from 'component/common/nag';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
import { generateDownloadUrl } from 'util/lbrytv';
|
|
||||||
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
|
||||||
|
|
||||||
const SPACE_BAR_KEYCODE = 32;
|
const SPACE_BAR_KEYCODE = 32;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
play: string => void,
|
play: string => void,
|
||||||
mediaType: string,
|
|
||||||
isText: boolean,
|
|
||||||
contentType: string,
|
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
history: { push: string => void },
|
||||||
obscurePreview: boolean,
|
obscurePreview: boolean,
|
||||||
insufficientCredits: boolean,
|
insufficientCredits: boolean,
|
||||||
isStreamable: boolean,
|
|
||||||
thumbnail?: string,
|
thumbnail?: string,
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
hasCostInfo: boolean,
|
hasCostInfo: boolean,
|
||||||
costInfo: any,
|
costInfo: any,
|
||||||
isAutoPlayable: boolean,
|
|
||||||
inline: boolean,
|
inline: boolean,
|
||||||
|
renderMode: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileViewerInitiator(props: Props) {
|
export default function FileRenderInitiator(props: Props) {
|
||||||
const {
|
const {
|
||||||
play,
|
play,
|
||||||
mediaType,
|
|
||||||
isText,
|
|
||||||
contentType,
|
|
||||||
isPlaying,
|
isPlaying,
|
||||||
fileInfo,
|
fileInfo,
|
||||||
uri,
|
uri,
|
||||||
obscurePreview,
|
obscurePreview,
|
||||||
insufficientCredits,
|
insufficientCredits,
|
||||||
|
history,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
autoplay,
|
autoplay,
|
||||||
isStreamable,
|
renderMode,
|
||||||
hasCostInfo,
|
hasCostInfo,
|
||||||
costInfo,
|
costInfo,
|
||||||
isAutoPlayable,
|
|
||||||
claim,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const cost = costInfo && costInfo.cost;
|
const cost = costInfo && costInfo.cost;
|
||||||
const forceVideo = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
const isFree = hasCostInfo && cost === 0;
|
||||||
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
|
|
||||||
const isImage = mediaType === 'image';
|
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
const isPlayable = RENDER_MODES.PLAYABLE_MODES.includes(renderMode);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap this in useCallback because we need to use it to the keyboard effect
|
// Wrap this in useCallback because we need to use it to the keyboard effect
|
||||||
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
|
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
|
||||||
|
@ -112,45 +86,55 @@ export default function FileViewerInitiator(props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const videoOnPage = document.querySelector('video');
|
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();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
disabled={!hasCostInfo}
|
onClick={disabled ? undefined : viewFile}
|
||||||
style={!obscurePreview && supported && thumbnail && !isPlaying ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
onClick={supported ? viewFile : undefined}
|
className={classnames('content__cover', {
|
||||||
className={classnames({
|
'content__cover--disabled': disabled,
|
||||||
content__cover: supported,
|
|
||||||
'content__cover--disabled': !supported,
|
|
||||||
'content__cover--hidden-for-text': isText,
|
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
'card__media--disabled': supported && !fileInfo && insufficientCredits,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!supported && (
|
{showAppNag && (
|
||||||
<Yrbl
|
<Nag
|
||||||
type="happy"
|
type="helpful"
|
||||||
title={getTitle()}
|
inline
|
||||||
subtitle={
|
message={__('This content requires LBRY Desktop to display.')}
|
||||||
<I18nMessage
|
actionText={__('Get the App')}
|
||||||
tokens={{
|
href="https://lbry.com/get"
|
||||||
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>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{insufficientCredits && !showAppNag && (
|
||||||
{!isPlaying && supported && (
|
<Nag
|
||||||
|
type="helpful"
|
||||||
|
inline
|
||||||
|
message={__('You need more credits to purchase this.')}
|
||||||
|
actionText={__('Open Rewards')}
|
||||||
|
onClick={() => history.push(`/$/${PAGES.REWARDS}`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!disabled && (
|
||||||
<Button
|
<Button
|
||||||
onClick={viewFile}
|
onClick={viewFile}
|
||||||
iconSize={30}
|
iconSize={30}
|
|
@ -1,24 +1,22 @@
|
||||||
import { connect } from 'react-redux';
|
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 {
|
import { makeSelectFileInfoForUri, makeSelectMediaTypeForUri, makeSelectContentTypeForUri } from 'lbry-redux';
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
makeSelectUriIsStreamable,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||||
import { makeSelectIsPlaying } from 'redux/selectors/content';
|
import {
|
||||||
|
makeSelectFileRenderModeForUri,
|
||||||
|
makeSelectIsPlaying,
|
||||||
|
makeSelectStreamingUrlForUriWebProxy,
|
||||||
|
} from 'redux/selectors/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
import FileViewer from './view';
|
import FileRenderInline from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
@ -26,9 +24,4 @@ const perform = dispatch => ({
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(connect(select, perform)(FileRenderInline));
|
||||||
connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileViewer)
|
|
||||||
);
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import FileRender from 'component/fileRender';
|
import FileRender from 'component/fileRender';
|
||||||
import usePrevious from 'effects/use-previous';
|
import usePrevious from 'effects/use-previous';
|
||||||
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mediaType: string,
|
mediaType: string,
|
||||||
|
@ -10,30 +10,20 @@ type Props = {
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
uri: string,
|
uri: string,
|
||||||
isStreamable: boolean,
|
renderMode: string,
|
||||||
streamingUrl?: string,
|
streamingUrl?: string,
|
||||||
triggerAnalyticsView: (string, number) => Promise<any>,
|
triggerAnalyticsView: (string, number) => Promise<any>,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TextViewer(props: Props) {
|
export default function FileRenderInline(props: Props) {
|
||||||
const {
|
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, claimRewards } = props;
|
||||||
isPlaying,
|
|
||||||
fileInfo,
|
|
||||||
uri,
|
|
||||||
streamingUrl,
|
|
||||||
isStreamable,
|
|
||||||
triggerAnalyticsView,
|
|
||||||
claimRewards,
|
|
||||||
mediaType,
|
|
||||||
contentType,
|
|
||||||
} = props;
|
|
||||||
const [playTime, setPlayTime] = useState();
|
const [playTime, setPlayTime] = useState();
|
||||||
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
|
|
||||||
const previousUri = usePrevious(uri);
|
const previousUri = usePrevious(uri);
|
||||||
const isNewView = uri && previousUri !== uri && isPlaying;
|
const isNewView = uri && previousUri !== uri && isPlaying;
|
||||||
const [hasRecordedView, setHasRecordedView] = useState(false);
|
const [hasRecordedView, setHasRecordedView] = useState(false);
|
||||||
const isReadyToPlay = (IS_WEB && (isStreamable || streamingUrl || webStreamOnly)) || (fileInfo && fileInfo.completed);
|
const isReadyToPlay = streamingUrl || (fileInfo && fileInfo.completed);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNewView) {
|
if (isNewView) {
|
||||||
|
@ -52,9 +42,9 @@ export default function TextViewer(props: Props) {
|
||||||
}
|
}
|
||||||
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
|
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
|
||||||
|
|
||||||
return (
|
if (!isPlaying) {
|
||||||
<div className={classnames('content__viewersss')}>
|
return null;
|
||||||
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
|
}
|
||||||
</div>
|
|
||||||
);
|
return isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={__('Preparing your content')} />;
|
||||||
}
|
}
|
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
|
@ -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 { connect } from 'react-redux';
|
||||||
import { makeSelectViewCountForUri } from 'lbryinc';
|
import { doFetchViewCount, makeSelectViewCountForUri } from 'lbryinc';
|
||||||
import FileViewCount from './view';
|
import FileViewCount from './view';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
viewCount: makeSelectViewCountForUri(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
|
// @flow
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import HelpLink from 'component/common/help-link';
|
import HelpLink from 'component/common/help-link';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
claim: StreamClaim,
|
||||||
|
fetchViewCount: string => void,
|
||||||
|
uri: string,
|
||||||
viewCount: string,
|
viewCount: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileViewCount(props: Props) {
|
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 (
|
return (
|
||||||
<span>
|
<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.
|
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.
|
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 { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -141,39 +141,39 @@ function PublishForm(props: Props) {
|
||||||
}, [name, channel, resolveUri, updatePublishForm]);
|
}, [name, channel, resolveUri, updatePublishForm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="card-stack">
|
||||||
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
|
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
|
||||||
{!publishing && (
|
{!publishing && (
|
||||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||||
<PublishText disabled={formDisabled} />
|
<PublishText disabled={formDisabled} />
|
||||||
<Card actions={<SelectThumbnail />} />
|
<Card actions={<SelectThumbnail />} />
|
||||||
|
|
||||||
<TagsSelect
|
<TagsSelect
|
||||||
suggestMature
|
suggestMature
|
||||||
disableAutoFocus
|
disableAutoFocus
|
||||||
hideHeader
|
hideHeader
|
||||||
label={__('Selected Tags')}
|
label={__('Selected Tags')}
|
||||||
empty={__('No tags added')}
|
empty={__('No tags added')}
|
||||||
limitSelect={TAGS_LIMIT}
|
limitSelect={TAGS_LIMIT}
|
||||||
help={__(
|
help={__(
|
||||||
'Add tags that are relevant to your content. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated.'
|
'Add tags that are relevant to your content. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated.'
|
||||||
)}
|
)}
|
||||||
placeholder={__('gaming, crypto')}
|
placeholder={__('gaming, crypto')}
|
||||||
onSelect={newTags => {
|
onSelect={newTags => {
|
||||||
const validatedTags = [];
|
const validatedTags = [];
|
||||||
newTags.forEach(newTag => {
|
newTags.forEach(newTag => {
|
||||||
if (!tags.some(tag => tag.name === newTag.name)) {
|
if (!tags.some(tag => tag.name === newTag.name)) {
|
||||||
validatedTags.push(newTag);
|
validatedTags.push(newTag);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updatePublishForm({ tags: [...tags, ...validatedTags] });
|
updatePublishForm({ tags: [...tags, ...validatedTags] });
|
||||||
}}
|
}}
|
||||||
onRemove={clickedTag => {
|
onRemove={clickedTag => {
|
||||||
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
|
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
|
||||||
updatePublishForm({ tags: newTags });
|
updatePublishForm({ tags: newTags });
|
||||||
}}
|
}}
|
||||||
tagsChosen={tags}
|
tagsChosen={tags}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
|
@ -209,7 +209,7 @@ function PublishForm(props: Props) {
|
||||||
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ type Options = {
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: ?StreamClaim,
|
claim: ?StreamClaim,
|
||||||
claimId: string,
|
|
||||||
recommendedContent: Array<string>,
|
recommendedContent: Array<string>,
|
||||||
isSearching: boolean,
|
isSearching: boolean,
|
||||||
search: (string, Options) => void,
|
search: (string, Options) => void,
|
||||||
|
@ -43,10 +42,10 @@ export default class RecommendedContent extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecommendedContent() {
|
getRecommendedContent() {
|
||||||
const { claim, search, mature, claimId } = this.props;
|
const { claim, search, mature } = this.props;
|
||||||
|
|
||||||
if (claim && claim.value && claim.value) {
|
if (claim && claim.value && claim.claim_id) {
|
||||||
const options: Options = { size: 20, related_to: claimId, isBackgroundSearch: true };
|
const options: Options = { size: 20, related_to: claim.claim_id, isBackgroundSearch: true };
|
||||||
if (claim && !mature) {
|
if (claim && !mature) {
|
||||||
options['nsfw'] = false;
|
options['nsfw'] = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ function AppViewer(props: Props) {
|
||||||
// }, [outpoint, contentType, setAppUrl, setLoading]);
|
// }, [outpoint, contentType, setAppUrl, setLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content__cover--disabled">
|
<div className="content__cover--none">
|
||||||
<Yrbl
|
<Yrbl
|
||||||
title={__('Sorry')}
|
title={__('Sorry')}
|
||||||
subtitle={__('Games and apps are currently disabled due to potential security concerns.')}
|
subtitle={__('Games and apps are currently disabled due to potential security concerns.')}
|
||||||
|
|
|
@ -3,11 +3,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
import Card from 'component/common/card';
|
||||||
import CodeViewer from 'component/viewers/codeViewer';
|
import CodeViewer from 'component/viewers/codeViewer';
|
||||||
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
theme: string,
|
theme: string,
|
||||||
|
renderMode: string,
|
||||||
source: {
|
source: {
|
||||||
file: (?string) => any,
|
file: (?string) => any,
|
||||||
stream: string,
|
stream: string,
|
||||||
|
@ -79,20 +82,15 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDocument() {
|
renderDocument() {
|
||||||
let viewer = null;
|
|
||||||
const { content } = this.state;
|
const { content } = this.state;
|
||||||
const { source, theme } = this.props;
|
const { source, theme, renderMode } = this.props;
|
||||||
const { fileType, contentType } = source;
|
const { 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} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return viewer;
|
return renderMode === RENDER_MODES.MARKDOWN ? (
|
||||||
|
<Card body={<MarkdownPreview content={content} />} />
|
||||||
|
) : (
|
||||||
|
<CodeViewer value={content} contentType={contentType} theme={theme} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -101,7 +99,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
||||||
const errorMessage = __("Sorry, looks like we can't load the document.");
|
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-render__viewer--document">
|
<div className="file-render__viewer file-render__viewer--document">
|
||||||
{loading && !error && <div className="placeholder--text-document" />}
|
{loading && !error && <div className="placeholder--text-document" />}
|
||||||
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
|
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
|
||||||
{isReady && this.renderDocument()}
|
{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.");
|
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-render__viewer--document">
|
<div className="file-render__viewer file-render__viewer--document">
|
||||||
{loading && <LoadingScreen status={loadingMessage} spinner />}
|
{loading && <LoadingScreen status={loadingMessage} spinner />}
|
||||||
{error && <LoadingScreen status={errorMessage} spinner={false} />}
|
{error && <LoadingScreen status={errorMessage} spinner={false} />}
|
||||||
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}
|
{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 { source } = this.props;
|
||||||
const { loading } = this.state;
|
const { loading } = this.state;
|
||||||
return (
|
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" />}
|
{loading && <div className="placeholder--text-document" />}
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
<iframe ref={this.iframe} hidden={loading} sandbox="" title={__('File preview')} src={`file://${source}`} />
|
<iframe ref={this.iframe} hidden={loading} sandbox="" title={__('File preview')} src={`file://${source}`} />
|
||||||
|
|
|
@ -1,57 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { stopContextMenu } from 'util/context-menu';
|
import { stopContextMenu } from 'util/context-menu';
|
||||||
import Button from 'component/button';
|
import IframeReact from 'component/IframeReact';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
// @if TARGET='app'
|
|
||||||
import { shell } from 'electron';
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
source: string,
|
source: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class PdfViewer extends React.PureComponent<Props> {
|
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() {
|
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 { source } = this.props;
|
||||||
|
const src = IS_WEB ? source : `file://${source}`;
|
||||||
return (
|
return (
|
||||||
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
|
<div className="file-render__viewer file-render__viewer--document" onContextMenu={stopContextMenu}>
|
||||||
{/* @if TARGET='app' */}
|
<div className="file-render__viewer file-render__viewer--iframe">
|
||||||
<p>
|
<IframeReact title={__('File preview')} src={src} />
|
||||||
<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>
|
</div>
|
||||||
{/* @endif */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const WalletBalance = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<section className="section__flex-wrap">
|
<section className="columns">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="section__title">{__('Available Balance')}</h2>
|
<h2 className="section__title">{__('Available Balance')}</h2>
|
||||||
<span className="section__title--large">
|
<span className="section__title--large">
|
||||||
|
|
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 { doSetContentHistoryItem } from 'redux/actions/content';
|
||||||
import {
|
import {
|
||||||
doFetchFileInfo,
|
doFetchFileInfo,
|
||||||
makeSelectClaimIsMine,
|
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectMetadataForUri,
|
makeSelectMetadataForUri,
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
selectBalance,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectIsText } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import FilePage from './view';
|
import FilePage from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -22,11 +20,9 @@ const select = (state, props) => ({
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowMatureContent(state),
|
obscureNsfw: !selectShowMatureContent(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||||
balance: selectBalance(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
isText: makeSelectIsText(props.uri)(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
@ -34,10 +30,6 @@ const perform = dispatch => ({
|
||||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||||
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(FilePage);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FilePage);
|
|
||||||
|
|
|
@ -1,41 +1,44 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Button from 'component/button';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import I18nMessage from 'component/i18nMessage/view';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import LayoutWrapperFile from 'component/layoutWrapperFile';
|
import ClaimUri from 'component/claimUri';
|
||||||
import LayoutWrapperText from 'component/layoutWrapperText';
|
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 = {
|
type Props = {
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
|
costInfo: ?{ includesData: boolean, cost: number },
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
uri: string,
|
uri: string,
|
||||||
claimIsMine: boolean,
|
|
||||||
costInfo: ?{ cost: number },
|
|
||||||
fetchFileInfo: string => void,
|
fetchFileInfo: string => void,
|
||||||
fetchCostInfo: string => void,
|
fetchCostInfo: string => void,
|
||||||
setViewed: string => void,
|
setViewed: string => void,
|
||||||
isSubscribed: ?string,
|
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
channelUri: string,
|
channelUri: string,
|
||||||
viewCount: number,
|
renderMode: string,
|
||||||
markSubscriptionRead: (string, string) => void,
|
markSubscriptionRead: (string, string) => void,
|
||||||
fetchViewCount: string => void,
|
|
||||||
balance: number,
|
|
||||||
isText: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePage extends React.Component<Props> {
|
class FilePage extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { uri, claim, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed, fetchViewCount } = this.props;
|
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
|
||||||
|
|
||||||
if (isSubscribed) {
|
if (isSubscribed) {
|
||||||
this.removeFromSubscriptionNotifications();
|
this.removeFromSubscriptionNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchViewCount(claim.claim_id);
|
|
||||||
|
|
||||||
// always refresh file info when entering file page to see if we have the file
|
// 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'
|
// @if TARGET='app'
|
||||||
fetchFileInfo(uri);
|
fetchFileInfo(uri);
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -46,16 +49,12 @@ class FilePage extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: 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) {
|
if (!prevProps.isSubscribed && isSubscribed) {
|
||||||
this.removeFromSubscriptionNotifications();
|
this.removeFromSubscriptionNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.uri !== uri) {
|
|
||||||
fetchViewCount(claim.claim_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.uri !== uri) {
|
if (prevProps.uri !== uri) {
|
||||||
setViewed(uri);
|
setViewed(uri);
|
||||||
}
|
}
|
||||||
|
@ -74,26 +73,67 @@ class FilePage extends React.Component<Props> {
|
||||||
markSubscriptionRead(channelUri, uri);
|
markSubscriptionRead(channelUri, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderFilePageLayout(uri, mode, cost) {
|
||||||
const { uri, claimIsMine, costInfo, fileInfo, balance, isText } = this.props;
|
if (RENDER_MODES.PLAYABLE_MODES.includes(mode)) {
|
||||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
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 (
|
return (
|
||||||
<Page className="main--file-page">
|
<React.Fragment>
|
||||||
{!fileInfo && insufficientCredits && (
|
<ClaimUri uri={uri} />
|
||||||
<div className="media__insufficient-credits help--warning">
|
<FileRenderInitiator uri={uri} />
|
||||||
<I18nMessage
|
<FileRenderInline uri={uri} />
|
||||||
tokens={{
|
<FileTitle uri={uri} />
|
||||||
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
|
</React.Fragment>
|
||||||
}}
|
);
|
||||||
>
|
}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ class HelpPage extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page className="card-stack">
|
||||||
<Card
|
<Card
|
||||||
title={__('Read the FAQ')}
|
title={__('Read the FAQ')}
|
||||||
subtitle={__('Our FAQ answers many common questions.')}
|
subtitle={__('Our FAQ answers many common questions.')}
|
||||||
|
@ -202,83 +202,82 @@ class HelpPage extends React.PureComponent<Props, State> {
|
||||||
<WalletBackup />
|
<WalletBackup />
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
<section className="card">
|
<Card
|
||||||
<header className="table__header">
|
title={__('About')}
|
||||||
<div className="table__header-text">
|
subtitle={
|
||||||
<h2 className="section__title">{__('About')}</h2>
|
this.state.upgradeAvailable !== null && this.state.upgradeAvailable ? (
|
||||||
|
<span>
|
||||||
{this.state.upgradeAvailable !== null && this.state.upgradeAvailable && (
|
{__('A newer version of LBRY is available.')}
|
||||||
<p className="section__subtitle">
|
<Button button="link" href={newVerLink} label={__('Download now!')} />
|
||||||
{__('A newer version of LBRY is available.')}{' '}
|
</span>
|
||||||
<Button button="link" href={newVerLink} label={__('Download now!')} />
|
) : null
|
||||||
</p>
|
}
|
||||||
)}
|
isBodyTable
|
||||||
</div>
|
body={
|
||||||
</header>
|
<div className="table__wrapper">
|
||||||
|
<table className="table table--stretch">
|
||||||
<div className="table__wrapper">
|
<tbody>
|
||||||
<table className="table table--stretch">
|
<tr>
|
||||||
<tbody>
|
<td>{__('App')}</td>
|
||||||
<tr>
|
<td>{this.state.uiVersion}</td>
|
||||||
<td>{__('App')}</td>
|
</tr>
|
||||||
<td>{this.state.uiVersion}</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Daemon (lbrynet)')}</td>
|
||||||
<tr>
|
<td>{ver ? ver.lbrynet_version : __('Loading...')}</td>
|
||||||
<td>{__('Daemon (lbrynet)')}</td>
|
</tr>
|
||||||
<td>{ver ? ver.lbrynet_version : __('Loading...')}</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Connected Email')}</td>
|
||||||
<tr>
|
<td>
|
||||||
<td>{__('Connected Email')}</td>
|
{user && user.primary_email ? (
|
||||||
<td>
|
<React.Fragment>
|
||||||
{user && user.primary_email ? (
|
{user.primary_email}{' '}
|
||||||
<React.Fragment>
|
<Button
|
||||||
{user.primary_email}{' '}
|
button="link"
|
||||||
<Button
|
href={`https://lbry.com/list/edit/${accessToken}`}
|
||||||
button="link"
|
label={__('Update mailing preferences')}
|
||||||
href={`https://lbry.com/list/edit/${accessToken}`}
|
/>
|
||||||
label={__('Update mailing preferences')}
|
</React.Fragment>
|
||||||
/>
|
) : (
|
||||||
</React.Fragment>
|
<React.Fragment>
|
||||||
) : (
|
<span className="empty">{__('none')} </span>
|
||||||
<React.Fragment>
|
<Button button="link" onClick={() => doAuth()} label={__('set email')} />
|
||||||
<span className="empty">{__('none')} </span>
|
</React.Fragment>
|
||||||
<Button button="link" onClick={() => doAuth()} label={__('set email')} />
|
)}
|
||||||
</React.Fragment>
|
</td>
|
||||||
)}
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Reward Eligible')}</td>
|
||||||
<tr>
|
<td>{user && user.is_reward_approved ? __('Yes') : __('No')}</td>
|
||||||
<td>{__('Reward Eligible')}</td>
|
</tr>
|
||||||
<td>{user && user.is_reward_approved ? __('Yes') : __('No')}</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Platform')}</td>
|
||||||
<tr>
|
<td>{platform}</td>
|
||||||
<td>{__('Platform')}</td>
|
</tr>
|
||||||
<td>{platform}</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Installation ID')}</td>
|
||||||
<tr>
|
<td>{this.state.lbryId}</td>
|
||||||
<td>{__('Installation ID')}</td>
|
</tr>
|
||||||
<td>{this.state.lbryId}</td>
|
<tr>
|
||||||
</tr>
|
<td>{__('Access Token')}</td>
|
||||||
<tr>
|
<td>
|
||||||
<td>{__('Access Token')}</td>
|
{this.state.accessTokenHidden && (
|
||||||
<td>
|
<Button button="link" label={__('View')} onClick={this.showAccessToken} />
|
||||||
{this.state.accessTokenHidden && (
|
)}
|
||||||
<Button button="link" label={__('View')} onClick={this.showAccessToken} />
|
{!this.state.accessTokenHidden && accessToken && (
|
||||||
)}
|
<div>
|
||||||
{!this.state.accessTokenHidden && accessToken && (
|
<p>{accessToken}</p>
|
||||||
<div>
|
<div className="help--warning">
|
||||||
<p>{accessToken}</p>
|
{__('This is equivalent to a password. Do not post or share this.')}
|
||||||
<div className="help--warning">
|
</div>
|
||||||
{__('This is equivalent to a password. Do not post or share this.')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
}
|
||||||
</section>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,7 +258,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
const endHours = ['5', '6', '7', '8'];
|
const endHours = ['5', '6', '7', '8'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page className="card-stack">
|
||||||
{!IS_WEB && noDaemonSettings ? (
|
{!IS_WEB && noDaemonSettings ? (
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
|
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
|
||||||
|
|
|
@ -5,76 +5,64 @@ import {
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
makeSelectClaimsInChannelForCurrentPageState,
|
makeSelectClaimsInChannelForCurrentPageState,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
makeSelectRecommendedContentForUri,
|
makeSelectRecommendedContentForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
|
selectBalance,
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
parseURI,
|
parseURI,
|
||||||
|
makeSelectContentTypeForUri,
|
||||||
|
makeSelectUriIsStreamable,
|
||||||
|
makeSelectFileNameForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectAllCostInfoByUri } from 'lbryinc';
|
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
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 RECENT_HISTORY_AMOUNT = 10;
|
||||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
const HISTORY_ITEMS_PER_PAGE = 50;
|
||||||
|
|
||||||
export const selectState = (state: any) => state.content || {};
|
export const selectState = (state: any) => state.content || {};
|
||||||
|
|
||||||
export const selectPlayingUri = createSelector(
|
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||||
selectState,
|
|
||||||
state => state.playingUri
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectIsPlaying = (uri: string) =>
|
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
|
||||||
createSelector(
|
|
||||||
selectPlayingUri,
|
|
||||||
playingUri => playingUri === uri
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectContentPositionForUri = (uri: string) =>
|
export const makeSelectContentPositionForUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
|
||||||
selectState,
|
if (!claim) {
|
||||||
makeSelectClaimForUri(uri),
|
return null;
|
||||||
(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;
|
|
||||||
}
|
}
|
||||||
);
|
const outpoint = `${claim.txid}:${claim.nout}`;
|
||||||
|
const id = claim.claim_id;
|
||||||
|
return state.positions[id] ? state.positions[id][outpoint] : null;
|
||||||
|
});
|
||||||
|
|
||||||
export const selectHistory = createSelector(
|
export const selectHistory = createSelector(selectState, state => state.history || []);
|
||||||
selectState,
|
|
||||||
state => state.history || []
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectHistoryPageCount = createSelector(
|
export const selectHistoryPageCount = createSelector(selectHistory, history =>
|
||||||
selectHistory,
|
Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
|
||||||
history => Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const makeSelectHistoryForPage = (page: number) =>
|
export const makeSelectHistoryForPage = (page: number) =>
|
||||||
createSelector(
|
createSelector(selectHistory, selectClaimsByUri, (history, claimsByUri) => {
|
||||||
selectHistory,
|
const left = page * HISTORY_ITEMS_PER_PAGE;
|
||||||
selectClaimsByUri,
|
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
|
||||||
(history, claimsByUri) => {
|
return historyItemsForPage;
|
||||||
const left = page * HISTORY_ITEMS_PER_PAGE;
|
});
|
||||||
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
|
|
||||||
return historyItemsForPage;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectHistoryForUri = (uri: string) =>
|
export const makeSelectHistoryForUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(selectHistory, history => history.find(i => i.uri === uri));
|
||||||
selectHistory,
|
|
||||||
history => history.find(i => i.uri === uri)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectHasVisitedUri = (uri: string) =>
|
export const makeSelectHasVisitedUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(makeSelectHistoryForUri(uri), history => Boolean(history));
|
||||||
makeSelectHistoryForUri(uri),
|
|
||||||
history => Boolean(history)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
|
@ -132,51 +120,104 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectRecentHistory = createSelector(
|
export const selectRecentHistory = createSelector(selectHistory, history => {
|
||||||
selectHistory,
|
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
||||||
history => {
|
});
|
||||||
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
|
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
|
||||||
createSelector(
|
createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), channelClaims => {
|
||||||
makeSelectClaimsInChannelForCurrentPageState(channel),
|
if (uris) return uris;
|
||||||
channelClaims => {
|
|
||||||
if (uris) return uris;
|
|
||||||
|
|
||||||
if (channelClaims) {
|
if (channelClaims) {
|
||||||
const CATEGORY_LIST_SIZE = 10;
|
const CATEGORY_LIST_SIZE = 10;
|
||||||
return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`);
|
return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`);
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
export const makeSelectShouldObscurePreview = (uri: string) =>
|
export const makeSelectShouldObscurePreview = (uri: string) =>
|
||||||
|
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(
|
createSelector(
|
||||||
selectShowMatureContent,
|
makeSelectContentTypeForUri(uri),
|
||||||
makeSelectClaimIsNsfw(uri),
|
makeSelectMediaTypeForUri(uri),
|
||||||
(showMatureContent, isClaimMature) => {
|
makeSelectFileExtensionForUri(uri),
|
||||||
return isClaimMature && !showMatureContent;
|
(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(
|
createSelector(
|
||||||
makeSelectMediaTypeForUri(uri),
|
makeSelectClaimIsMine(uri),
|
||||||
mediaType => {
|
makeSelectCostInfoForUri(uri),
|
||||||
const canAutoPlay = ['audio', 'video', 'image', 'text', 'document'].includes(mediaType);
|
selectBalance,
|
||||||
return canAutoPlay;
|
(isMine, costInfo, balance) => {
|
||||||
}
|
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectIsText = (uri: string) =>
|
|
||||||
createSelector(
|
|
||||||
makeSelectMediaTypeForUri(uri),
|
|
||||||
mediaType => {
|
|
||||||
const isText = ['text', 'document', 'script'].includes(mediaType);
|
|
||||||
return isText;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--color-card-background);
|
background-color: var(--color-card-background);
|
||||||
margin-bottom: var(--spacing-large);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--card-radius);
|
border-radius: var(--card-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -79,6 +78,12 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-stack {
|
||||||
|
.card:not(:last-of-type) {
|
||||||
|
margin-bottom: var(--spacing-large);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card__list {
|
.card__list {
|
||||||
column-count: 2;
|
column-count: 2;
|
||||||
column-gap: var(--spacing-large);
|
column-gap: var(--spacing-large);
|
||||||
|
@ -86,7 +91,7 @@
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 var(--spacing-large);
|
margin-bottom: var(--spacing-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
@ -139,22 +144,48 @@
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__media--disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__header {
|
.card__header {
|
||||||
margin: var(--spacing-medium) var(--spacing-large);
|
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 {
|
.card__body {
|
||||||
padding: var(--spacing-large);
|
padding: var(--spacing-large);
|
||||||
padding-top: 0;
|
&:not(.card__body--no-title) {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
&.card__body--table {
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__main-actions {
|
.card__main-actions {
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
.comment {
|
.comment {
|
||||||
padding: var(--spacing-small) 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
padding: var(--spacing-medium) 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
border-bottom: 1px solid var(--color-border);
|
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;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__cover--disabled {
|
.content__cover--none {
|
||||||
@include thumbnail;
|
@include thumbnail;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -128,8 +130,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__cover--hidden-for-text {
|
.content__cover--disabled {
|
||||||
display: none;
|
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 {
|
.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 {
|
.file-render {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: var(--inline-player-max-height);
|
max-height: var(--inline-player-max-height);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
// margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-render--document {
|
.file-render--document {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
.content__loading {
|
.content__loading {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -22,6 +34,21 @@
|
||||||
color: var(--color-text);
|
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 {
|
.file-render__viewer {
|
||||||
|
@ -45,28 +72,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-render__viewer--document {
|
.file-render__viewer--iframe {
|
||||||
@extend .file-render__viewer;
|
display: flex; /*this eliminates extra height from whitespace, if someone edits this with a better technique, tell Jeremy*/
|
||||||
overflow: auto;
|
/*
|
||||||
|
ideally iframes would dynamiclly grow, see <IframeReact> for a start at this
|
||||||
.markdown-preview {
|
for now, since we don't know size, let's make as large as we can without being larger than available area
|
||||||
height: 100%;
|
*/
|
||||||
overflow: auto;
|
iframe {
|
||||||
|
height: calc(100vh - var(--header-height) - var(--spacing-medium) * 2);
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
padding: var(--spacing-small);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-render__viewer--pdf {
|
|
||||||
@extend .file-render__viewer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-render__content {
|
.file-render__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -138,6 +154,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-render {
|
.file-render {
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
|
||||||
.video-js {
|
.video-js {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
.main--auth-page {
|
||||||
max-width: 60rem;
|
max-width: 60rem;
|
||||||
margin-top: var(--spacing-main-padding);
|
margin-top: var(--spacing-main-padding);
|
||||||
|
@ -75,6 +48,9 @@
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
> .card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main--launching {
|
.main--launching {
|
||||||
|
@ -100,26 +76,3 @@
|
||||||
.main--full-width {
|
.main--full-width {
|
||||||
width: 100%;
|
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 {
|
.markdown-preview {
|
||||||
|
> :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
> *:last-child {
|
> *:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,25 +15,6 @@
|
||||||
// M E D I A
|
// M E D I A
|
||||||
// T I T L E
|
// 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 {
|
.media__uri {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateY(-130%);
|
transform: translateY(-130%);
|
||||||
|
@ -65,14 +46,6 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__uri--large {
|
|
||||||
margin-bottom: var(--spacing-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media__insufficient-credits {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// M E D I A
|
// M E D I A
|
||||||
// S U B T I T L E
|
// S U B T I T L E
|
||||||
|
|
||||||
|
@ -90,19 +63,7 @@
|
||||||
@extend .media__subtitle;
|
@extend .media__subtitle;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
margin-bottom: var(--spacing-small);
|
||||||
|
|
||||||
.media__subtitle--large {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
> button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media__subtitle__channel {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
margin: var(--spacing-small) 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__info-text {
|
.media__info-text {
|
||||||
|
@ -118,13 +79,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__actions {
|
.media__actions {
|
||||||
@extend .section__actions;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__document-thumbnail {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: var(--spacing-xlarge);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yrbl {
|
.yrbl {
|
||||||
|
|
|
@ -13,7 +13,6 @@ $nag-error-z-index: 100001;
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.button--link {
|
.button--link {
|
||||||
font-weight: var(--font-weight-bold);
|
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 {
|
.nag--helpful {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
|
|
@ -22,21 +22,11 @@
|
||||||
.section__flex {
|
.section__flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
> .icon__wrapper:first-child {
|
||||||
& > :first-child {
|
|
||||||
margin-right: var(--spacing-large);
|
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 {
|
.section__title {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: var(--font-title);
|
font-size: var(--font-title);
|
||||||
|
@ -101,6 +91,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section__actions--no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
.section__actions {
|
.section__actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -146,6 +146,7 @@ img {
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: var(--spacing-medium);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
|
|
||||||
|
@ -216,12 +217,16 @@ img {
|
||||||
.help--inline {
|
.help--inline {
|
||||||
@extend .help;
|
@extend .help;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: var(--color-text-empty);
|
color: var(--color-text-empty);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.empty--centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.qr-code {
|
.qr-code {
|
||||||
width: 134px;
|
width: 134px;
|
||||||
|
|
|
@ -19,9 +19,6 @@ $breakpoint-medium: 1150px;
|
||||||
--spacing-large: 2rem;
|
--spacing-large: 2rem;
|
||||||
--spacing-xlarge: 3rem;
|
--spacing-xlarge: 3rem;
|
||||||
--spacing-main-padding: var(--spacing-xlarge);
|
--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-width: 32rem;
|
||||||
--floating-viewer-height: 18rem; // 32 * 9/16
|
--floating-viewer-height: 18rem; // 32 * 9/16
|
||||||
--floating-viewer-info-height: 5rem;
|
--floating-viewer-info-height: 5rem;
|
||||||
|
|
This doesn't do anything. If iframes are serving files from the same domain, growable iframes would work. Ironically we just changed this.