File downloads and refactoring (#3918)
* am I done? * post diff * unused selector cleanup * missed commit * mess with button styles * fix flow Co-authored-by: Jeremy Kauffman <jeremy@lbry.io> Co-authored-by: Sean Yesmunt <sean@lbry.io>
This commit is contained in:
parent
86c75f13b6
commit
872259b73a
75 changed files with 1157 additions and 1194 deletions
|
@ -68,7 +68,7 @@
|
||||||
"@babel/register": "^7.0.0",
|
"@babel/register": "^7.0.0",
|
||||||
"@exponent/electron-cookies": "^2.0.0",
|
"@exponent/electron-cookies": "^2.0.0",
|
||||||
"@hot-loader/react-dom": "^16.8",
|
"@hot-loader/react-dom": "^16.8",
|
||||||
"@lbry/components": "^3.0.12",
|
"@lbry/components": "^4.0.1",
|
||||||
"@reach/menu-button": "0.7.4",
|
"@reach/menu-button": "0.7.4",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Binary file not shown.
Binary file not shown.
8
ui/component/IframeReact/index.js
Normal file
8
ui/component/IframeReact/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import IframeReact from './view';
|
||||||
|
|
||||||
|
const select = state => ({});
|
||||||
|
|
||||||
|
const perform = () => ({});
|
||||||
|
|
||||||
|
export default connect(select, perform)(IframeReact);
|
35
ui/component/IframeReact/view.jsx
Normal file
35
ui/component/IframeReact/view.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fullHeight: boolean,
|
||||||
|
src: string,
|
||||||
|
title: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function I18nMessage(props: Props) {
|
||||||
|
const { src, title } = props;
|
||||||
|
|
||||||
|
// const iframeRef = useRef();
|
||||||
|
|
||||||
|
// const [iframeHeight, setIframeHeight] = useState('80vh');
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
/*
|
||||||
|
|
||||||
|
iframe domain restrictions prevent naive design :-(
|
||||||
|
|
||||||
|
const obj = iframeRef.current;
|
||||||
|
if (obj) {
|
||||||
|
setIframeHeight(obj.contentWindow.document.body.scrollHeight + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// style={{height: iframeHeight}}
|
||||||
|
// ref={iframeRef}
|
||||||
|
<iframe src={src} title={title} onLoad={onLoad} />
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import ReactModal from 'react-modal';
|
||||||
import { openContextMenu } from 'util/context-menu';
|
import { 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' */}
|
||||||
|
|
|
@ -86,13 +86,14 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
|
||||||
const innerRef = useRef(null);
|
const innerRef = useRef(null);
|
||||||
const combinedRef = useCombinedRefs(ref, innerRef, myref);
|
const combinedRef = useCombinedRefs(ref, innerRef, myref);
|
||||||
|
const size = iconSize || (!label && !children) ? 18 : undefined; // Fall back to default
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<span className="button__content">
|
<span className="button__content">
|
||||||
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
{icon && <Icon icon={icon} iconColor={iconColor} size={size} />}
|
||||||
{label && <span className="button__label">{label}</span>}
|
{label && <span className="button__label">{label}</span>}
|
||||||
{children && children}
|
{children && children}
|
||||||
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
|
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={size} />}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
9
ui/component/claimInsufficientCredits/index.js
Normal file
9
ui/component/claimInsufficientCredits/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
|
||||||
|
import ClaimInsufficientCredits from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(ClaimInsufficientCredits);
|
33
ui/component/claimInsufficientCredits/view.jsx
Normal file
33
ui/component/claimInsufficientCredits/view.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
fileInfo: FileListItem,
|
||||||
|
isInsufficientCredits: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ClaimInsufficientCredits(props: Props) {
|
||||||
|
const { isInsufficientCredits, fileInfo } = props;
|
||||||
|
|
||||||
|
if (fileInfo || !isInsufficientCredits) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media__insufficient-credits help--warning">
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it. Check
|
||||||
|
out %reward_link% for free LBC or send more LBC to your wallet.
|
||||||
|
</I18nMessage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClaimInsufficientCredits;
|
|
@ -6,6 +6,7 @@ import classnames from 'classnames';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
import 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);
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ function ClaimUri(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
button="link"
|
||||||
className={classnames('media__uri', { 'media__uri--inline': inline })}
|
className={classnames('media__uri', { 'media__uri--inline': inline })}
|
||||||
button="alt"
|
|
||||||
label={noShortUrl ? uri : shortUrl || uri}
|
label={noShortUrl ? uri : shortUrl || uri}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.writeText(shortUrl || uri);
|
clipboard.writeText(shortUrl || uri);
|
||||||
|
|
|
@ -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,
|
||||||
|
actionIconPadding = true,
|
||||||
|
} = 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
|
||||||
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}
|
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class IconComponent extends React.PureComponent<Props> {
|
||||||
color = this.getIconColor(iconColor);
|
color = this.getIconColor(iconColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconSize = size || 14;
|
const iconSize = size || 16;
|
||||||
|
|
||||||
let tooltipText;
|
let tooltipText;
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
|
|
|
@ -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,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<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!')}
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import {
|
import { makeSelectClaimIsMine, makeSelectFileInfoForUri, makeSelectClaimForUri, doPrepareEdit } from 'lbry-redux';
|
||||||
makeSelectClaimIsMine,
|
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectClaimForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
doPrepareEdit,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import FilePage from './view';
|
import FileActions from './view';
|
||||||
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
|
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
|
||||||
});
|
});
|
||||||
|
@ -27,7 +22,4 @@ const perform = dispatch => ({
|
||||||
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
|
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(FileActions);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FilePage);
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { Node } from 'react';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import { buildURI } from 'lbry-redux';
|
import { buildURI } from 'lbry-redux';
|
||||||
import * as PAGES from '../../constants/pages';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as CS from '../../constants/claim_search';
|
import useIsMobile from 'effects/use-is-mobile';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -16,14 +17,14 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
costInfo: ?{ cost: number },
|
costInfo: ?{ cost: number },
|
||||||
contentType: string,
|
renderMode: string,
|
||||||
supportOption: boolean,
|
supportOption: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileActions(props: Props) {
|
function FileActions(props: Props) {
|
||||||
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, contentType, supportOption, prepareEdit } = props;
|
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, renderMode, supportOption, prepareEdit } = props;
|
||||||
const webShareable =
|
const isMobile = useIsMobile();
|
||||||
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
|
const webShareable = costInfo && costInfo.cost === 0 && RENDER_MODES.WEB_SHAREABLE_MODES.includes(renderMode);
|
||||||
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const { signing_channel: signingChannel } = claim;
|
const { signing_channel: signingChannel } = claim;
|
||||||
|
@ -44,23 +45,16 @@ function FileActions(props: Props) {
|
||||||
editUri = buildURI(uriObject);
|
editUri = buildURI(uriObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
let repostLabel = <span>{__('Repost')}</span>;
|
const ActionWrapper = (props: { children: Node }) =>
|
||||||
if (claim.meta.reposted > 0) {
|
isMobile ? (
|
||||||
repostLabel = (
|
<React.Fragment>{props.children}</React.Fragment>
|
||||||
<Fragment>
|
) : (
|
||||||
{repostLabel}
|
<div className="section__actions section__actions--no-margin">{props.children}</div>
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
label={__('(%count%)', { count: claim.meta.reposted })}
|
|
||||||
navigate={`/$/${PAGES.DISCOVER}?${CS.REPOSTED_URI_KEY}=${encodeURIComponent(uri)}`}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="media__actions">
|
<div className="media__actions">
|
||||||
<div className="section__actions">
|
<ActionWrapper>
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
icon={ICONS.SHARE}
|
icon={ICONS.SHARE}
|
||||||
|
@ -70,7 +64,7 @@ function FileActions(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
icon={ICONS.REPOST}
|
icon={ICONS.REPOST}
|
||||||
label={repostLabel}
|
label={__('Repost %count%', { count: claim.meta.reposted > 0 ? `(${claim.meta.reposted})` : '' })}
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
onClick={() => openModal(MODALS.REPOST, { uri })}
|
onClick={() => openModal(MODALS.REPOST, { uri })}
|
||||||
/>
|
/>
|
||||||
|
@ -95,9 +89,9 @@ function FileActions(props: Props) {
|
||||||
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
|
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ActionWrapper>
|
||||||
|
|
||||||
<div className="section__actions">
|
<ActionWrapper>
|
||||||
<FileDownloadLink uri={uri} />
|
<FileDownloadLink uri={uri} />
|
||||||
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
|
@ -129,7 +123,7 @@ function FileActions(props: Props) {
|
||||||
href={`https://lbry.com/dmca/${claimId}`}
|
href={`https://lbry.com/dmca/${claimId}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ActionWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
<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,15 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
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';
|
||||||
|
|
||||||
|
@ -19,14 +20,13 @@ const select = (state, props) => {
|
||||||
return {
|
return {
|
||||||
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
|
||||||
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 +34,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';
|
||||||
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';
|
||||||
|
@ -30,18 +26,17 @@ import ThreeViewer from 'component/viewers/threeViewer';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: 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 +108,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 +159,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
10
ui/component/fileRenderDownload/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import FileRenderDownload from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(select)(FileRenderDownload));
|
43
ui/component/fileRenderDownload/view.jsx
Normal file
43
ui/component/fileRenderDownload/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
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.FLOATING_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}
|
9
ui/component/fileRenderHeader/index.js
Normal file
9
ui/component/fileRenderHeader/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import ClaimUri from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(ClaimUri);
|
32
ui/component/fileRenderHeader/view.jsx
Normal file
32
ui/component/fileRenderHeader/view.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import * as CS from 'constants/claim_search';
|
||||||
|
import React from 'react';
|
||||||
|
import ClaimUri from 'component/claimUri';
|
||||||
|
import Button from 'component/button';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
claim: ?Claim,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileRenderHeader(props: Props) {
|
||||||
|
const { uri, claim } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ClaimUri uri={uri} />
|
||||||
|
|
||||||
|
{claim.meta.reposted > 0 && (
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
className="media__uri--right"
|
||||||
|
label={__('View %count% reposts', { count: claim.meta.reposted })}
|
||||||
|
navigate={`/$/${PAGES.DISCOVER}?${CS.REPOSTED_URI_KEY}=${encodeURIComponent(uri)}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileRenderHeader;
|
|
@ -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.FLOATING_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,51 @@ 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 && isPlayable) || 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_IN_THIS_APP.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}
|
25
ui/component/fileRenderInline/index.js
Normal file
25
ui/component/fileRenderInline/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectFileInfoForUri } from 'lbry-redux';
|
||||||
|
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||||
|
import {
|
||||||
|
makeSelectFileRenderModeForUri,
|
||||||
|
makeSelectIsPlaying,
|
||||||
|
makeSelectStreamingUrlForUriWebProxy,
|
||||||
|
} from 'redux/selectors/content';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
|
import FileRenderInline from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
|
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
||||||
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||||
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(select, perform)(FileRenderInline));
|
|
@ -1,39 +1,27 @@
|
||||||
// @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,
|
|
||||||
contentType: string,
|
|
||||||
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 +40,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
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);
|
46
ui/component/fileTitle/view.jsx
Normal file
46
ui/component/fileTitle/view.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// @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>
|
||||||
|
<ClaimInsufficientCredits uri={uri} />
|
||||||
|
<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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
|
||||||
makeSelectFileInfoForUri,
|
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
|
||||||
makeSelectContentTypeForUri,
|
|
||||||
makeSelectUriIsStreamable,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
|
||||||
import { makeSelectIsPlaying } from 'redux/selectors/content';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
|
||||||
import FileViewer from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
|
||||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
|
||||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
|
||||||
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withRouter(
|
|
||||||
connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileViewer)
|
|
||||||
);
|
|
|
@ -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">
|
||||||
|
|
32
ui/constants/file_render_modes.js
Normal file
32
ui/constants/file_render_modes.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
export const VIDEO = 'video';
|
||||||
|
export const AUDIO = 'audio';
|
||||||
|
|
||||||
|
export const FLOATING_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 WEB_SHAREABLE_MODES = AUTO_RENDER_MODES.concat(FLOATING_MODES);
|
||||||
|
|
||||||
|
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,45 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Button from 'component/button';
|
import classnames from 'classnames';
|
||||||
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 FileRenderHeader from 'component/fileRenderHeader';
|
||||||
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 +50,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 +74,74 @@ class FilePage extends React.Component<Props> {
|
||||||
markSubscriptionRead(channelUri, uri);
|
markSubscriptionRead(channelUri, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderFilePageLayout(uri: string, mode: string, cost: ?number) {
|
||||||
const { uri, claimIsMine, costInfo, fileInfo, balance, isText } = this.props;
|
if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
|
||||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<FileRenderHeader 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>
|
||||||
|
<FileRenderHeader uri={uri} />
|
||||||
|
<FileTitle uri={uri} />
|
||||||
|
<FileRenderDownload uri={uri} isFree={cost === 0} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RENDER_MODES.TEXT_MODES.includes(mode)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<FileRenderHeader uri={uri} />
|
||||||
|
<FileTitle uri={uri} />
|
||||||
|
<FileRenderInitiator uri={uri} />
|
||||||
|
<FileRenderInline uri={uri} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className="main--file-page">
|
<React.Fragment>
|
||||||
{!fileInfo && insufficientCredits && (
|
<FileRenderHeader 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={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||||
|
{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={
|
||||||
|
<div>
|
||||||
|
<CommentCreate uri={uri} />
|
||||||
|
<CommentsList uri={uri} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,8 @@ export default function SearchPage(props: Props) {
|
||||||
<div className="claim-preview__actions--header">
|
<div className="claim-preview__actions--header">
|
||||||
<ClaimUri uri={uriFromQuery} noShortUrl />
|
<ClaimUri uri={uriFromQuery} noShortUrl />
|
||||||
<Button
|
<Button
|
||||||
|
button="link"
|
||||||
className="media__uri--right"
|
className="media__uri--right"
|
||||||
button="alt"
|
|
||||||
label={__('View top claims for %normalized_uri%', {
|
label={__('View top claims for %normalized_uri%', {
|
||||||
normalized_uri: uriFromQuery,
|
normalized_uri: uriFromQuery,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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,62 @@ import {
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
makeSelectClaimsInChannelForCurrentPageState,
|
makeSelectClaimsInChannelForCurrentPageState,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
makeSelectRecommendedContentForUri,
|
makeSelectRecommendedContentForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
|
selectBalance,
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
parseURI,
|
parseURI,
|
||||||
|
makeSelectContentTypeForUri,
|
||||||
|
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 { 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 +118,105 @@ 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
let makeSelectStreamingUrlForUriWebProxy;
|
||||||
|
// @if TARGET='web'
|
||||||
|
makeSelectStreamingUrlForUriWebProxy = (uri: string) =>
|
||||||
|
createSelector(makeSelectClaimForUri(uri), claim => (claim ? generateStreamUrl(claim.name, claim.claim_id) : null));
|
||||||
|
// @endif
|
||||||
|
// @if TARGET='app'
|
||||||
|
makeSelectStreamingUrlForUriWebProxy = (uri: string) => createSelector(makeSelectStreamingUrlForUri(uri), url => url);
|
||||||
|
// @endif
|
||||||
|
export { makeSelectStreamingUrlForUriWebProxy };
|
||||||
|
|
||||||
|
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,8 +1,3 @@
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: var(--font-weight-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--uri-indicator {
|
.button--uri-indicator {
|
||||||
@extend .button--link;
|
@extend .button--link;
|
||||||
color: var(--color-text-subtitle);
|
color: var(--color-text-subtitle);
|
||||||
|
|
|
@ -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 {
|
.icon__wrapper {
|
||||||
margin-bottom: 0;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-large);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__main-actions {
|
.card__main-actions {
|
||||||
|
@ -177,3 +208,14 @@
|
||||||
margin-left: var(--spacing-small);
|
margin-left: var(--spacing-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__header,
|
||||||
|
.card__body,
|
||||||
|
.card__main-actions {
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ $metadata-z-index: 1;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
.button {
|
.button--alt {
|
||||||
color: #fff;
|
padding: 0 var(--spacing-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,10 +67,6 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-preview__wrapper--channel {
|
|
||||||
background-color: var(--color-card-background-highlighted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-preview__wrapper--notice {
|
.claim-preview__wrapper--notice {
|
||||||
background-color: var(--color-notice);
|
background-color: var(--color-notice);
|
||||||
}
|
}
|
||||||
|
@ -208,6 +204,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-preview-info {
|
.claim-preview-info {
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
|
.comments {
|
||||||
|
padding-top: var(--spacing-large);
|
||||||
|
}
|
||||||
|
|
||||||
.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,48 @@
|
||||||
|
.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-page__md {
|
||||||
|
.card {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card + .file-render {
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
border-bottom-right-radius: var(--border-radius);
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.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 +53,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 +91,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 +173,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,8 @@
|
||||||
@extend .media__subtitle;
|
@extend .media__subtitle;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
align-items: center;
|
||||||
|
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 +80,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
@media (max-width: $breakpoint-small) {
|
||||||
margin-top: 0;
|
justify-content: flex-start;
|
||||||
|
padding-top: var(--spacing-small);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: var(--spacing-small);
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
--color-button-secondary-bg: #395877;
|
--color-button-secondary-bg: #395877;
|
||||||
--color-button-secondary-bg-hover: #4b6d8f;
|
--color-button-secondary-bg-hover: #4b6d8f;
|
||||||
--color-button-secondary-text: #a3c1e0;
|
--color-button-secondary-text: #a3c1e0;
|
||||||
|
--color-button-alt-bg: #4d5660;
|
||||||
|
--color-button-alt-bg-hover: #3e464d;
|
||||||
|
--color-button-alt-text: #e2e9f0;
|
||||||
--color-header-button: var(--color-link-icon);
|
--color-header-button: var(--color-link-icon);
|
||||||
|
|
||||||
// Color
|
// Color
|
||||||
|
|
|
@ -819,9 +819,10 @@
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.18.0"
|
scheduler "^0.18.0"
|
||||||
|
|
||||||
"@lbry/components@^3.0.12":
|
"@lbry/components@^4.0.1":
|
||||||
version "3.0.12"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-3.0.12.tgz#9ba4598edf26496060a97023ca0132d39c388c3b"
|
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.0.1.tgz#8dcf7348920383d854c0db640faaf1ac5a72f7ef"
|
||||||
|
integrity sha512-vY84ziZ9EaXoezDBK2VsajvXcSPXDV0fr1VWn2w0iHkGa756RWvNySpnqaKMZH+myK12mvNNc/NkGIW5oO7+5w==
|
||||||
|
|
||||||
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
|
|
Loading…
Reference in a new issue