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:
Jeremy Kauffman 2020-04-01 14:43:50 -04:00 committed by GitHub
parent 86c75f13b6
commit 872259b73a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1157 additions and 1194 deletions

View file

@ -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",

View file

@ -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.

View 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);

View 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} />
);
}

View file

@ -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' */}

View file

@ -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>
); );

View 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);

View 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;

View file

@ -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>
); );
} }

View file

@ -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);

View file

@ -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);

View file

@ -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 })}

View file

@ -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) {

View file

@ -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', {

View file

@ -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!')}

View file

@ -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);

View file

@ -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>
); );
} }

View file

@ -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>
); );
} }

View file

@ -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}

View file

@ -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);

View file

@ -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,
})} })}
> >

View 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));

View 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 />} />;
}

View file

@ -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)
);

View file

@ -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}

View 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);

View 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;

View file

@ -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);

View file

@ -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}

View 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));

View file

@ -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')} />;
} }

View 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);

View 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;

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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>
); );
} }

View file

@ -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;
} }

View file

@ -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)
);

View file

@ -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.')}

View file

@ -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()}

View file

@ -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 }} />}

View file

@ -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}`} />

View file

@ -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>
); );
} }

View file

@ -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">

View 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]))
);

View file

@ -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);

View file

@ -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>
); );
} }

View file

@ -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>
); );
} }

View file

@ -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,
})} })}

View file

@ -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>

View file

@ -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;
} }
); );

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
} }
} }

View file

@ -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 {

View file

@ -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);
} }
} }

View file

@ -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 {

View file

@ -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;

View file

@ -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%;
}
}

View file

@ -1,4 +1,8 @@
.markdown-preview { .markdown-preview {
> :first-child {
margin-top: 0;
}
> *:last-child { > *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -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);
}
}
} }

View file

@ -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 {

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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"