new text viewer layout

This commit is contained in:
Sean Yesmunt 2020-01-06 13:32:35 -05:00
parent 9a6f2a1975
commit 72b9f3efdd
47 changed files with 658 additions and 274 deletions

View file

@ -10,7 +10,7 @@ import ReactModal from 'react-modal';
import { openContextMenu } from 'util/context-menu'; import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout'; import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer'; import FloatingViewer from 'component/floatingViewer';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous'; import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag'; import Nag from 'component/common/nag';
@ -203,7 +203,7 @@ function App(props: Props) {
> >
<Router /> <Router />
<ModalRouter /> <ModalRouter />
<FileViewer pageUri={uri} /> <FloatingViewer pageUri={uri} />
{/* @if TARGET='web' */} {/* @if TARGET='web' */}
<YoutubeWelcome /> <YoutubeWelcome />

View file

@ -147,6 +147,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
}} }}
className={combinedClassName} className={combinedClassName}
activeClassName={activeClass} activeClassName={activeClass}
{...otherProps}
> >
{content} {content}
</NavLink> </NavLink>

View file

@ -101,7 +101,7 @@ export default function ClaimList(props: Props) {
{header !== false && ( {header !== false && (
<React.Fragment> <React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>} {headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
<div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}> <div className={classnames('claim-list__header', { 'section__title--small': type === 'small' })}>
{header} {header}
{loading && <Spinner type="small" />} {loading && <Spinner type="small" />}
<div className="claim-list__alt-controls"> <div className="claim-list__alt-controls">

View file

@ -7,7 +7,7 @@ import { withRouter } from 'react-router-dom';
import { openCopyLinkMenu } from 'util/context-menu'; import { openCopyLinkMenu } from 'util/context-menu';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import { isEmpty } from 'util/object'; import { isEmpty } from 'util/object';
import CardMedia from 'component/cardMedia'; import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import TruncatedText from 'component/common/truncated-text'; import TruncatedText from 'component/common/truncated-text';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
@ -200,7 +200,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<ChannelThumbnail uri={uri} obscure={channelIsBlocked} /> <ChannelThumbnail uri={uri} obscure={channelIsBlocked} />
</UriIndicator> </UriIndicator>
) : ( ) : (
<CardMedia thumbnail={thumbnail} /> <FileThumbnail thumbnail={thumbnail} />
)} )}
<div className="claim-preview__text"> <div className="claim-preview__text">
<div className="claim-preview-metadata"> <div className="claim-preview-metadata">

View file

@ -1,21 +1,33 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux'; import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc'; import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import FileActions from './view'; import fs from 'fs';
import FilePage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
/* availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix */
costInfo: makeSelectCostInfoForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(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),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
}); });
export default connect( export default connect(
select, select,
perform perform
)(FileActions); )(FilePage);

View file

@ -3,40 +3,114 @@ import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import Tooltip from 'component/common/tooltip'; import FileDownloadLink from 'component/fileDownloadLink';
import { buildURI } from 'lbry-redux';
type Props = { type Props = {
uri: string, uri: string,
claimId: string, claim: StreamClaim,
openModal: (id: string, { uri: string }) => void, openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
prepareEdit: ({}, string, {}) => void,
claimIsMine: boolean, claimIsMine: boolean,
fileInfo: FileListItem, fileInfo: FileListItem,
costInfo: ?{ cost: number },
contentType: string,
supportOption: boolean,
}; };
class FileActions extends React.PureComponent<Props> { function FileActions(props: Props) {
render() { const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, contentType, supportOption, prepareEdit } = props;
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props; const webShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0)); const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
const claimId = claim && claim.claim_id;
const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
// We want to use the short form uri for editing
// This is what the user is used to seeing, they don't care about the claim id
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = {
streamName: claim.name,
streamClaimId: claim.claim_id,
};
if (channelName) {
uriObject.channelName = channelName;
}
editUri = buildURI(uriObject);
}
return ( return (
<React.Fragment> <div className="media__actions">
{showDelete && ( <div className="section__actions">
<Tooltip label={__('Remove from your library')}>
<Button <Button
button="alt"
icon={ICONS.SHARE}
label={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
{!claimIsMine && (
<Button
button="alt"
icon={ICONS.TIP}
label={__('Tip')}
requiresAuth={IS_WEB}
title={__('Send a tip to this creator')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: false })}
/>
)}
{(claimIsMine || (!claimIsMine && supportOption)) && (
<Button
button="alt"
icon={ICONS.SUPPORT}
label={__('Support')}
requiresAuth={IS_WEB}
title={__('Support this claim')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
<div className="section__actions">
{/* @if TARGET='app' */}
<FileDownloadLink uri={uri} />
{/* @endif */}
{claimIsMine && (
<Button
button="alt"
icon={ICONS.EDIT}
label={__('Edit')}
navigate="/$/publish"
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
{showDelete && (
<Button
title={__('Remove from your library')}
button="alt" button="alt"
icon={ICONS.DELETE} icon={ICONS.DELETE}
description={__('Delete')} description={__('Delete')}
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })} onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
/> />
</Tooltip>
)} )}
{!claimIsMine && ( {!claimIsMine && (
<Tooltip label={__('Report content')}> <Button
<Button button="alt" icon={ICONS.REPORT} href={`https://lbry.com/dmca/${claimId}`} /> title={__('Report content')}
</Tooltip> button="alt"
icon={ICONS.REPORT}
href={`https://lbry.com/dmca/${claimId}`}
/>
)} )}
</React.Fragment> </div>
</div>
); );
}
} }
export default FileActions; export default FileActions;

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { makeSelectChannelForClaimUri } from 'lbry-redux';
import FileAuthor from './view';
const select = (state, props) => ({
channelUri: makeSelectChannelForClaimUri(props.uri)(state),
});
export default connect(select)(FileAuthor);

View file

@ -0,0 +1,19 @@
// @flow
import * as React from 'react';
import ClaimPreview from 'component/claimPreview';
type Props = {
channelUri: string,
};
function LayoutWrapperDocument(props: Props) {
const { channelUri } = props;
return channelUri ? (
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock />
) : (
<div className="claim-preview--inline claim-preview-title">{__('Anonymous')}</div>
);
}
export default LayoutWrapperDocument;

View file

@ -43,11 +43,9 @@ class FileDetails extends PureComponent<Props> {
<Fragment> <Fragment>
<Expandable> <Expandable>
{description && ( {description && (
<Fragment>
<div className="media__info-text"> <div className="media__info-text">
<MarkdownPreview content={description} /> <MarkdownPreview content={description} />
</div> </div>
</Fragment>
)} )}
<ClaimTags uri={uri} type="large" /> <ClaimTags uri={uri} type="large" />
<table className="table table--condensed table--fixed table--file-details"> <table className="table table--condensed table--fixed table--file-details">

View file

@ -1,6 +1,7 @@
// @flow // @flow
import { remote } from 'electron'; import { remote } from 'electron';
import React, { Suspense, Fragment } from 'react'; import React, { Suspense, Fragment } from 'react';
import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import VideoViewer from 'component/viewers/videoViewer'; import VideoViewer from 'component/viewers/videoViewer';
import ImageViewer from 'component/viewers/imageViewer'; import ImageViewer from 'component/viewers/imageViewer';
@ -186,8 +187,10 @@ class FileRender extends React.PureComponent<Props> {
} }
render() { render() {
const { mediaType } = this.props;
return ( return (
<div className="file-render"> <div className={classnames('file-render', { 'file-render--document': mediaType === 'text' })}>
<Suspense fallback={<div />}>{this.renderViewer()}</Suspense> <Suspense fallback={<div />}>{this.renderViewer()}</Suspense>
</div> </div>
); );

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -21,11 +21,11 @@ class CardMedia extends React.PureComponent<Props> {
// @if TARGET='web' // @if TARGET='web'
// Pass image urls through a compression proxy // Pass image urls through a compression proxy
url = thumbnail || Placeholder; url = thumbnail || Placeholder;
// url = thumbnail // url = thumbnail
// ? 'https://ext.thumbnails.lbry.com/400x,q55/' + // ? 'https://ext.thumbnails.lbry.com/400x,q55/' +
// The image server will redirect if we don't remove the double slashes after http(s) // The image server will redirect if we don't remove the double slashes after http(s)
// thumbnail.replace('https://', 'https:/').replace('http://', 'http:/') // thumbnail.replace('https://', 'https:/').replace('http://', 'http:/')
// : Placeholder; // : Placeholder;
// @endif // @endif
// @if TARGET='app' // @if TARGET='app'
url = thumbnail || Placeholder; url = thumbnail || Placeholder;

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { makeSelectViewCountForUri } from 'lbryinc';
import FileViewCount from './view';
const select = (state, props) => ({
viewCount: makeSelectViewCountForUri(props.uri)(state),
});
export default connect(select)(FileViewCount);

View file

@ -0,0 +1,20 @@
// @flow
import React from 'react';
import HelpLink from 'component/common/help-link';
type Props = {
viewCount: string,
};
function LayoutWrapperDocument(props: Props) {
const { viewCount } = props;
return (
<span>
{viewCount !== 1 ? __('%view_count% Views', { view_count: viewCount }) : __('1 View')}
<HelpLink href="https://lbry.com/faq/views" />
</span>
);
}
export default LayoutWrapperDocument;

View file

@ -1,6 +1,6 @@
// @flow // @flow
// This component is entirely for triggering the start of a file view // This component is entirely for triggering the start of a file view
// The actual viewer for a file exists in FileViewer // The actual viewer for a file exists in TextViewer and FloatingViewer
// They can't exist in one component because we need to handle/listen for the start of a new file view // They can't exist in one component because we need to handle/listen for the start of a new file view
// while a file is currently being viewed // while a file is currently being viewed
import React, { useEffect, useCallback, Fragment } from 'react'; import React, { useEffect, useCallback, Fragment } from 'react';
@ -27,9 +27,10 @@ type Props = {
hasCostInfo: boolean, hasCostInfo: boolean,
costInfo: any, costInfo: any,
isAutoPlayable: boolean, isAutoPlayable: boolean,
inline: boolean,
}; };
export default function FileViewer(props: Props) { export default function FileViewerInitiator(props: Props) {
const { const {
play, play,
mediaType, mediaType,
@ -49,6 +50,7 @@ export default function FileViewer(props: Props) {
const cost = costInfo && costInfo.cost; const cost = costInfo && costInfo.cost;
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType); const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo; const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
const isText = mediaType === 'text';
const fileStatus = fileInfo && fileInfo.status; const fileStatus = fileInfo && fileInfo.status;
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text'; const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true; const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true;
@ -95,10 +97,10 @@ export default function FileViewer(props: Props) {
useEffect(() => { useEffect(() => {
const videoOnPage = document.querySelector('video'); const videoOnPage = document.querySelector('video');
if (autoplay && !videoOnPage && isAutoPlayable && hasCostInfo && cost === 0) { if (((autoplay && !videoOnPage && isAutoPlayable) || isText) && hasCostInfo && cost === 0) {
viewFile(); viewFile();
} }
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost]); }, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost, isText]);
return ( return (
<div <div
@ -108,6 +110,7 @@ export default function FileViewer(props: Props) {
className={classnames({ className={classnames({
content__cover: supported, content__cover: supported,
'content__cover--disabled': !supported, 'content__cover--disabled': !supported,
'content__cover--hidden-for-text': isText,
'card__media--nsfw': obscurePreview, 'card__media--nsfw': obscurePreview,
'card__media--disabled': supported && !fileInfo && insufficientCredits, 'card__media--disabled': supported && !fileInfo && insufficientCredits,
})} })}
@ -127,6 +130,7 @@ export default function FileViewer(props: Props) {
} }
/> />
)} )}
{!isPlaying && supported && ( {!isPlaying && supported && (
<Button <Button
onClick={viewFile} onClick={viewFile}

View file

@ -8,7 +8,7 @@ import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import usePrevious from 'effects/use-previous'; import usePrevious from 'effects/use-previous';
import { FILE_WRAPPER_CLASS } from 'page/file/view'; import { FILE_WRAPPER_CLASS } from 'component/layoutWrapperFile/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import { onFullscreenChange } from 'util/full-screen'; import { onFullscreenChange } from 'util/full-screen';
@ -96,7 +96,6 @@ export default function FileViewer(props: Props) {
function handleResize() { function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`); const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
if (!element) { if (!element) {
console.error("Can't find file viewer wrapper to attach to the inline viewer to"); // eslint-disable-line
return; return;
} }
@ -125,7 +124,10 @@ export default function FileViewer(props: Props) {
} }
const hidePlayer = const hidePlayer =
!isPlaying || !uri || (!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType))); mediaType === 'text' ||
!isPlaying ||
!uri ||
(!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
if (hidePlayer) { if (hidePlayer) {
return null; return null;

View file

@ -0,0 +1,35 @@
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

@ -0,0 +1,89 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileAuthor from 'component/fileAuthor';
import FileActions from 'component/fileActions';
import DateTime from 'component/dateTime';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import FileViewCount from 'component/fileViewCount';
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">
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<FileViewCount uri={uri} />
</div>
<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

@ -0,0 +1,39 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
makeSelectTitleForUri,
makeSelectMetadataForUri,
makeSelectThumbnailForUri,
} 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),
metadata: makeSelectMetadataForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(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

@ -0,0 +1,98 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FilePrice from 'component/filePrice';
import FileAuthor from 'component/fileAuthor';
import FileThumbnail from 'component/fileThumbnail';
import FileViewCount from 'component/fileViewCount';
import FileActions from 'component/fileActions';
import TextViewer from 'component/textViewer';
import DateTime from 'component/dateTime';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import MarkdownPreview from 'component/common/markdown-preview';
import ClaimUri from 'component/claimUri';
import FileViewerInitiator from 'component/fileViewerInitiator';
type Props = {
uri: string,
metadata: StreamMetadata,
title: string,
nsfw: boolean,
claim: StreamClaim,
thumbnail: ?string,
};
function LayoutWrapperDocument(props: Props) {
const { uri, claim, metadata, title, nsfw, thumbnail } = props;
const { description } = metadata;
return (
<div className="">
<div className="main__document-wrapper">
<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>
</div>
<div className="media__document-thumbnail">
<FileThumbnail thumbnail={thumbnail} />
</div>
<div className="section main__document-wrapper">
<div className="section__subtitle">
<em>
<MarkdownPreview content={description} />
</em>
</div>
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<FileViewCount uri={uri} />
</div>
<FileActions uri={uri} />
<div className="section__divider">
<hr />
</div>
<FileAuthor uri={uri} />
<div className="section__divider">
<hr />
</div>
{/* Render the initiator to trigger the view of the file */}
<FileViewerInitiator uri={uri} />
<TextViewer uri={uri} />
<div className="section__divider">
<hr />
</div>
</div>
<div className="columns">
<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 LayoutWrapperDocument;

View file

@ -0,0 +1,34 @@
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

@ -0,0 +1,60 @@
// @flow
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import FileRender from 'component/fileRender';
import usePrevious from 'effects/use-previous';
type Props = {
mediaType: string,
contentType: string,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
isStreamable: boolean,
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;
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);
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);
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
return (
<div className={classnames('content__viewersss')}>
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
</div>
);
}

View file

@ -98,12 +98,11 @@ class DocumentViewer extends React.PureComponent<Props, State> {
render() { render() {
const { error, loading, content } = this.state; const { error, loading, content } = this.state;
const isReady = content && !error; const isReady = content && !error;
const loadingMessage = __('Rendering document.');
const errorMessage = __("Sorry, looks like we can't load the document."); const errorMessage = __("Sorry, looks like we can't load the document.");
return ( return (
<div className="file-render__viewer--document"> <div className="file-render__viewer--document">
{loading && !error && <LoadingScreen status={loadingMessage} spinner />} {loading && !error && <div className="placeholder--text-document" />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />} {error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && this.renderDocument()} {isReady && this.renderDocument()}
</div> </div>

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import CardMedia from 'component/cardMedia'; import FileThumbnail from 'component/fileThumbnail';
type Props = { type Props = {
params: UpdatePublishFormData, params: UpdatePublishFormData,
progress: string, progress: string,
@ -13,7 +13,7 @@ export default function WebUploadItem(props: Props) {
return ( return (
<li className={'claim-preview claim-preview--inactive card--inline'}> <li className={'claim-preview claim-preview--inactive card--inline'}>
<CardMedia thumbnail={params.thumbnail_url} /> <FileThumbnail thumbnail={params.thumbnail_url} />
<div className={'claim-preview-metadata'}> <div className={'claim-preview-metadata'}>
<div className="claim-preview-info"> <div className="claim-preview-info">
<div className="claim-preview-title">{params.title}</div> <div className="claim-preview-title">{params.title}</div>

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import WalletAddress from './node_modules/component/walletAddress'; import WalletAddress from 'component/walletAddress';
import Page from './node_modules/component/page'; import Page from 'component/page';
const WalletAddressPage = () => ( const WalletAddressPage = () => (
<Page className="main--contained"> <Page className="main--contained">

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import WalletSend from './node_modules/component/walletSend'; import WalletSend from 'component/walletSend';
const WalletSendModal = () => ( const WalletSendModal = () => (
<div> <div>

View file

@ -1,32 +1,23 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions'; import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSetContentHistoryItem } from 'redux/actions/content'; import { doSetContentHistoryItem } from 'redux/actions/content';
import { import {
doFetchFileInfo, doFetchFileInfo,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
selectBalance, selectBalance,
makeSelectTitleForUri, makeSelectMediaTypeForUri,
makeSelectThumbnailForUri,
makeSelectClaimIsNsfw,
doPrepareEdit,
} from 'lbry-redux'; } from 'lbry-redux';
import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc'; import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FilePage from './view'; import FilePage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowMatureContent(state), obscureNsfw: !selectShowMatureContent(state),
@ -34,20 +25,13 @@ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state), channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state),
balance: selectBalance(state), balance: selectBalance(state),
title: makeSelectTitleForUri(props.uri)(state), mediaType: makeSelectMediaTypeForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)), setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)), markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)), fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),

View file

@ -1,31 +1,14 @@
// @flow // @flow
import * as MODALS from 'constants/modal_types';
import * as icons from 'constants/icons';
import * as React from 'react'; import * as React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileActions from 'component/fileActions';
import DateTime from 'component/dateTime';
import Button from 'component/button'; import Button from 'component/button';
import Page from 'component/page'; import Page from 'component/page';
import FileDownloadLink from 'component/fileDownloadLink';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import ClaimPreview from 'component/claimPreview';
import HelpLink from 'component/common/help-link';
import I18nMessage from 'component/i18nMessage/view'; import I18nMessage from 'component/i18nMessage/view';
import LayoutWrapperFile from 'component/layoutWrapperFile';
export const FILE_WRAPPER_CLASS = 'grid-area--content'; import LayoutWrapperText from 'component/layoutWrapperText';
type Props = { type Props = {
claim: StreamClaim, claim: StreamClaim,
fileInfo: FileListItem, fileInfo: FileListItem,
contentType: string,
uri: string, uri: string,
claimIsMine: boolean, claimIsMine: boolean,
costInfo: ?{ cost: number }, costInfo: ?{ cost: number },
@ -36,14 +19,10 @@ type Props = {
isSubscribed: boolean, isSubscribed: boolean,
channelUri: string, channelUri: string,
viewCount: number, viewCount: number,
prepareEdit: ({}, string, {}) => void,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
markSubscriptionRead: (string, string) => void, markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void, fetchViewCount: string => void,
balance: number, balance: number,
title: string, mediaType: string,
nsfw: boolean,
supportOption: boolean,
}; };
class FilePage extends React.Component<Props> { class FilePage extends React.Component<Props> {
@ -96,50 +75,11 @@ class FilePage extends React.Component<Props> {
} }
render() { render() {
const { const { uri, claimIsMine, costInfo, fileInfo, balance, mediaType } = this.props;
claim,
contentType,
uri,
openModal,
claimIsMine,
prepareEdit,
costInfo,
fileInfo,
channelUri,
viewCount,
balance,
title,
nsfw,
supportOption,
} = this.props;
// File info
const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
const webShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
// We want to use the short form uri for editing
// This is what the user is used to seeing, they don't care about the claim id
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = {
streamName: claim.name,
streamClaimId: claim.claim_id,
};
if (channelName) {
uriObject.channelName = channelName;
}
editUri = buildURI(uriObject);
}
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance; const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
return ( return (
<Page className="main--file-page"> <Page className="main--file-page">
<ClaimUri uri={uri} />
<div className={`card ${FILE_WRAPPER_CLASS}`}>
{!fileInfo && insufficientCredits && ( {!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning"> <div className="media__insufficient-credits help--warning">
<I18nMessage <I18nMessage
@ -147,109 +87,13 @@ class FilePage extends React.Component<Props> {
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />, 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 The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.
it. Check out %reward_link% for free LBC or send more LBC to your wallet. Check out %reward_link% for free LBC or send more LBC to your wallet.
</I18nMessage> </I18nMessage>
</div> </div>
)} )}
<FileViewerInitiator uri={uri} insufficientCredits={insufficientCredits} />
</div>
<div className="media__title"> {mediaType === 'text' ? <LayoutWrapperText uri={uri} /> : <LayoutWrapperFile uri={uri} />}
<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">
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<span>
{viewCount !== 1 ? __('%view_count% Views', { view_count: viewCount }) : __('1 View')}
<HelpLink href="https://lbry.com/faq/views" />
</span>
</div>
<div className="media__actions">
<div className="section__actions">
{claimIsMine && (
<Button
button="alt"
icon={icons.EDIT}
label={__('Edit')}
navigate="/$/publish"
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
<Button
button="alt"
icon={icons.SHARE}
label={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
{!claimIsMine && (
<Button
button="alt"
icon={icons.TIP}
label={__('Tip')}
requiresAuth={IS_WEB}
title={__('Send a tip to this creator')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: false })}
/>
)}
{(claimIsMine || (!claimIsMine && supportOption)) && (
<Button
button="alt"
icon={icons.SUPPORT}
label={__('Support')}
requiresAuth={IS_WEB}
title={__('Support this claim')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
<div className="section__actions">
{/* @if TARGET='app' */}
<FileDownloadLink uri={uri} />
{/* @endif */}
<FileActions uri={uri} claimId={claim.claim_id} />
</div>
</div>
<div className="section__divider">
<hr />
</div>
{channelUri ? (
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock />
) : (
<div className="claim-preview--inline claim-preview-title">{__('Anonymous')}</div>
)}
<FileDetails uri={uri} />
<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>
</Page> </Page>
); );
} }

View file

@ -17,10 +17,6 @@
} }
} }
.claim-list__header--small {
color: var(--color-text-subtitle);
}
.claim-list__dropdown { .claim-list__dropdown {
padding: 0 var(--spacing-medium); padding: 0 var(--spacing-medium);
@ -139,7 +135,6 @@
.claim-preview--inline { .claim-preview--inline {
padding: 0; padding: 0;
border-bottom: none; border-bottom: none;
margin-bottom: var(--spacing-medium);
.channel-thumbnail { .channel-thumbnail {
width: var(--channel-thumbnail-width--small); width: var(--channel-thumbnail-width--small);

View file

@ -102,6 +102,10 @@
} }
} }
.content__cover--hidden-for-text {
display: none;
}
.content__loading { .content__loading {
height: 100%; height: 100%;
display: flex; display: flex;

View file

@ -6,6 +6,10 @@
max-height: var(--inline-player-max-height); max-height: var(--inline-player-max-height);
} }
.file-render--document {
max-height: none;
}
.file-render__viewer { .file-render__viewer {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -26,12 +30,14 @@
.file-render__viewer--document { .file-render__viewer--document {
@extend .file-render__viewer; @extend .file-render__viewer;
overflow: auto; overflow: auto;
background-color: var(--color-file-viewer-background);
.markdown-preview { .markdown-preview {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding: var(--spacing-large);
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
}
} }
} }

View file

@ -119,10 +119,6 @@
border-radius: 1.5rem; border-radius: 1.5rem;
margin-left: var(--spacing-small); margin-left: var(--spacing-small);
svg {
stroke: var(--color-text);
}
&:hover { &:hover {
background-color: var(--color-primary-alt); background-color: var(--color-primary-alt);
} }

View file

@ -25,7 +25,6 @@
.main { .main {
position: relative; position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-large)); width: calc(100% - var(--side-nav-width) - var(--spacing-large));
margin-right: var(--spacing-main-padding);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
width: 100%; width: 100%;
@ -101,3 +100,12 @@
.main--full-width { .main--full-width {
width: 100%; width: 100%;
} }
.main__document-wrapper {
width: 60%;
margin: auto;
@media (max-width: $breakpoint-small) {
width: 100%;
}
}

View file

@ -13,7 +13,33 @@
font-size: inherit; font-size: inherit;
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-medium); margin-bottom: var(--spacing-medium);
padding-top: var(--spacing-medium);
&:not(:first-child) {
margin-top: var(--spacing-large);
}
}
h1 {
font-size: 1.8em;
}
h2 {
font-size: 1.7em;
}
h3 {
font-size: 1.6em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.4em;
}
h6 {
font-size: 1.3em;
}
@media (max-width: $breakpoint-small) {
font-size: 0.8em;
} }
// Paragraphs // Paragraphs
@ -28,10 +54,6 @@
} }
} }
// Strikethrough text
del {
}
// Tables // Tables
table { table {
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
@ -54,6 +76,13 @@
img { img {
margin-bottom: var(--spacing-medium); margin-bottom: var(--spacing-medium);
padding-top: var(--spacing-medium); padding-top: var(--spacing-medium);
max-height: 40vh;
object-position: left;
@media (max-width: $breakpoint-small) {
max-height: 30vh;
font-size: 0.8em;
}
} }
// Horizontal Rule // Horizontal Rule

View file

@ -108,3 +108,7 @@
justify-content: space-between; justify-content: space-between;
margin-top: 0; margin-top: 0;
} }
.media__document-thumbnail {
margin-top: 0;
}

View file

@ -1,6 +1,7 @@
.navigation { .navigation {
width: var(--side-nav-width); width: var(--side-nav-width);
font-size: var(--font-body); font-size: var(--font-body);
margin-left: var(--spacing-main-padding);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
display: none; display: none;

View file

@ -27,3 +27,8 @@
} }
} }
} }
.placeholder--text-document {
@include placeholder;
height: 60vh;
}

View file

@ -18,6 +18,11 @@ html {
color: var(--color-text); color: var(--color-text);
background-color: var(--color-background); background-color: var(--color-background);
font-size: 16px;
}
body {
font-size: 1em;
} }
h1, h1,
@ -30,8 +35,6 @@ h6 {
} }
p { p {
font-size: var(--font-body);
& + p { & + p {
margin-top: var(--spacing-small); margin-top: var(--spacing-small);
} }
@ -43,7 +46,7 @@ ol {
li { li {
list-style-position: outside; list-style-position: outside;
margin: var(--spacing-medium); margin: var(--spacing-xsmall) var(--spacing-medium);
margin-bottom: 0; margin-bottom: 0;
} }
} }

View file

@ -12,8 +12,8 @@
--color-link: var(--color-primary); --color-link: var(--color-primary);
--color-link-hover: #60e1ba; --color-link-hover: #60e1ba;
--color-link-active: #60e1ba; --color-link-active: #60e1ba;
--color-link-icon: #89939e; --color-link-icon: #6a7580;
--color-navigation-link: var(--color-link-icon); --color-navigation-link: #b3bcc6;
--color-button-primary-bg: var(--color-primary-alt); --color-button-primary-bg: var(--color-primary-alt);
--color-button-primary-bg-hover: #44796c; --color-button-primary-bg-hover: #44796c;
--color-button-primary-text: var(--color-primary); --color-button-primary-text: var(--color-primary);