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",
"@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.8",
"@lbry/components": "^3.0.12",
"@lbry/components": "^4.0.1",
"@reach/menu-button": "0.7.4",
"@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5",

View file

@ -335,7 +335,6 @@
"credits": "credits",
"No channel name after @.": "No channel name after @.",
"View channel": "View channel",
"Add to your library": "Add to your library",
"Web link": "Web link",
"Facebook": "Facebook",
"Twitter": "Twitter",
@ -842,7 +841,6 @@
"Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.": "Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.",
"Loading 3D model.": "Loading 3D model.",
"Click here": "Click here",
"PDF opened externally. %click_here% to open it again.": "PDF opened externally. %click_here% to open it again.",
"Wallet Server": "Wallet Server",
"lbry.tv wallet servers": "lbry.tv wallet servers",
"Custom wallet servers": "Custom wallet servers",

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 useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FloatingViewer from 'component/floatingViewer';
import FileRenderFloating from 'component/fileRenderFloating';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
@ -286,7 +286,7 @@ function App(props: Props) {
<React.Fragment>
<Router />
<ModalRouter />
<FloatingViewer pageUri={uri} />
<FileRenderFloating pageUri={uri} />
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
{/* @if TARGET='app' */}

View file

@ -86,13 +86,14 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
const innerRef = useRef(null);
const combinedRef = useCombinedRefs(ref, innerRef, myref);
const size = iconSize || (!label && !children) ? 18 : undefined; // Fall back to default
const 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>}
{children && children}
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={size} />}
</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 Spinner from 'component/spinner';
import { FormField } from 'component/common/form';
import Card from 'component/common/card';
import usePersistedState from 'effects/use-persisted-state';
const SORT_NEW = 'new';
@ -161,9 +162,9 @@ export default function ClaimList(props: Props) {
</ul>
)}
{!timedOut && urisLength === 0 && !loading && (
<div className="card--section main--empty empty">{empty || __('No results')}</div>
<div className="empty empty--centered">{empty || __('No results')}</div>
)}
{timedOut && timedOutMessage && <div className="card--section main--empty empty">{timedOutMessage}</div>}
{timedOut && timedOutMessage && <div className="empty empty--centered">{timedOutMessage}</div>}
</section>
);
}

View file

@ -11,11 +11,10 @@ import {
selectBlockedChannels,
selectChannelIsBlocked,
doFileGet,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectHasVisitedUri, makeSelectStreamingUrlForUriWebProxy } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import ClaimPreview from './view';
@ -34,7 +33,7 @@ const select = (state, props) => ({
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
});
const perform = dispatch => ({
@ -42,7 +41,4 @@ const perform = dispatch => ({
getFile: uri => dispatch(doFileGet(uri, false)),
});
export default connect(
select,
perform
)(ClaimPreview);
export default connect(select, perform)(ClaimPreview);

View file

@ -17,8 +17,8 @@ function ClaimUri(props: Props) {
return (
<Button
button="link"
className={classnames('media__uri', { 'media__uri--inline': inline })}
button="alt"
label={noShortUrl ? uri : shortUrl || uri}
onClick={() => {
clipboard.writeText(shortUrl || uri);

View file

@ -11,26 +11,47 @@ type Props = {
actions?: string | Node,
icon?: string,
className?: string,
isPageTitle?: boolean,
isBodyTable?: boolean,
actionIconPadding?: boolean,
};
export default function Card(props: Props) {
const { title, subtitle, body, actions, icon, className, actionIconPadding = true } = props;
const {
title,
subtitle,
body,
actions,
icon,
className,
isPageTitle = false,
isBodyTable = false,
actionIconPadding = true,
} = props;
return (
<section className={classnames(className, 'card')}>
{(title || subtitle) && (
<div className="card__header">
<div className="section__flex">
{icon && <Icon sectionIcon icon={icon} />}
<div>
<h2 className="section__title">{title}</h2>
{subtitle && <div className="section__subtitle">{subtitle}</div>}
</div>
{icon && <Icon sectionIcon icon={icon} />}
<div>
{isPageTitle && <h1 className="card__title">{title}</h1>}
{!isPageTitle && <h2 className="card__title">{title}</h2>}
{subtitle && <div className="card__subtitle">{subtitle}</div>}
</div>
</div>
)}
{body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>}
{body && (
<div
className={classnames('card__body', {
'card__body--with-icon': icon,
'card__body--no-title': !title && !subtitle,
'card__body--table': isBodyTable,
})}
>
{body}
</div>
)}
{actions && (
<div
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);
}
const iconSize = size || 14;
const iconSize = size || 16;
let tooltipText;
if (tooltip) {

View file

@ -10,17 +10,24 @@ type Props = {
actionText: string,
href?: string,
type?: string,
inline?: boolean,
onClick?: () => void,
onClose?: () => void,
};
export default function Nag(props: Props) {
const { message, actionText, href, onClick, onClose, type } = props;
const { message, actionText, href, onClick, onClose, type, inline } = props;
const buttonProps = onClick ? { onClick } : { href };
return (
<div className={classnames('nag', { 'nag--helpful': type === 'helpful', 'nag--error': type === 'error' })}>
<div
className={classnames('nag', {
'nag--helpful': type === 'helpful',
'nag--error': type === 'error',
'nag--inline': inline,
})}
>
<div className="nag__message">{message}</div>
<Button
className={classnames('nag__button', {

View file

@ -73,7 +73,7 @@ class ErrorBoundary extends React.Component<Props, State> {
if (hasError) {
return (
<div className="main main--empty">
<div className="main main--full-width main--empty">
<Yrbl
type="sad"
title={__('Aw shucks!')}

View file

@ -1,23 +1,18 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
} from 'lbry-redux';
import { makeSelectClaimIsMine, makeSelectFileInfoForUri, makeSelectClaimForUri, doPrepareEdit } 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';
import FileActions from './view';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
});
@ -27,7 +22,4 @@ const perform = dispatch => ({
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
});
export default connect(
select,
perform
)(FilePage);
export default connect(select, perform)(FileActions);

View file

@ -1,12 +1,13 @@
// @flow
import type { Node } from 'react';
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import React from 'react';
import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink';
import { buildURI } from 'lbry-redux';
import * as PAGES from '../../constants/pages';
import * as CS from '../../constants/claim_search';
import * as RENDER_MODES from 'constants/file_render_modes';
import useIsMobile from 'effects/use-is-mobile';
type Props = {
uri: string,
@ -16,14 +17,14 @@ type Props = {
claimIsMine: boolean,
fileInfo: FileListItem,
costInfo: ?{ cost: number },
contentType: string,
renderMode: string,
supportOption: boolean,
};
function FileActions(props: Props) {
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, contentType, supportOption, prepareEdit } = props;
const webShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, renderMode, supportOption, prepareEdit } = props;
const isMobile = useIsMobile();
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 claimId = claim && claim.claim_id;
const { signing_channel: signingChannel } = claim;
@ -44,23 +45,16 @@ function FileActions(props: Props) {
editUri = buildURI(uriObject);
}
let repostLabel = <span>{__('Repost')}</span>;
if (claim.meta.reposted > 0) {
repostLabel = (
<Fragment>
{repostLabel}
<Button
button="alt"
label={__('(%count%)', { count: claim.meta.reposted })}
navigate={`/$/${PAGES.DISCOVER}?${CS.REPOSTED_URI_KEY}=${encodeURIComponent(uri)}`}
/>
</Fragment>
const ActionWrapper = (props: { children: Node }) =>
isMobile ? (
<React.Fragment>{props.children}</React.Fragment>
) : (
<div className="section__actions section__actions--no-margin">{props.children}</div>
);
}
return (
<div className="media__actions">
<div className="section__actions">
<ActionWrapper>
<Button
button="alt"
icon={ICONS.SHARE}
@ -70,7 +64,7 @@ function FileActions(props: Props) {
<Button
button="alt"
icon={ICONS.REPOST}
label={repostLabel}
label={__('Repost %count%', { count: claim.meta.reposted > 0 ? `(${claim.meta.reposted})` : '' })}
requiresAuth={IS_WEB}
onClick={() => openModal(MODALS.REPOST, { uri })}
/>
@ -95,9 +89,9 @@ function FileActions(props: Props) {
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
</ActionWrapper>
<div className="section__actions">
<ActionWrapper>
<FileDownloadLink uri={uri} />
{claimIsMine && (
@ -129,7 +123,7 @@ function FileActions(props: Props) {
href={`https://lbry.com/dmca/${claimId}`}
/>
)}
</div>
</ActionWrapper>
</div>
);
}

View file

@ -5,6 +5,7 @@ import Button from 'component/button';
import Expandable from 'component/expandable';
import path from 'path';
import ClaimTags from 'component/claimTags';
import Card from 'component/common/card';
type Props = {
uri: string,
@ -42,73 +43,78 @@ class FileDetails extends PureComponent<Props> {
return (
<Fragment>
<Expandable>
{description && (
<div className="media__info-text">
<MarkdownPreview content={description} />
</div>
)}
<ClaimTags uri={uri} type="large" />
<table className="table table--condensed table--fixed table--file-details">
<tbody>
<tr>
<td> {__('Content Type')}</td>
<td>{mediaType}</td>
</tr>
{fileSize && (
<tr>
<td> {__('File Size')}</td>
<td>{fileSize}</td>
</tr>
<Card
title={__('Details')}
body={
<Expandable>
{description && (
<div className="media__info-text">
<MarkdownPreview content={description} />
</div>
)}
<tr>
<td> {__('Bid Amount')}</td>
<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>
<ClaimTags uri={uri} type="large" />
<table className="table table--condensed table--fixed table--file-details">
<tbody>
<tr>
<td> {__('Content Type')}</td>
<td>{mediaType}</td>
</tr>
{fileSize && (
<tr>
<td> {__('File Size')}</td>
<td>{fileSize}</td>
</tr>
)}
<tr>
<td> {__('Bid Amount')}</td>
<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 && (
<tr>
<td>{__('Languages')}</td>
<td>{languages.join(' ')}</td>
</tr>
)}
<tr>
<td>{__('License')}</td>
<td>{license}</td>
</tr>
{downloadPath && (
<tr>
<td>{__('Downloaded to')}</td>
<td>
{/* {downloadPath.replace(/(.{10})/g, '$1\u200b')} */}
<Button
button="link"
className="button--download-link"
onClick={() => {
if (downloadPath) {
openFolder(downloadPath);
}
}}
label={downloadNote || downloadPath.replace(/(.{10})/g, '$1\u200b')}
/>
</td>
</tr>
)}
</tbody>
</table>
</Expandable>
{languages && (
<tr>
<td>{__('Languages')}</td>
<td>{languages.join(' ')}</td>
</tr>
)}
<tr>
<td>{__('License')}</td>
<td>{license}</td>
</tr>
{downloadPath && (
<tr>
<td>{__('Downloaded to')}</td>
<td>
{/* {downloadPath.replace(/(.{10})/g, '$1\u200b')} */}
<Button
button="link"
className="button--download-link"
onClick={() => {
if (downloadPath) {
openFolder(downloadPath);
}
}}
label={downloadNote || downloadPath.replace(/(.{10})/g, '$1\u200b')}
/>
</td>
</tr>
)}
</tbody>
</table>
</Expandable>
}
/>
</Fragment>
);
}

View file

@ -11,13 +11,14 @@ type Props = {
claimIsMine: boolean,
downloading: boolean,
loading: boolean,
isStreamable: boolean,
fileInfo: ?FileListItem,
openModal: (id: string, { path: string }) => void,
pause: () => void,
download: string => void,
triggerViewEvent: string => void,
costInfo: ?{ cost: string },
buttonType: ?string,
showLabel: ?boolean,
hideOpenButton: boolean,
hideDownloadStatus: boolean,
};
@ -35,6 +36,8 @@ function FileDownloadLink(props: Props) {
claim,
triggerViewEvent,
costInfo,
buttonType = 'alt',
showLabel = false,
hideOpenButton = false,
hideDownloadStatus = false,
} = props;
@ -73,10 +76,12 @@ function FileDownloadLink(props: Props) {
}
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
const openLabel = __('Open file');
return hideOpenButton ? null : (
<Button
button="alt"
title={__('Open file')}
button={buttonType}
title={openLabel}
label={showLabel ? openLabel : null}
icon={ICONS.EXTERNAL}
onClick={() => {
pause();
@ -86,11 +91,14 @@ function FileDownloadLink(props: Props) {
);
}
const label = IS_WEB ? __('Download') : __('Download to your Library');
return (
<Button
button="alt"
title={IS_WEB ? __('Download') : __('Add to your library')}
button={buttonType}
title={label}
icon={ICONS.DOWNLOAD}
label={showLabel ? label : null}
onClick={handleDownload}
// @if TARGET='web'
download={fileName}

View file

@ -3,14 +3,15 @@ import {
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectContentTypeForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectDownloadPathForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsText } from 'redux/selectors/content';
import {
makeSelectFileRenderModeForUri,
makeSelectFileExtensionForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { doSetPlayingUri } from 'redux/actions/content';
import FileRender from './view';
@ -19,14 +20,13 @@ const select = (state, props) => {
return {
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
claim: makeSelectClaimForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
fileName: makeSelectFileNameForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
autoplay: autoplay,
isText: makeSelectIsText(props.uri)(state),
};
};
@ -34,7 +34,4 @@ const perform = dispatch => ({
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
});
export default connect(
select,
perform
)(FileRender);
export default connect(select, perform)(FileRender);

View file

@ -1,9 +1,9 @@
// @flow
import { URL } from 'config';
import { remote } from 'electron';
import React, { Suspense, Fragment } from 'react';
import React, { Suspense } from 'react';
import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen';
import * as RENDER_MODES from 'constants/file_render_modes';
import VideoViewer from 'component/viewers/videoViewer';
import ImageViewer from 'component/viewers/imageViewer';
import AppViewer from 'component/viewers/appViewer';
@ -11,18 +11,14 @@ import Button from 'component/button';
import { withRouter } from 'react-router-dom';
import AutoplayCountdown from 'component/autoplayCountdown';
import { formatLbryUrlForWeb } from 'util/url';
// @if TARGET='web'
import { generateStreamUrl } from 'util/lbrytv';
// @endif
import path from 'path';
import fs from 'fs';
import Yrbl from 'component/yrbl';
import DocumentViewer from 'component/viewers/documentViewer';
import PdfViewer from 'component/viewers/pdfViewer';
import HtmlViewer from 'component/viewers/htmlViewer';
// @if TARGET='app'
// should match
import DocxViewer from 'component/viewers/docxViewer';
import ComicBookViewer from 'component/viewers/comicBookViewer';
import ThreeViewer from 'component/viewers/threeViewer';
@ -30,18 +26,17 @@ import ThreeViewer from 'component/viewers/threeViewer';
type Props = {
uri: string,
mediaType: string,
isText: true,
streamingUrl: string,
embedded?: boolean,
contentType: string,
claim: StreamClaim,
currentTheme: string,
downloadPath: string,
fileName: string,
fileExtension: string,
autoplay: boolean,
setPlayingUri: (string | null) => void,
currentlyFloating: boolean,
renderMode: string,
thumbnail: string,
};
@ -113,114 +108,50 @@ class FileRender extends React.PureComponent<Props, State> {
}
renderViewer() {
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
const fileType = fileName && path.extname(fileName).substring(1);
const { currentTheme, contentType, downloadPath, fileExtension, streamingUrl, uri, renderMode } = this.props;
const source = streamingUrl;
// Ideally the lbrytv api server would just replace the streaming_url returned by the sdk so we don't need this check
// https://github.com/lbryio/lbrytv/issues/51
const source = IS_WEB ? generateStreamUrl(claim.name, claim.claim_id) : streamingUrl;
// Human-readable files (scripts and plain-text files)
const readableFiles = ['text', 'document', 'script'];
// Supported mediaTypes
const mediaTypes = {
// @if TARGET='app'
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
application: <AppViewer uri={uri} />,
// @endif
video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
image: <ImageViewer uri={uri} source={source} />,
// Add routes to viewer...
};
// Supported contentTypes
const contentTypes = {
'application/x-ext-mkv': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
),
'video/x-matroska': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
),
'application/pdf': <PdfViewer source={downloadPath || source} />,
'text/html': <HtmlViewer source={downloadPath || source} />,
'text/htm': <HtmlViewer source={downloadPath || source} />,
};
// Supported fileType
const fileTypes = {
// @if TARGET='app'
docx: <DocxViewer source={downloadPath} />,
// @endif
// Add routes to viewer...
};
// Check for a valid fileType, mediaType, or contentType
let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType] || contentTypes[contentType];
// Check for Human-readable files
if (!viewer && readableFiles.includes(mediaType)) {
viewer = (
<DocumentViewer
source={{
// @if TARGET='app'
file: options => fs.createReadStream(downloadPath, options),
// @endif
stream: source,
fileType,
contentType,
}}
theme={currentTheme}
/>
);
switch (renderMode) {
case RENDER_MODES.AUDIO:
case RENDER_MODES.VIDEO:
return <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />;
case RENDER_MODES.IMAGE:
return <ImageViewer uri={uri} source={source} />;
case RENDER_MODES.HTML:
return <HtmlViewer source={downloadPath || source} />;
case RENDER_MODES.DOCUMENT:
case RENDER_MODES.MARKDOWN:
return (
<DocumentViewer
source={{
// @if TARGET='app'
file: options => fs.createReadStream(downloadPath, options),
// @endif
stream: source,
fileExtension,
contentType,
}}
renderMode={renderMode}
theme={currentTheme}
/>
);
case RENDER_MODES.DOCX:
return <DocxViewer source={downloadPath} />;
case RENDER_MODES.PDF:
return <PdfViewer source={downloadPath || source} />;
case RENDER_MODES.CAD:
return <ThreeViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
case RENDER_MODES.COMIC:
return <ComicBookViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
case RENDER_MODES.APPLICATION:
return <AppViewer uri={uri} />;
}
// @if TARGET='web'
// temp workaround to disabled paid content on web
if (claim && claim.value.fee && Number(claim.value.fee.amount) > 0) {
const paidMessage = __(
'Currently, only free content is available on lbry.tv. Try viewing it in the desktop app.'
);
const paid = <LoadingScreen status={paidMessage} spinner={false} />;
return paid;
}
// @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;
return null;
}
render() {
const { isText, uri, currentlyFloating, embedded } = this.props;
const { uri, currentlyFloating, embedded, renderMode } = this.props;
const { showAutoplayCountdown, showEmbededMessage } = this.state;
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
@ -228,7 +159,7 @@ class FileRender extends React.PureComponent<Props, State> {
<div
className={classnames({
'file-render': !embedded,
'file-render--document': isText && !embedded,
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
'file-render__embed': embedded,
})}
>

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 { connect } from 'react-redux';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable,
makeSelectTitleForUri,
} from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import {
makeSelectIsPlaying,
makeSelectShouldObscurePreview,
selectPlayingUri,
makeSelectIsText,
makeSelectFileRenderModeForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
import FileViewer from './view';
import FileRenderFloating from './view';
const select = (state, props) => {
const uri = selectPlayingUri(state);
return {
uri,
title: makeSelectTitleForUri(uri)(state),
thumbnail: makeSelectThumbnailForUri(uri)(state),
mediaType: makeSelectMediaTypeForUri(uri)(state),
contentType: makeSelectContentTypeForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
isPlaying: makeSelectIsPlaying(uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
isStreamable: makeSelectUriIsStreamable(uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
isText: makeSelectIsText(uri)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
};
};
@ -46,9 +33,4 @@ const perform = dispatch => ({
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(
connect(
select,
perform
)(FileViewer)
);
export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -1,5 +1,6 @@
// @flow
import * as ICONS from 'constants/icons';
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useState, useEffect } from 'react';
import Button from 'component/button';
import classnames from 'classnames';
@ -8,24 +9,17 @@ import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state';
import usePrevious from 'effects/use-previous';
import { FILE_WRAPPER_CLASS } from 'component/layoutWrapperFile/view';
import { FILE_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip';
import { onFullscreenChange } from 'util/full-screen';
import useIsMobile from 'effects/use-is-mobile';
type Props = {
mediaType: string,
contentType: string,
isText: boolean,
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
obscurePreview: boolean,
insufficientCredits: boolean,
isStreamable: boolean,
thumbnail?: string,
streamingUrl?: string,
floatingPlayer: boolean,
pageUri: ?string,
@ -33,25 +27,23 @@ type Props = {
floatingPlayerEnabled: boolean,
clearPlayingUri: () => void,
triggerAnalyticsView: (string, number) => Promise<any>,
renderMode: string,
claimRewards: () => void,
};
export default function FileViewer(props: Props) {
export default function FloatingViewer(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
isStreamable,
pageUri,
title,
clearPlayingUri,
floatingPlayerEnabled,
triggerAnalyticsView,
claimRewards,
mediaType,
contentType,
isText,
renderMode,
} = props;
const isMobile = useIsMobile();
const [playTime, setPlayTime] = useState();
@ -60,40 +52,17 @@ export default function FileViewer(props: Props) {
x: -25,
y: window.innerHeight - 400,
});
const inline = pageUri === uri;
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const isReadyToPlay =
(IS_WEB && (isStreamable || webStreamOnly || forceVideo)) ||
((isStreamable || forceVideo) && streamingUrl) ||
(fileInfo && fileInfo.completed);
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
const loadingMessage =
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
: __('Loading');
const previousUri = usePrevious(uri);
const isNewView = uri && previousUri !== uri && isPlaying;
const [hasRecordedView, setHasRecordedView] = useState(false);
useEffect(() => {
if (isNewView) {
setPlayTime(Date.now());
}
}, [isNewView, uri]);
useEffect(() => {
if (playTime && isReadyToPlay && !hasRecordedView) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart).then(() => {
claimRewards();
setHasRecordedView(false); // This is a terrible variable name, rename this
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
useEffect(() => {
function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
@ -115,6 +84,27 @@ export default function FileViewer(props: Props) {
};
}, [setFileViewerRect, inline]);
useEffect(() => {
if (isNewView) {
setPlayTime(Date.now());
}
}, [isNewView, uri]);
useEffect(() => {
if (playTime && isReadyToPlay && !hasRecordedView) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart).then(() => {
claimRewards();
setHasRecordedView(false); // This is a terrible variable name, rename this
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
if (!isPlayable || !isPlaying || !uri || (!inline && (isMobile || !floatingPlayerEnabled))) {
return null;
}
function handleDrag(e, ui) {
const { x, y } = position;
const newX = x + ui.deltaX;
@ -125,16 +115,6 @@ export default function FileViewer(props: Props) {
});
}
const hidePlayer =
isText ||
!isPlaying ||
!uri ||
(!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
if (hidePlayer) {
return null;
}
return (
<Draggable
onDrag={handleDrag}

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 { connect } from 'react-redux';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable,
makeSelectClaimForUri,
} from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { withRouter } from 'react-router';
import {
makeSelectIsPlaying,
makeSelectShouldObscurePreview,
selectPlayingUri,
makeSelectCanAutoplay,
makeSelectIsText,
makeSelectInsufficientCreditsForUri,
makeSelectStreamingUrlForUriWebProxy,
makeSelectFileRenderModeForUri,
} from 'redux/selectors/content';
import FileViewer from './view';
import FileRenderInitiator from './view';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state),
playingUri: selectPlayingUri(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
isAutoPlayable: makeSelectCanAutoplay(props.uri)(state),
isText: makeSelectIsText(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
});
@ -48,7 +39,4 @@ const perform = dispatch => ({
},
});
export default connect(
select,
perform
)(FileViewer);
export default withRouter(connect(select, perform)(FileRenderInitiator));

View file

@ -5,78 +5,52 @@
// while a file is currently being viewed
import React, { useEffect, useCallback } from 'react';
import classnames from 'classnames';
import * as PAGES from 'constants/pages';
import * as RENDER_MODES from 'constants/file_render_modes';
import Button from 'component/button';
import isUserTyping from 'util/detect-typing';
import Yrbl from 'component/yrbl';
import I18nMessage from 'component/i18nMessage';
import { generateDownloadUrl } from 'util/lbrytv';
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
import Nag from 'component/common/nag';
const SPACE_BAR_KEYCODE = 32;
type Props = {
play: string => void,
mediaType: string,
isText: boolean,
contentType: string,
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
history: { push: string => void },
obscurePreview: boolean,
insufficientCredits: boolean,
isStreamable: boolean,
thumbnail?: string,
autoplay: boolean,
hasCostInfo: boolean,
costInfo: any,
isAutoPlayable: boolean,
inline: boolean,
renderMode: string,
claim: StreamClaim,
};
export default function FileViewerInitiator(props: Props) {
export default function FileRenderInitiator(props: Props) {
const {
play,
mediaType,
isText,
contentType,
isPlaying,
fileInfo,
uri,
obscurePreview,
insufficientCredits,
history,
thumbnail,
autoplay,
isStreamable,
renderMode,
hasCostInfo,
costInfo,
isAutoPlayable,
claim,
} = props;
const cost = costInfo && costInfo.cost;
const forceVideo = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
const isImage = mediaType === 'image';
const isFree = hasCostInfo && cost === 0;
const fileStatus = fileInfo && fileInfo.status;
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true;
const { name, claim_id: claimId, value } = claim;
const fileName = value && value.source && value.source.name;
const downloadUrl = generateDownloadUrl(name, claimId);
function getTitle() {
let message = __('Unsupported File');
// @if TARGET='web'
if (cost) {
message = __('Paid Content Not Supported on lbry.tv');
} else {
message = __("We're not quite ready to display this file on lbry.tv yet");
}
// @endif
return message;
}
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
// Wrap this in useCallback because we need to use it to the keyboard effect
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
@ -112,45 +86,51 @@ export default function FileViewerInitiator(props: Props) {
useEffect(() => {
const videoOnPage = document.querySelector('video');
if (((autoplay && !videoOnPage && isAutoPlayable) || isText || isImage) && hasCostInfo && cost === 0) {
if (isFree && ((autoplay && !videoOnPage && isPlayable) || RENDER_MODES.AUTO_RENDER_MODES.includes(renderMode))) {
viewFile();
}
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost, isText, isImage]);
}, [autoplay, viewFile, isFree, renderMode]);
/*
once content is playing, let the appropriate <FileRender> take care of it...
but for playables, always render so area can be used to fill with floating player
*/
if (isPlaying && !isPlayable) {
return null;
}
const showAppNag = IS_WEB && (!isFree || RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode));
const disabled = showAppNag || (!fileInfo && insufficientCredits);
return (
<div
disabled={!hasCostInfo}
style={!obscurePreview && supported && thumbnail && !isPlaying ? { backgroundImage: `url("${thumbnail}")` } : {}}
onClick={supported ? viewFile : undefined}
className={classnames({
content__cover: supported,
'content__cover--disabled': !supported,
'content__cover--hidden-for-text': isText,
onClick={disabled ? undefined : viewFile}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'content__cover--disabled': disabled,
'card__media--nsfw': obscurePreview,
'card__media--disabled': supported && !fileInfo && insufficientCredits,
})}
>
{!supported && (
<Yrbl
type="happy"
title={getTitle()}
subtitle={
<I18nMessage
tokens={{
download_the_app: <Button button="link" label={__('download the app')} href="https://lbry.com/get" />,
download_this_file: (
<Button button="link" label={__('download this file')} download={fileName} href={downloadUrl} />
),
}}
>
Good news, though! You can %download_the_app% and gain access to everything, or %download_this_file% and
view it on your device.
</I18nMessage>
}
{showAppNag && (
<Nag
type="helpful"
inline
message={__('This content requires LBRY Desktop to display.')}
actionText={__('Get the App')}
href="https://lbry.com/get"
/>
)}
{!isPlaying && supported && (
{insufficientCredits && !showAppNag && (
<Nag
type="helpful"
inline
message={__('You need more credits to purchase this.')}
actionText={__('Open Rewards')}
onClick={() => history.push(`/$/${PAGES.REWARDS}`)}
/>
)}
{!disabled && (
<Button
onClick={viewFile}
iconSize={30}

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
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import FileRender from 'component/fileRender';
import usePrevious from 'effects/use-previous';
import LoadingScreen from 'component/common/loading-screen';
type Props = {
mediaType: string,
contentType: string,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
isStreamable: boolean,
renderMode: string,
streamingUrl?: string,
triggerAnalyticsView: (string, number) => Promise<any>,
claimRewards: () => void,
};
export default function TextViewer(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
isStreamable,
triggerAnalyticsView,
claimRewards,
mediaType,
contentType,
} = props;
export default function FileRenderInline(props: Props) {
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, claimRewards } = props;
const [playTime, setPlayTime] = useState();
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const previousUri = usePrevious(uri);
const isNewView = uri && previousUri !== uri && isPlaying;
const [hasRecordedView, setHasRecordedView] = useState(false);
const isReadyToPlay = (IS_WEB && (isStreamable || streamingUrl || webStreamOnly)) || (fileInfo && fileInfo.completed);
const isReadyToPlay = streamingUrl || (fileInfo && fileInfo.completed);
useEffect(() => {
if (isNewView) {
@ -52,9 +40,9 @@ export default function TextViewer(props: Props) {
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
return (
<div className={classnames('content__viewersss')}>
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
</div>
);
if (!isPlaying) {
return null;
}
return isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={__('Preparing your content')} />;
}

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 { makeSelectViewCountForUri } from 'lbryinc';
import { doFetchViewCount, makeSelectViewCountForUri } from 'lbryinc';
import FileViewCount from './view';
import { makeSelectClaimForUri } from 'lbry-redux';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state),
});
export default connect(select)(FileViewCount);
const perform = dispatch => ({
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
});
export default connect(select, perform)(FileViewCount);

View file

@ -1,13 +1,22 @@
// @flow
import React from 'react';
import React, { useEffect } from 'react';
import HelpLink from 'component/common/help-link';
type Props = {
claim: StreamClaim,
fetchViewCount: string => void,
uri: string,
viewCount: string,
};
function FileViewCount(props: Props) {
const { viewCount } = props;
const { claim, uri, fetchViewCount, viewCount } = props;
useEffect(() => {
if (claim && claim.claim_id) {
fetchViewCount(claim.claim_id);
}
}, [fetchViewCount, uri, claim]);
return (
<span>

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.
File upload is carried out in the background by that function.
*/
import React, { useEffect, Fragment } from 'react';
import React, { useEffect } from 'react';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
import Button from 'component/button';
@ -141,39 +141,39 @@ function PublishForm(props: Props) {
}, [name, channel, resolveUri, updatePublishForm]);
return (
<Fragment>
<div className="card-stack">
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
{!publishing && (
<div className={classnames({ 'card--disabled': formDisabled })}>
<PublishText disabled={formDisabled} />
<Card actions={<SelectThumbnail />} />
<TagsSelect
suggestMature
disableAutoFocus
hideHeader
label={__('Selected Tags')}
empty={__('No tags added')}
limitSelect={TAGS_LIMIT}
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.'
)}
placeholder={__('gaming, crypto')}
onSelect={newTags => {
const validatedTags = [];
newTags.forEach(newTag => {
if (!tags.some(tag => tag.name === newTag.name)) {
validatedTags.push(newTag);
}
});
updatePublishForm({ tags: [...tags, ...validatedTags] });
}}
onRemove={clickedTag => {
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
updatePublishForm({ tags: newTags });
}}
tagsChosen={tags}
/>
<TagsSelect
suggestMature
disableAutoFocus
hideHeader
label={__('Selected Tags')}
empty={__('No tags added')}
limitSelect={TAGS_LIMIT}
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.'
)}
placeholder={__('gaming, crypto')}
onSelect={newTags => {
const validatedTags = [];
newTags.forEach(newTag => {
if (!tags.some(tag => tag.name === newTag.name)) {
validatedTags.push(newTag);
}
});
updatePublishForm({ tags: [...tags, ...validatedTags] });
}}
onRemove={clickedTag => {
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
updatePublishForm({ tags: newTags });
}}
tagsChosen={tags}
/>
<Card
actions={
@ -209,7 +209,7 @@ function PublishForm(props: Props) {
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
</p>
</section>
</Fragment>
</div>
);
}

View file

@ -11,7 +11,6 @@ type Options = {
type Props = {
uri: string,
claim: ?StreamClaim,
claimId: string,
recommendedContent: Array<string>,
isSearching: boolean,
search: (string, Options) => void,
@ -43,10 +42,10 @@ export default class RecommendedContent extends React.PureComponent<Props> {
}
getRecommendedContent() {
const { claim, search, mature, claimId } = this.props;
const { claim, search, mature } = this.props;
if (claim && claim.value && claim.value) {
const options: Options = { size: 20, related_to: claimId, isBackgroundSearch: true };
if (claim && claim.value && claim.claim_id) {
const options: Options = { size: 20, related_to: claim.claim_id, isBackgroundSearch: true };
if (claim && !mature) {
options['nsfw'] = false;
}

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]);
return (
<div className="content__cover--disabled">
<div className="content__cover--none">
<Yrbl
title={__('Sorry')}
subtitle={__('Games and apps are currently disabled due to potential security concerns.')}

View file

@ -3,11 +3,14 @@
import React from 'react';
import LoadingScreen from 'component/common/loading-screen';
import MarkdownPreview from 'component/common/markdown-preview';
import Card from 'component/common/card';
import CodeViewer from 'component/viewers/codeViewer';
import * as RENDER_MODES from 'constants/file_render_modes';
import * as https from 'https';
type Props = {
theme: string,
renderMode: string,
source: {
file: (?string) => any,
stream: string,
@ -79,20 +82,15 @@ class DocumentViewer extends React.PureComponent<Props, State> {
}
renderDocument() {
let viewer = null;
const { content } = this.state;
const { source, theme } = this.props;
const { fileType, contentType } = source;
const markdownType = ['md', 'markdown'];
if (markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md') {
// Render markdown
viewer = <MarkdownPreview content={content} />;
} else {
// Render plain text
viewer = <CodeViewer value={content} contentType={contentType} theme={theme} />;
}
const { source, theme, renderMode } = this.props;
const { contentType } = source;
return viewer;
return renderMode === RENDER_MODES.MARKDOWN ? (
<Card body={<MarkdownPreview content={content} />} />
) : (
<CodeViewer value={content} contentType={contentType} theme={theme} />
);
}
render() {
@ -101,7 +99,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer--document">
<div className="file-render__viewer file-render__viewer--document">
{loading && !error && <div className="placeholder--text-document" />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && this.renderDocument()}

View file

@ -58,7 +58,7 @@ class DocxViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer--document">
<div className="file-render__viewer file-render__viewer--document">
{loading && <LoadingScreen status={loadingMessage} spinner />}
{error && <LoadingScreen status={errorMessage} spinner={false} />}
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}

View file

@ -35,7 +35,10 @@ class HtmlViewer extends React.PureComponent<Props, State> {
const { source } = this.props;
const { loading } = this.state;
return (
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
<div
className="file-render__viewer file-render__viewer--html file-render__viewer--iframe"
onContextMenu={stopContextMenu}
>
{loading && <div className="placeholder--text-document" />}
{/* @if TARGET='app' */}
<iframe ref={this.iframe} hidden={loading} sandbox="" title={__('File preview')} src={`file://${source}`} />

View file

@ -1,57 +1,21 @@
// @flow
import * as React from 'react';
import { stopContextMenu } from 'util/context-menu';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
// @if TARGET='app'
import { shell } from 'electron';
// @endif
import IframeReact from 'component/IframeReact';
type Props = {
source: string,
};
class PdfViewer extends React.PureComponent<Props> {
constructor() {
super();
(this: any).openFile = this.openFile.bind(this);
}
componentDidMount() {
this.openFile();
}
openFile() {
const { source } = this.props;
const path = `file://${source}`;
// @if TARGET='app'
shell.openExternal(path);
// @endif
}
render() {
// We used to be able to just render a webview and display the pdf inside the app
// This was disabled on electron@3
// https://github.com/electron/electron/issues/12337
const { source } = this.props;
const src = IS_WEB ? source : `file://${source}`;
return (
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
{/* @if TARGET='app' */}
<p>
<I18nMessage
tokens={{ click_here: <Button button="link" label={__('Click here')} onClick={this.openFile} /> }}
>
PDF opened externally. %click_here% to open it again.
</I18nMessage>
</p>
{/* @endif */}
{/* @if TARGET='web' */}
<div className="file-render__viewer">
<iframe title={__('File preview')} src={source} />
<div className="file-render__viewer file-render__viewer--document" onContextMenu={stopContextMenu}>
<div className="file-render__viewer file-render__viewer--iframe">
<IframeReact title={__('File preview')} src={src} />
</div>
{/* @endif */}
</div>
);
}

View file

@ -23,7 +23,7 @@ const WalletBalance = (props: Props) => {
return (
<React.Fragment>
<section className="section__flex-wrap">
<section className="columns">
<div>
<h2 className="section__title">{__('Available Balance')}</h2>
<span className="section__title--large">

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 {
doFetchFileInfo,
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectChannelForClaimUri,
selectBalance,
} from 'lbry-redux';
import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { makeSelectIsText } from 'redux/selectors/content';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import FilePage from './view';
const select = (state, props) => ({
@ -22,11 +20,9 @@ const select = (state, props) => ({
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowMatureContent(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
balance: selectBalance(state),
isText: makeSelectIsText(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});
const perform = dispatch => ({
@ -34,10 +30,6 @@ const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
});
export default connect(
select,
perform
)(FilePage);
export default connect(select, perform)(FilePage);

View file

@ -1,41 +1,45 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import Page from 'component/page';
import I18nMessage from 'component/i18nMessage/view';
import LayoutWrapperFile from 'component/layoutWrapperFile';
import LayoutWrapperText from 'component/layoutWrapperText';
import * as RENDER_MODES from 'constants/file_render_modes';
import FileRenderHeader from 'component/fileRenderHeader';
import FileTitle from 'component/fileTitle';
import FileRenderInitiator from 'component/fileRenderInitiator';
import FileRenderInline from 'component/fileRenderInline';
import FileRenderDownload from 'component/fileRenderDownload';
import Card from 'component/common/card';
import FileDetails from 'component/fileDetails';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
export const FILE_WRAPPER_CLASS = 'grid-area--content';
type Props = {
claim: StreamClaim,
costInfo: ?{ includesData: boolean, cost: number },
fileInfo: FileListItem,
uri: string,
claimIsMine: boolean,
costInfo: ?{ cost: number },
fetchFileInfo: string => void,
fetchCostInfo: string => void,
setViewed: string => void,
isSubscribed: ?string,
isSubscribed: boolean,
channelUri: string,
viewCount: number,
renderMode: string,
markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void,
balance: number,
isText: boolean,
};
class FilePage extends React.Component<Props> {
componentDidMount() {
const { uri, claim, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed, fetchViewCount } = this.props;
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
if (isSubscribed) {
this.removeFromSubscriptionNotifications();
}
fetchViewCount(claim.claim_id);
// always refresh file info when entering file page to see if we have the file
// this could probably be refactored into more direct components now
// @if TARGET='app'
fetchFileInfo(uri);
// @endif
@ -46,16 +50,12 @@ class FilePage extends React.Component<Props> {
}
componentDidUpdate(prevProps: Props) {
const { isSubscribed, claim, uri, fileInfo, setViewed, fetchViewCount, fetchFileInfo } = this.props;
const { isSubscribed, uri, fileInfo, setViewed, fetchFileInfo } = this.props;
if (!prevProps.isSubscribed && isSubscribed) {
this.removeFromSubscriptionNotifications();
}
if (prevProps.uri !== uri) {
fetchViewCount(claim.claim_id);
}
if (prevProps.uri !== uri) {
setViewed(uri);
}
@ -74,26 +74,74 @@ class FilePage extends React.Component<Props> {
markSubscriptionRead(channelUri, uri);
}
render() {
const { uri, claimIsMine, costInfo, fileInfo, balance, isText } = this.props;
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
renderFilePageLayout(uri: string, mode: string, cost: ?number) {
if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
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 (
<Page className="main--file-page">
{!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning">
<I18nMessage
tokens={{
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
}}
>
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.
Check out %reward_link% for free LBC or send more LBC to your wallet.
</I18nMessage>
</div>
)}
<React.Fragment>
<FileRenderHeader uri={uri} />
<FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} />
<FileTitle uri={uri} />
</React.Fragment>
);
}
{isText ? <LayoutWrapperText uri={uri} /> : <LayoutWrapperFile uri={uri} />}
render() {
const { uri, renderMode, costInfo } = this.props;
return (
<Page className="file-page">
<div className={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>
);
}

View file

@ -126,7 +126,7 @@ class HelpPage extends React.PureComponent<Props, State> {
}
return (
<Page>
<Page className="card-stack">
<Card
title={__('Read the FAQ')}
subtitle={__('Our FAQ answers many common questions.')}
@ -202,83 +202,82 @@ class HelpPage extends React.PureComponent<Props, State> {
<WalletBackup />
{/* @endif */}
<section className="card">
<header className="table__header">
<div className="table__header-text">
<h2 className="section__title">{__('About')}</h2>
{this.state.upgradeAvailable !== null && this.state.upgradeAvailable && (
<p className="section__subtitle">
{__('A newer version of LBRY is available.')}{' '}
<Button button="link" href={newVerLink} label={__('Download now!')} />
</p>
)}
</div>
</header>
<div className="table__wrapper">
<table className="table table--stretch">
<tbody>
<tr>
<td>{__('App')}</td>
<td>{this.state.uiVersion}</td>
</tr>
<tr>
<td>{__('Daemon (lbrynet)')}</td>
<td>{ver ? ver.lbrynet_version : __('Loading...')}</td>
</tr>
<tr>
<td>{__('Connected Email')}</td>
<td>
{user && user.primary_email ? (
<React.Fragment>
{user.primary_email}{' '}
<Button
button="link"
href={`https://lbry.com/list/edit/${accessToken}`}
label={__('Update mailing preferences')}
/>
</React.Fragment>
) : (
<React.Fragment>
<span className="empty">{__('none')} </span>
<Button button="link" onClick={() => doAuth()} label={__('set email')} />
</React.Fragment>
)}
</td>
</tr>
<tr>
<td>{__('Reward Eligible')}</td>
<td>{user && user.is_reward_approved ? __('Yes') : __('No')}</td>
</tr>
<tr>
<td>{__('Platform')}</td>
<td>{platform}</td>
</tr>
<tr>
<td>{__('Installation ID')}</td>
<td>{this.state.lbryId}</td>
</tr>
<tr>
<td>{__('Access Token')}</td>
<td>
{this.state.accessTokenHidden && (
<Button button="link" label={__('View')} onClick={this.showAccessToken} />
)}
{!this.state.accessTokenHidden && accessToken && (
<div>
<p>{accessToken}</p>
<div className="help--warning">
{__('This is equivalent to a password. Do not post or share this.')}
<Card
title={__('About')}
subtitle={
this.state.upgradeAvailable !== null && this.state.upgradeAvailable ? (
<span>
{__('A newer version of LBRY is available.')}
<Button button="link" href={newVerLink} label={__('Download now!')} />
</span>
) : null
}
isBodyTable
body={
<div className="table__wrapper">
<table className="table table--stretch">
<tbody>
<tr>
<td>{__('App')}</td>
<td>{this.state.uiVersion}</td>
</tr>
<tr>
<td>{__('Daemon (lbrynet)')}</td>
<td>{ver ? ver.lbrynet_version : __('Loading...')}</td>
</tr>
<tr>
<td>{__('Connected Email')}</td>
<td>
{user && user.primary_email ? (
<React.Fragment>
{user.primary_email}{' '}
<Button
button="link"
href={`https://lbry.com/list/edit/${accessToken}`}
label={__('Update mailing preferences')}
/>
</React.Fragment>
) : (
<React.Fragment>
<span className="empty">{__('none')} </span>
<Button button="link" onClick={() => doAuth()} label={__('set email')} />
</React.Fragment>
)}
</td>
</tr>
<tr>
<td>{__('Reward Eligible')}</td>
<td>{user && user.is_reward_approved ? __('Yes') : __('No')}</td>
</tr>
<tr>
<td>{__('Platform')}</td>
<td>{platform}</td>
</tr>
<tr>
<td>{__('Installation ID')}</td>
<td>{this.state.lbryId}</td>
</tr>
<tr>
<td>{__('Access Token')}</td>
<td>
{this.state.accessTokenHidden && (
<Button button="link" label={__('View')} onClick={this.showAccessToken} />
)}
{!this.state.accessTokenHidden && accessToken && (
<div>
<p>{accessToken}</p>
<div className="help--warning">
{__('This is equivalent to a password. Do not post or share this.')}
</div>
</div>
</div>
)}
</td>
</tr>
</tbody>
</table>
</div>
</section>
)}
</td>
</tr>
</tbody>
</table>
</div>
}
/>
</Page>
);
}

View file

@ -79,8 +79,8 @@ export default function SearchPage(props: Props) {
<div className="claim-preview__actions--header">
<ClaimUri uri={uriFromQuery} noShortUrl />
<Button
button="link"
className="media__uri--right"
button="alt"
label={__('View top claims for %normalized_uri%', {
normalized_uri: uriFromQuery,
})}

View file

@ -258,7 +258,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
const endHours = ['5', '6', '7', '8'];
return (
<Page>
<Page className="card-stack">
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>

View file

@ -5,76 +5,62 @@ import {
selectClaimsByUri,
makeSelectClaimsInChannelForCurrentPageState,
makeSelectClaimIsNsfw,
makeSelectClaimIsMine,
makeSelectRecommendedContentForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
selectBalance,
selectBlockedChannels,
parseURI,
makeSelectContentTypeForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import { selectAllCostInfoByUri } from 'lbryinc';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes';
import path from 'path';
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
// @if TARGET='web'
import { generateStreamUrl } from 'util/lbrytv';
// @endif
const RECENT_HISTORY_AMOUNT = 10;
const HISTORY_ITEMS_PER_PAGE = 50;
export const selectState = (state: any) => state.content || {};
export const selectPlayingUri = createSelector(
selectState,
state => state.playingUri
);
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
export const makeSelectIsPlaying = (uri: string) =>
createSelector(
selectPlayingUri,
playingUri => playingUri === uri
);
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
export const makeSelectContentPositionForUri = (uri: string) =>
createSelector(
selectState,
makeSelectClaimForUri(uri),
(state, claim) => {
if (!claim) {
return null;
}
const outpoint = `${claim.txid}:${claim.nout}`;
const id = claim.claim_id;
return state.positions[id] ? state.positions[id][outpoint] : null;
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
if (!claim) {
return null;
}
);
const outpoint = `${claim.txid}:${claim.nout}`;
const id = claim.claim_id;
return state.positions[id] ? state.positions[id][outpoint] : null;
});
export const selectHistory = createSelector(
selectState,
state => state.history || []
);
export const selectHistory = createSelector(selectState, state => state.history || []);
export const selectHistoryPageCount = createSelector(
selectHistory,
history => Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
export const selectHistoryPageCount = createSelector(selectHistory, history =>
Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
);
export const makeSelectHistoryForPage = (page: number) =>
createSelector(
selectHistory,
selectClaimsByUri,
(history, claimsByUri) => {
const left = page * HISTORY_ITEMS_PER_PAGE;
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
return historyItemsForPage;
}
);
createSelector(selectHistory, selectClaimsByUri, (history, claimsByUri) => {
const left = page * HISTORY_ITEMS_PER_PAGE;
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
return historyItemsForPage;
});
export const makeSelectHistoryForUri = (uri: string) =>
createSelector(
selectHistory,
history => history.find(i => i.uri === uri)
);
createSelector(selectHistory, history => history.find(i => i.uri === uri));
export const makeSelectHasVisitedUri = (uri: string) =>
createSelector(
makeSelectHistoryForUri(uri),
history => Boolean(history)
);
createSelector(makeSelectHistoryForUri(uri), history => Boolean(history));
export const makeSelectNextUnplayedRecommended = (uri: string) =>
createSelector(
@ -132,51 +118,105 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
}
);
export const selectRecentHistory = createSelector(
selectHistory,
history => {
return history.slice(0, RECENT_HISTORY_AMOUNT);
}
);
export const selectRecentHistory = createSelector(selectHistory, history => {
return history.slice(0, RECENT_HISTORY_AMOUNT);
});
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
createSelector(
makeSelectClaimsInChannelForCurrentPageState(channel),
channelClaims => {
if (uris) return uris;
createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), channelClaims => {
if (uris) return uris;
if (channelClaims) {
const CATEGORY_LIST_SIZE = 10;
return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`);
}
return null;
if (channelClaims) {
const CATEGORY_LIST_SIZE = 10;
return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`);
}
);
return null;
});
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(
selectShowMatureContent,
makeSelectClaimIsNsfw(uri),
(showMatureContent, isClaimMature) => {
return isClaimMature && !showMatureContent;
makeSelectContentTypeForUri(uri),
makeSelectMediaTypeForUri(uri),
makeSelectFileExtensionForUri(uri),
(contentType, mediaType, extension) => {
if (mediaType === 'video' || FORCE_CONTENT_TYPE_PLAYER.includes(contentType)) {
return RENDER_MODES.VIDEO;
}
if (mediaType === 'image') {
return RENDER_MODES.IMAGE;
}
if (['md', 'markdown'].includes(extension) || ['text/md', 'text/markdown'].includes(contentType)) {
return RENDER_MODES.MARKDOWN;
}
if (contentType === 'application/pdf') {
return RENDER_MODES.PDF;
}
if (['text/htm', 'text/html'].includes(contentType)) {
return RENDER_MODES.HTML;
}
if (['text', 'document', 'script'].includes(mediaType)) {
return RENDER_MODES.DOCUMENT;
}
if (extension === 'docx') {
return RENDER_MODES.DOCX;
}
// when writing this my local copy of Lbry.getMediaType had '3D-file', but I was receiving model...'
if (['3D-file', 'model'].includes(mediaType)) {
return RENDER_MODES.CAD;
}
if (mediaType === 'comic-book') {
return RENDER_MODES.COMIC;
}
if (
[
'application/zip',
'application/x-gzip',
'application/x-gtar',
'application/x-tgz',
'application/vnd.rar',
'application/x-7z-compressed',
].includes(contentType)
) {
return RENDER_MODES.DOWNLOAD;
}
if (mediaType === 'application') {
return RENDER_MODES.APPLICATION;
}
return RENDER_MODES.UNSUPPORTED;
}
);
export const makeSelectCanAutoplay = (uri: string) =>
export const makeSelectInsufficientCreditsForUri = (uri: string) =>
createSelector(
makeSelectMediaTypeForUri(uri),
mediaType => {
const canAutoPlay = ['audio', 'video', 'image', 'text', 'document'].includes(mediaType);
return canAutoPlay;
}
);
export const makeSelectIsText = (uri: string) =>
createSelector(
makeSelectMediaTypeForUri(uri),
mediaType => {
const isText = ['text', 'document', 'script'].includes(mediaType);
return isText;
makeSelectClaimIsMine(uri),
makeSelectCostInfoForUri(uri),
selectBalance,
(isMine, costInfo, balance) => {
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
}
);

View file

@ -1,8 +1,3 @@
.button {
display: inline-block;
font-weight: var(--font-weight-base);
}
.button--uri-indicator {
@extend .button--link;
color: var(--color-text-subtitle);

View file

@ -1,6 +1,5 @@
.card {
background-color: var(--color-card-background);
margin-bottom: var(--spacing-large);
position: relative;
border-radius: var(--card-radius);
overflow: hidden;
@ -79,6 +78,12 @@
justify-content: center;
}
.card-stack {
.card:not(:last-of-type) {
margin-bottom: var(--spacing-large);
}
}
.card__list {
column-count: 2;
column-gap: var(--spacing-large);
@ -86,7 +91,7 @@
.card {
display: inline-block;
margin: 0 0 var(--spacing-large);
margin-bottom: var(--spacing-large);
}
@media (max-width: $breakpoint-small) {
@ -139,22 +144,48 @@
background-color: black;
}
.card__media--disabled {
opacity: 0.5;
pointer-events: none;
}
.card__header {
margin: var(--spacing-medium) var(--spacing-large);
display: flex;
align-items: flex-start;
.section__subtitle {
margin-bottom: 0;
.icon__wrapper {
margin-right: var(--spacing-large);
}
}
.card__title {
font-size: var(--font-title);
font-weight: var(--font-weight-light);
display: block;
/* .badge rule inherited from file page prices, should be refactored */
.badge {
float: right;
margin-left: var(--spacing-small);
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
}
}
.card__subtitle {
color: var(--color-text-subtitle);
margin: var(--spacing-small) 0;
font-size: var(--font-body);
}
.card__body {
padding: var(--spacing-large);
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 {
@ -177,3 +208,14 @@
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;
color: #fff;
.button {
color: #fff;
.button--alt {
padding: 0 var(--spacing-small);
}
}

View file

@ -67,10 +67,6 @@
list-style: none;
}
.claim-preview__wrapper--channel {
background-color: var(--color-card-background-highlighted);
}
.claim-preview__wrapper--notice {
background-color: var(--color-notice);
}
@ -208,6 +204,10 @@
display: flex;
flex-direction: column;
flex: 1;
@media (max-width: $breakpoint-small) {
margin-bottom: var(--spacing-small);
}
}
.claim-preview-info {

View file

@ -1,17 +1,19 @@
.comments {
padding-top: var(--spacing-large);
}
.comment {
padding: var(--spacing-small) 0;
display: flex;
flex-direction: row;
font-size: var(--font-body);
padding: var(--spacing-medium) 0;
margin: 0;
&:first-of-type {
border-top: 1px solid var(--color-border);
}
&:not(:last-of-type) {
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-medium) 0;
}
&:last-of-type {
padding-top: var(--spacing-medium);
}
}

View file

@ -89,6 +89,8 @@
position: relative;
display: flex;
align-items: center;
border-radius: var(--card-radius);
border: 1px solid var(--color-border);
justify-content: center;
background-position: 50% 50%;
background-repeat: no-repeat;
@ -110,7 +112,7 @@
}
}
.content__cover--disabled {
.content__cover--none {
@include thumbnail;
cursor: default;
position: relative;
@ -128,8 +130,21 @@
}
}
.content__cover--hidden-for-text {
display: none;
.content__cover--disabled {
pointer-events: none;
.nag {
/* boo fire Jeremy */
pointer-events: auto;
}
&:after {
content: '';
background: rgba(255, 255, 255, 0.5);
top: 0;
left: 0;
bottom: 0;
right: 0;
position: absolute;
}
}
.content__loading {

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 {
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
max-height: var(--inline-player-max-height);
@media (max-width: $breakpoint-small) {
// margin-top: 10px;
}
}
.file-render--document {
max-height: none;
overflow: auto;
.content__loading {
background-color: transparent;
@ -22,6 +53,21 @@
color: var(--color-text);
}
}
.markdown-preview {
height: 100%;
overflow: auto;
width: 40em;
margin-left: auto;
margin-right: auto;
max-width: unset;
min-width: unset;
@media (max-width: $breakpoint-small) {
width: 100%;
padding: var(--spacing-small);
}
}
}
.file-render__viewer {
@ -45,28 +91,17 @@
}
}
.file-render__viewer--document {
@extend .file-render__viewer;
overflow: auto;
.markdown-preview {
height: 100%;
overflow: auto;
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
}
.file-render__viewer--iframe {
display: flex; /*this eliminates extra height from whitespace, if someone edits this with a better technique, tell Jeremy*/
/*
ideally iframes would dynamiclly grow, see <IframeReact> for a start at this
for now, since we don't know size, let's make as large as we can without being larger than available area
*/
iframe {
height: calc(100vh - var(--header-height) - var(--spacing-medium) * 2);
}
}
.file-render__viewer--pdf {
@extend .file-render__viewer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
.file-render__content {
width: 100%;
height: 100%;
@ -138,6 +173,8 @@
}
.file-render {
border-radius: var(--card-radius);
.video-js {
display: flex;
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 {
max-width: 60rem;
margin-top: var(--spacing-main-padding);
@ -75,6 +48,9 @@
margin-top: 100px;
margin-bottom: 100px;
text-align: center;
> .card {
width: 100%;
}
}
.main--launching {
@ -100,26 +76,3 @@
.main--full-width {
width: 100%;
}
.main__document-wrapper {
max-width: 100%;
min-width: 40em;
width: fit-content;
margin: auto;
margin-bottom: var(--spacing-xlarge);
@media (max-width: $breakpoint-small) {
width: 100%;
}
}
.main__document-wrapper--markdown {
@extend .main__document-wrapper;
width: 40em;
max-width: unset;
min-width: unset;
@media (max-width: $breakpoint-small) {
width: 100%;
}
}

View file

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

View file

@ -15,25 +15,6 @@
// M E D I A
// T I T L E
.media__title {
margin-bottom: var(--spacing-medium);
}
.media__title-text {
overflow: hidden;
text-overflow: ellipsis;
font-weight: var(--font-weight-bold);
white-space: normal;
font-size: var(--font-title);
display: inline;
}
.media__title-badge {
float: right;
margin-left: var(--spacing-small);
margin-top: 5px;
}
.media__uri {
position: absolute;
transform: translateY(-130%);
@ -65,14 +46,6 @@
right: 0;
}
.media__uri--large {
margin-bottom: var(--spacing-medium);
}
.media__insufficient-credits {
padding: 10px;
}
// M E D I A
// S U B T I T L E
@ -90,19 +63,8 @@
@extend .media__subtitle;
display: flex;
justify-content: space-between;
}
.media__subtitle--large {
display: block;
> button {
display: block;
}
}
.media__subtitle__channel {
font-weight: var(--font-weight-bold);
margin: var(--spacing-small) 0;
align-items: center;
margin-bottom: var(--spacing-small);
}
.media__info-text {
@ -118,13 +80,18 @@
}
.media__actions {
@extend .section__actions;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 0;
}
.media__document-thumbnail {
margin-top: 0;
@media (max-width: $breakpoint-small) {
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;
vertical-align: middle;
text-align: left;
margin-bottom: var(--spacing-xlarge);
}
.yrbl {

View file

@ -13,7 +13,6 @@ $nag-error-z-index: 100001;
color: var(--color-white);
font-weight: var(--font-weight-bold);
text-align: center;
z-index: 2;
.button--link {
font-weight: var(--font-weight-bold);
@ -26,6 +25,13 @@ $nag-error-z-index: 100001;
}
}
.nag--inline {
position: absolute;
top: 0;
bottom: auto;
z-index: 1 !important; /* booooooo */
}
.nag--helpful {
background-color: var(--color-secondary);
color: var(--color-white);

View file

@ -22,21 +22,11 @@
.section__flex {
display: flex;
align-items: flex-start;
& > :first-child {
> .icon__wrapper:first-child {
margin-right: var(--spacing-large);
}
}
.section__flex-wrap {
@extend .section__flex;
flex-wrap: wrap;
margin-bottom: var(--spacing-large);
& > * {
margin-bottom: var(--spacing-large);
}
}
.section__title {
text-align: left;
font-size: var(--font-title);
@ -101,6 +91,10 @@
}
}
.section__actions--no-margin {
margin-top: 0;
}
@media (max-width: $breakpoint-small) {
.section__actions {
flex-wrap: wrap;

View file

@ -146,6 +146,7 @@ img {
& > * {
margin: 0;
margin-bottom: var(--spacing-medium);
width: 100%;
flex-basis: auto;
@ -216,12 +217,16 @@ img {
.help--inline {
@extend .help;
margin-top: 0;
margin-bottom: 0;
}
.empty {
color: var(--color-text-empty);
font-style: italic;
}
.empty--centered {
text-align: center;
}
.qr-code {
width: 134px;

View file

@ -19,9 +19,6 @@ $breakpoint-medium: 1150px;
--spacing-large: 2rem;
--spacing-xlarge: 3rem;
--spacing-main-padding: var(--spacing-xlarge);
--file-page-max-width: 1787px;
--file-max-height: 788px;
--file-max-width: 1400px;
--floating-viewer-width: 32rem;
--floating-viewer-height: 18rem; // 32 * 9/16
--floating-viewer-info-height: 5rem;

View file

@ -21,6 +21,9 @@
--color-button-secondary-bg: #395877;
--color-button-secondary-bg-hover: #4b6d8f;
--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

View file

@ -819,9 +819,10 @@
prop-types "^15.6.2"
scheduler "^0.18.0"
"@lbry/components@^3.0.12":
version "3.0.12"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-3.0.12.tgz#9ba4598edf26496060a97023ca0132d39c388c3b"
"@lbry/components@^4.0.1":
version "4.0.1"
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":
version "0.1.3"