[redesign] File page #963
46 changed files with 831 additions and 689 deletions
|
@ -20,5 +20,6 @@ module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
|
||||||
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
|
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
|
||||||
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/rewards\1'
|
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/rewards\1'
|
||||||
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
|
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
|
||||||
|
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/app\1'
|
||||||
|
|
||||||
[strict]
|
[strict]
|
||||||
|
|
3
flow-typed/react-markdown.js
vendored
Normal file
3
flow-typed/react-markdown.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-markdown' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
3
flow-typed/render-media.js
vendored
Normal file
3
flow-typed/render-media.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'render-media' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ class App extends React.PureComponent<Props> {
|
||||||
const { currentStackIndex: prevStackIndex } = prevProps;
|
const { currentStackIndex: prevStackIndex } = prevProps;
|
||||||
const { currentStackIndex, currentPageAttributes } = this.props;
|
const { currentStackIndex, currentPageAttributes } = this.props;
|
||||||
|
|
||||||
if (this.mainContent && currentStackIndex !== prevStackIndex) {
|
if (this.mainContent && currentStackIndex !== prevStackIndex && currentPageAttributes) {
|
||||||
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,62 +43,4 @@ export class CurrencySymbol extends React.PureComponent {
|
||||||
return <span>LBC</span>;
|
return <span>LBC</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Thumbnail extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
src: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleError() {
|
|
||||||
if (this.state.imageUrl != this._defaultImageUri) {
|
|
||||||
this.setState({
|
|
||||||
imageUri: this._defaultImageUri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this._defaultImageUri = lbry.imagePath('default-thumb.svg');
|
|
||||||
this._maxLoadTime = 10000;
|
|
||||||
this._isMounted = false;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
imageUri: this.props.src || this._defaultImageUri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._isMounted = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this._isMounted && !this.refs.img.complete) {
|
|
||||||
this.setState({
|
|
||||||
imageUri: this._defaultImageUri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, this._maxLoadTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this._isMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const className = this.props.className ? this.props.className : '',
|
|
||||||
otherProps = Object.assign({}, this.props);
|
|
||||||
delete otherProps.className;
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
ref="img"
|
|
||||||
onError={() => {
|
|
||||||
this.handleError();
|
|
||||||
}}
|
|
||||||
{...otherProps}
|
|
||||||
className={className}
|
|
||||||
src={this.state.imageUri}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
|
@ -2,15 +2,23 @@
|
||||||
/* eslint-disable react/no-multi-comp */
|
/* eslint-disable react/no-multi-comp */
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Button from 'component/link';
|
import Button from 'component/link';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
type FormRowProps = {
|
type FormRowProps = {
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
|
padded?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormRow = (props: FormRowProps) => {
|
export class FormRow extends React.PureComponent<FormRowProps> {
|
||||||
const { children } = props;
|
static defaultProps = {
|
||||||
return <div className="form-row">{children}</div>;
|
padded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, padded } = this.props;
|
||||||
|
return <div className={classnames('form-row', { 'form-row--padded': padded })}>{children}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type FormFieldProps = {
|
type FormFieldProps = {
|
||||||
render: () => React.Node,
|
render: () => React.Node,
|
||||||
|
@ -18,18 +26,18 @@ type FormFieldProps = {
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
|
helper?: string | React.Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FormField extends React.PureComponent<FormFieldProps> {
|
export class FormField extends React.PureComponent<FormFieldProps> {
|
||||||
render() {
|
render() {
|
||||||
const { render, label, prefix, postfix, error } = this.props;
|
const { render, label, prefix, postfix, error, helper } = this.props;
|
||||||
|
/* eslint-disable jsx-a11y/label-has-for */
|
||||||
|
// Will come back to this on the settings page
|
||||||
|
// Need htmlFor on the label
|
||||||
return (
|
return (
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
{label && (
|
{label && <label className="form-field__label">{label}</label>}
|
||||||
<label className="form-field__label">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<div className="form-field__wrapper">
|
<div className="form-field__wrapper">
|
||||||
{prefix && <span className="form-field__prefix">{prefix}</span>}
|
{prefix && <span className="form-field__prefix">{prefix}</span>}
|
||||||
{render()}
|
{render()}
|
||||||
|
@ -40,8 +48,10 @@ export class FormField extends React.PureComponent<FormFieldProps> {
|
||||||
{typeof error === 'string' ? error : __('There was an error')}
|
{typeof error === 'string' ? error : __('There was an error')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{helper && <div className="form-field__help">{helper}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable jsx-a11y/label-has-for */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
|
// @flow
|
||||||
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
export default ({ dark, className }) => (
|
const Spinner = () => (
|
||||||
<div
|
<div className="spinner">
|
||||||
className={classnames(
|
<div className="rect1" />
|
||||||
'spinner',
|
<div className="rect2" />
|
||||||
{
|
<div className="rect3" />
|
||||||
'spinner--dark': dark,
|
<div className="rect4" />
|
||||||
},
|
<div className="rect5" />
|
||||||
className
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
|
|
23
src/renderer/component/common/thumbnail.jsx
Normal file
23
src/renderer/component/common/thumbnail.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
src: string,
|
||||||
|
shouldObscure: boolean,
|
||||||
|
className: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Thumbnail = (props: Props) => {
|
||||||
|
const { className, src, shouldObscure } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={__('Image thumbnail')}
|
||||||
|
className={classnames({ 'card--obscured': shouldObscure }, className)}
|
||||||
|
src={src}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Thumbnail;
|
|
@ -1,48 +1,53 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import * as modals from 'constants/modal_types';
|
import * as modals from 'constants/modal_types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
class FileActions extends React.PureComponent {
|
type FileInfo = {
|
||||||
|
claim_id: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
openModal: (string, any) => void,
|
||||||
|
claimIsMine: boolean,
|
||||||
|
fileInfo: FileInfo,
|
||||||
|
vertical?: boolean, // should the buttons be stacked vertically?
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileActions extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { fileInfo, uri, openModal, claimIsMine } = this.props;
|
const { fileInfo, uri, openModal, claimIsMine, vertical } = this.props;
|
||||||
|
|
||||||
const claimId = fileInfo ? fileInfo.claim_id : null,
|
const claimId = fileInfo ? fileInfo.claim_id : '';
|
||||||
showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
// showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
||||||
|
|
||||||
|
const showDelete = true;
|
||||||
return (
|
return (
|
||||||
<section className="card__actions">
|
<section className={classnames('card__actions', { 'card__actions--vertical': vertical })}>
|
||||||
<FileDownloadLink uri={uri} />
|
<FileDownloadLink uri={uri} />
|
||||||
{showDelete && (
|
{showDelete && (
|
||||||
<Link
|
<Button
|
||||||
button="text"
|
alt
|
||||||
icon="icon-trash"
|
icon="Trash"
|
||||||
label={__('Remove')}
|
label={__('Remove')}
|
||||||
className="no-underline"
|
|
||||||
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
|
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
<Link
|
<Button
|
||||||
button="text"
|
alt
|
||||||
icon="icon-flag"
|
icon="Flag"
|
||||||
href={`https://lbry.io/dmca?claim_id=${claimId}`}
|
href={`https://lbry.io/dmca?claim_id=${claimId}`}
|
||||||
className="no-underline"
|
label={__('Report')}
|
||||||
label={__('report')}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Link
|
|
||||||
button="primary"
|
|
||||||
icon="icon-gift"
|
|
||||||
label={__('Support')}
|
|
||||||
navigate="/show"
|
|
||||||
className="card__action--right"
|
|
||||||
navigateParams={{ uri, tab: 'tip' }}
|
|
||||||
/>
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
<Link
|
<Button
|
||||||
button="alt"
|
icon="Edit3"
|
||||||
icon="icon-edit"
|
|
||||||
label={__('Edit')}
|
label={__('Edit')}
|
||||||
navigate="/publish"
|
navigate="/publish"
|
||||||
className="card__action--right"
|
className="card__action--right"
|
||||||
|
|
|
@ -1,70 +1,73 @@
|
||||||
import React from 'react';
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import lbry from 'lbry.js';
|
import lbry from 'lbry';
|
||||||
import FileActions from 'component/fileActions';
|
import Button from 'component/link';
|
||||||
import Link from 'component/link';
|
import path from 'path';
|
||||||
import DateTime from 'component/dateTime';
|
|
||||||
|
|
||||||
const path = require('path');
|
type Props = {
|
||||||
|
claim: {},
|
||||||
|
fileInfo: {
|
||||||
|
download_path: string,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
description: string,
|
||||||
|
language: string,
|
||||||
|
license: string,
|
||||||
|
},
|
||||||
|
openFolder: string => void,
|
||||||
|
contentType: string,
|
||||||
|
};
|
||||||
|
|
||||||
class FileDetails extends React.PureComponent {
|
const FileDetails = (props: Props) => {
|
||||||
render() {
|
const { claim, contentType, fileInfo, metadata, openFolder } = props;
|
||||||
const { claim, contentType, fileInfo, metadata, openFolder, uri } = this.props;
|
|
||||||
|
|
||||||
if (!claim || !metadata) {
|
|
||||||
return (
|
|
||||||
<div className="card__content">
|
|
||||||
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { description, language, license } = metadata;
|
|
||||||
const mediaType = lbry.getMediaType(contentType);
|
|
||||||
|
|
||||||
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
|
|
||||||
|
|
||||||
|
if (!claim || !metadata) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="card__content">
|
||||||
<div className="divider__horizontal" />
|
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
||||||
<FileActions uri={uri} />
|
</div>
|
||||||
<div className="divider__horizontal" />
|
);
|
||||||
<div className="card__content card__subtext card__subtext--allow-newlines">
|
}
|
||||||
|
|
||||||
|
const { description, language, license } = metadata;
|
||||||
|
const mediaType = lbry.getMediaType(contentType);
|
||||||
|
|
||||||
|
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="card__content">
|
||||||
|
<div className="card__subtext-title">About</div>
|
||||||
|
<div className="card__subtext">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
source={description || ''}
|
source={description || ''}
|
||||||
escapeHtml
|
escapeHtml
|
||||||
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
|
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="card__content">
|
<div className="card__subtext-title">Info</div>
|
||||||
<table className="table-standard table-stretch">
|
<div className="card__subtext">
|
||||||
<tbody>
|
<dl>
|
||||||
<tr>
|
<dt>{__('Content-Type')}</dt>
|
||||||
<td>{__('Content-Type')}</td>
|
<dd>{mediaType}</dd>
|
||||||
<td>{mediaType}</td>
|
<dt>{__('Language')}</dt>
|
||||||
</tr>
|
<dd>{language}</dd>
|
||||||
<tr>
|
<dt>{__('License')}</dt>
|
||||||
<td>{__('Language')}</td>
|
<dd>{license}</dd>
|
||||||
<td>{language}</td>
|
{downloadPath && (
|
||||||
</tr>
|
<React.Fragment>
|
||||||
<tr>
|
<dt>{__('Downloaded to')}</dt>
|
||||||
<td>{__('License')}</td>
|
<dd>
|
||||||
<td>{license}</td>
|
<Button fakeLink onClick={() => openFolder(downloadPath)} label={downloadPath} />
|
||||||
</tr>
|
</dd>
|
||||||
{downloadPath && (
|
</React.Fragment>
|
||||||
<tr>
|
)}
|
||||||
<td>{__('Downloaded to')}</td>
|
</dl>
|
||||||
<td>
|
|
||||||
<Link onClick={() => openFolder(downloadPath)}>{downloadPath}</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FileDetails;
|
export default FileDetails;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// I'll come back to this
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BusyMessage } from 'component/common';
|
import { BusyMessage } from 'component/common';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
@ -81,9 +83,8 @@ class FileDownloadLink extends React.PureComponent {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
button="text"
|
|
||||||
label={__('Download')}
|
label={__('Download')}
|
||||||
icon="icon-download"
|
icon="DownloadCloud"
|
||||||
className="no-underline"
|
className="no-underline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
purchaseUri(uri);
|
purchaseUri(uri);
|
||||||
|
@ -91,15 +92,7 @@ class FileDownloadLink extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (fileInfo && fileInfo.download_path) {
|
} else if (fileInfo && fileInfo.download_path) {
|
||||||
return (
|
return <Link label={__('Open')} icon="BookOpen" onClick={() => openFile()} />;
|
||||||
<Link
|
|
||||||
label={__('Open')}
|
|
||||||
button="text"
|
|
||||||
icon="icon-external-link-square"
|
|
||||||
className="no-underline"
|
|
||||||
onClick={() => openFile()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -107,3 +100,4 @@ class FileDownloadLink extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileDownloadLink;
|
export default FileDownloadLink;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
|
@ -38,7 +38,7 @@ class FilePrice extends React.PureComponent<Props> {
|
||||||
const isEstimate = costInfo ? !costInfo.includesData : false;
|
const isEstimate = costInfo ? !costInfo.includesData : false;
|
||||||
|
|
||||||
if (!costInfo) {
|
if (!costInfo) {
|
||||||
return <span className="credit-amount">???</span>;
|
return <span className="credit-amount">PRICE</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,6 +25,7 @@ type Props = {
|
||||||
noStyle: ?boolean,
|
noStyle: ?boolean,
|
||||||
noUnderline: ?boolean,
|
noUnderline: ?boolean,
|
||||||
description: ?string,
|
description: ?string,
|
||||||
|
secondary: ?boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Button = (props: Props) => {
|
const Button = (props: Props) => {
|
||||||
|
@ -49,6 +50,7 @@ const Button = (props: Props) => {
|
||||||
description,
|
description,
|
||||||
noStyle,
|
noStyle,
|
||||||
noUnderline,
|
noUnderline,
|
||||||
|
secondary,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -58,7 +60,8 @@ const Button = (props: Props) => {
|
||||||
? 'btn--no-style'
|
? 'btn--no-style'
|
||||||
: {
|
: {
|
||||||
'btn--link': fakeLink,
|
'btn--link': fakeLink,
|
||||||
'btn--primary': !alt && !fakeLink,
|
'btn--primary': !alt && !fakeLink && !secondary, // default to primary
|
||||||
|
'btn--secondary': secondary,
|
||||||
'btn--alt': alt,
|
'btn--alt': alt,
|
||||||
'btn--inverse': inverse,
|
'btn--inverse': inverse,
|
||||||
'btn--disabled': disabled,
|
'btn--disabled': disabled,
|
||||||
|
|
|
@ -1,7 +1,23 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
|
import type { Subscription } from 'redux/reducers/subscriptions';
|
||||||
|
|
||||||
export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannelUnsubscribe }) => {
|
type SubscribtionArgs = {
|
||||||
|
channelName: string,
|
||||||
|
uri: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelName: ?string,
|
||||||
|
uri: ?string,
|
||||||
|
subscriptions: Array<Subscription>,
|
||||||
|
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
|
||||||
|
doChannelUnsubscribe: SubscribtionArgs => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (props: Props) => {
|
||||||
|
const { channelName, uri, subscriptions, doChannelSubscribe, doChannelUnsubscribe } = props;
|
||||||
const isSubscribed =
|
const isSubscribed =
|
||||||
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
|
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
|
||||||
|
|
||||||
|
@ -10,18 +26,16 @@ export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannel
|
||||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||||
|
|
||||||
return channelName && uri ? (
|
return channelName && uri ? (
|
||||||
<div className="card__actions">
|
<Button
|
||||||
<Link
|
iconRight="AtSign"
|
||||||
iconRight={isSubscribed ? '' : 'at'}
|
alt={isSubscribed}
|
||||||
button={isSubscribed ? 'alt' : 'primary'}
|
label={subscriptionLabel}
|
||||||
label={subscriptionLabel}
|
onClick={() =>
|
||||||
onClick={() =>
|
subscriptionHandler({
|
||||||
subscriptionHandler({
|
channelName,
|
||||||
channelName,
|
uri,
|
||||||
uri,
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Spinner from 'component/common/spinner';
|
import Spinner from 'component/common/spinner';
|
||||||
|
|
||||||
const LoadingScreen = ({ status, spinner = true }) => (
|
type Props = {
|
||||||
<div className="video__loading-screen">
|
spinner: boolean,
|
||||||
<div>
|
status: string,
|
||||||
{spinner && <Spinner />}
|
};
|
||||||
|
|
||||||
<div className="video__loading-status">{status}</div>
|
class LoadingScreen extends React.PureComponent<Props> {
|
||||||
</div>
|
static defaultProps = {
|
||||||
</div>
|
spinner: true,
|
||||||
);
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, spinner } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="video__loading-screen">
|
||||||
|
{spinner && <Spinner />}
|
||||||
|
|
||||||
|
<span className="video__loading-text">{status}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default LoadingScreen;
|
export default LoadingScreen;
|
||||||
|
|
|
@ -1,47 +1,42 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
|
|
||||||
class VideoPlayButton extends React.PureComponent {
|
type Props = {
|
||||||
componentDidMount() {
|
play: string => void,
|
||||||
this.keyDownListener = this.onKeyDown.bind(this);
|
isLoading: boolean,
|
||||||
Play/pause from the spacebar seems to work fine without this. Maybe it's need for other OS's? Play/pause from the spacebar seems to work fine without this. Maybe it's need for other OS's?
yeah it should be fine without this... ill check linux yeah it should be fine without this... ill check linux
yeah it should be fine without this, I will check linux yeah it should be fine without this, I will check linux
ok i figured our why it was there. When you load the page, don't click anything, and press the space bar, this starts the video / initiates the download. When you take this out, it no longer works -- at least on linux. That said, I think its better to take out this functionality, since we are going to implement autoplay soon anyway. ok i figured our why it was there.
When you load the page, don't click anything, and press the space bar, this starts the video / initiates the download. When you take this out, it no longer works -- at least on linux.
That said, I think its better to take out this functionality, since we are going to implement autoplay soon anyway.
|
|||||||
document.addEventListener('keydown', this.keyDownListener);
|
uri: string,
|
||||||
}
|
mediaType: string,
|
||||||
|
fileInfo: ?{},
|
||||||
componentWillUnmount() {
|
};
|
||||||
document.removeEventListener('keydown', this.keyDownListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(event) {
|
|
||||||
if (event.target.tagName.toLowerCase() !== 'input' && event.code === 'Space') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.watch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class VideoPlayButton extends React.PureComponent<Props> {
|
||||||
watch() {
|
watch() {
|
||||||
this.props.play(this.props.uri);
|
this.props.play(this.props.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { button, label, isLoading, fileInfo, mediaType } = this.props;
|
const { isLoading, fileInfo, mediaType } = this.props;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
title={
|
TODO: Add title back to button
|
||||||
isLoading ? "Video is Loading" :
|
title={
|
||||||
!costInfo ? "Waiting on cost info..." :
|
isLoading ? "Video is Loading" :
|
||||||
fileInfo === undefined ? "Waiting on file info..." : ""
|
!costInfo ? "Waiting on cost info..." :
|
||||||
}
|
fileInfo === undefined ? "Waiting on file info..." : ""
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const disabled = isLoading || fileInfo === undefined;
|
const disabled = isLoading || fileInfo === undefined;
|
||||||
const icon = ['audio', 'video'].indexOf(mediaType) !== -1 ? 'icon-play' : 'icon-folder-o';
|
const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||||
|
const icon = doesPlayback ? 'Play' : 'Folder';
|
||||||
|
const label = doesPlayback ? 'Play' : 'View';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Button
|
||||||
button={button || null}
|
secondary
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={label || ''}
|
label={label}
|
||||||
className="video__play-button"
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
onClick={() => this.watch()}
|
onClick={() => this.watch()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const { remote } = require('electron');
|
// This file has a lot going on.
|
||||||
|
// I will add flow when I come back to fix the issue where the video player doesn't scale
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { remote } from 'electron';
|
||||||
import { Thumbnail } from 'component/common';
|
import { Thumbnail } from 'component/common';
|
||||||
import player from 'render-media';
|
import player from 'render-media';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -21,9 +23,9 @@ class VideoPlayer extends React.PureComponent {
|
||||||
this.togglePlayListener = this.togglePlay.bind(this);
|
this.togglePlayListener = this.togglePlay.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(next) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const el = this.refs.media.children[0];
|
const el = this.refs.media.children[0];
|
||||||
if (!this.props.paused && next.paused && !el.paused) el.pause();
|
if (!this.props.paused && nextProps.paused && !el.paused) el.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -171,7 +173,7 @@ class VideoPlayer extends React.PureComponent {
|
||||||
const needsMetadata = this.playableType();
|
const needsMetadata = this.playableType();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<React.Fragment>
|
||||||
{['audio', 'application'].indexOf(mediaType) !== -1 &&
|
{['audio', 'application'].indexOf(mediaType) !== -1 &&
|
||||||
(!this.playableType() || hasMetadata) &&
|
(!this.playableType() || hasMetadata) &&
|
||||||
!unplayable && <Thumbnail src={poster} className="video-embedded" />}
|
!unplayable && <Thumbnail src={poster} className="video-embedded" />}
|
||||||
|
@ -180,9 +182,10 @@ class VideoPlayer extends React.PureComponent {
|
||||||
!unplayable && <LoadingScreen status={noMetadataMessage} />}
|
!unplayable && <LoadingScreen status={noMetadataMessage} />}
|
||||||
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
|
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
|
||||||
<div ref="media" className="media" />
|
<div ref="media" className="media" />
|
||||||
</div>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VideoPlayer;
|
export default VideoPlayer;
|
||||||
|
/* eslint-disable */
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbry from 'lbry';
|
import lbry from 'lbry';
|
||||||
|
import classnames from 'classnames';
|
||||||
import VideoPlayer from './internal/player';
|
import VideoPlayer from './internal/player';
|
||||||
import VideoPlayButton from './internal/play-button';
|
import VideoPlayButton from './internal/play-button';
|
||||||
import LoadingScreen from './internal/loading-screen';
|
import LoadingScreen from './internal/loading-screen';
|
||||||
import NsfwOverlay from 'component/nsfwOverlay';
|
|
||||||
|
|
||||||
class Video extends React.PureComponent {
|
type Props = {
|
||||||
constructor(props) {
|
cancelPlay: () => void,
|
||||||
super(props);
|
fileInfo: {
|
||||||
this.state = {
|
outpoint: string,
|
||||||
showNsfwHelp: false,
|
file_name: string,
|
||||||
};
|
written_bytes: number,
|
||||||
}
|
download_path: string,
|
||||||
|
completed: boolean,
|
||||||
|
},
|
||||||
|
metadata: ?{
|
||||||
|
nsfw: boolean,
|
||||||
|
thumbnail: string,
|
||||||
|
},
|
||||||
|
isLoading: boolean,
|
||||||
|
isDownloading: boolean,
|
||||||
|
playingUri: ?string,
|
||||||
|
contentType: string,
|
||||||
|
changeVolume: number => void,
|
||||||
|
volume: number,
|
||||||
|
claim: {},
|
||||||
|
uri: string,
|
||||||
|
doPlay: () => void,
|
||||||
|
doPause: () => void,
|
||||||
|
savePosition: (string, number) => void,
|
||||||
|
mediaPaused: boolean,
|
||||||
|
mediaPosition: ?number,
|
||||||
|
className: ?string,
|
||||||
|
obscureNsfw: boolean,
|
||||||
|
play: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Video extends React.PureComponent<Props> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.cancelPlay();
|
this.props.cancelPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
isMediaSame(nextProps) {
|
isMediaSame(nextProps: Props) {
|
||||||
return (
|
return (
|
||||||
this.props.fileInfo &&
|
this.props.fileInfo &&
|
||||||
nextProps.fileInfo &&
|
nextProps.fileInfo &&
|
||||||
|
@ -25,22 +50,6 @@ class Video extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseOver() {
|
|
||||||
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
|
|
||||||
this.setState({
|
|
||||||
showNsfwHelp: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseOut() {
|
|
||||||
if (this.state.showNsfwHelp) {
|
|
||||||
this.setState({
|
|
||||||
showNsfwHelp: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
|
@ -58,11 +67,14 @@ class Video extends React.PureComponent {
|
||||||
savePosition,
|
savePosition,
|
||||||
mediaPaused,
|
mediaPaused,
|
||||||
mediaPosition,
|
mediaPosition,
|
||||||
|
className,
|
||||||
|
obscureNsfw,
|
||||||
|
play,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isPlaying = playingUri === uri;
|
const isPlaying = playingUri === uri;
|
||||||
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
|
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
|
||||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
|
||||||
const mediaType = lbry.getMediaType(contentType, fileInfo && fileInfo.file_name);
|
const mediaType = lbry.getMediaType(contentType, fileInfo && fileInfo.file_name);
|
||||||
|
|
||||||
let loadStatusMessage = '';
|
let loadStatusMessage = '';
|
||||||
|
@ -77,23 +89,10 @@ class Video extends React.PureComponent {
|
||||||
loadStatusMessage = __('Downloading stream... not long left now!');
|
loadStatusMessage = __('Downloading stream... not long left now!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const klasses = [];
|
const poster = metadata && metadata.thumbnail;
|
||||||
klasses.push(obscureNsfw ? 'video--obscured ' : '');
|
|
||||||
if (isLoading || isDownloading) klasses.push('video-embedded', 'video');
|
|
||||||
if (mediaType === 'video') {
|
|
||||||
klasses.push('video-embedded', 'video');
|
|
||||||
klasses.push(isPlaying ? 'video--active' : 'video--hidden');
|
|
||||||
} else if (mediaType === 'application') {
|
|
||||||
klasses.push('video-embedded');
|
|
||||||
} else if (!isPlaying) klasses.push('video-embedded');
|
|
||||||
const poster = metadata.thumbnail;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classnames('video', {}, className)}>
|
||||||
className={klasses.join(' ')}
|
|
||||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
|
||||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
|
||||||
>
|
|
||||||
{isPlaying &&
|
{isPlaying &&
|
||||||
(!isReadyToPlay ? (
|
(!isReadyToPlay ? (
|
||||||
<LoadingScreen status={loadStatusMessage} />
|
<LoadingScreen status={loadStatusMessage} />
|
||||||
|
@ -117,11 +116,19 @@ class Video extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isPlaying && (
|
{!isPlaying && (
|
||||||
<div className="video__cover" style={{ backgroundImage: `url("${metadata.thumbnail}")` }}>
|
<div
|
||||||
<VideoPlayButton {...this.props} mediaType={mediaType} />
|
className={classnames('video__cover', { 'card--obscured': shouldObscureNsfw })}
|
||||||
|
style={!shouldObscureNsfw && poster ? { backgroundImage: `url("${poster}")` } : {}}
|
||||||
|
>
|
||||||
|
<VideoPlayButton
|
||||||
|
play={play}
|
||||||
|
fileInfo={fileInfo}
|
||||||
|
uri={uri}
|
||||||
|
isLoading={isLoading}
|
||||||
|
mediaType={mediaType}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.showNsfwHelp && <NsfwOverlay />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,92 @@
|
||||||
// I'll come back to this
|
// @flow
|
||||||
/* eslint-disable */
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
import { FormRow } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
|
||||||
class WalletSendTip extends React.PureComponent {
|
type Props = {
|
||||||
constructor(props) {
|
claim_id: string,
|
||||||
|
uri: string,
|
||||||
|
title: string,
|
||||||
|
errorMessage: string,
|
||||||
|
isPending: boolean,
|
||||||
|
sendSupport: (number, string, string) => void,
|
||||||
|
onCancel: () => void,
|
||||||
|
sendTipCallback?: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
amount: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
class WalletSendTip extends React.PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
amount: 0.0,
|
amount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(this: any).handleSendButtonClicked = this.handleSendButtonClicked.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSendButtonClicked() {
|
handleSendButtonClicked() {
|
||||||
const { claim_id, uri } = this.props;
|
const { claim_id: claimId, uri, sendSupport, sendTipCallback } = this.props;
|
||||||
const amount = this.state.amount;
|
const { amount } = this.state;
|
||||||
this.props.sendSupport(amount, claim_id, uri);
|
|
||||||
|
sendSupport(amount, claimId, uri);
|
||||||
I dont think it really matters, but isn't the better way to do this to make a type for WalletSendTip? I dont think it really matters, but isn't the better way to do this to make a type for WalletSendTip?
Not sure I follow Not sure I follow
|
|||||||
|
|
||||||
|
// ex: close modal
|
||||||
|
if (sendTipCallback) {
|
||||||
|
sendTipCallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSupportPriceChange(event) {
|
handleSupportPriceChange(event: SyntheticInputEvent<*>) {
|
||||||
this.setState({
|
this.setState({
|
||||||
amount: Number(event.target.value),
|
amount: Number(event.target.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { errorMessage, isPending, title, uri } = this.props;
|
const { errorMessage, isPending, title, uri, onCancel } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="card__title-primary">
|
<div className="card__title-primary">
|
||||||
<h1>
|
<h1>
|
||||||
{__('Support')} <UriIndicator uri={uri} />
|
{__('Send a tip to')} <UriIndicator uri={uri} />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
<FormRow
|
<FormField
|
||||||
label={__('Amount')}
|
label={__('Amount')}
|
||||||
postfix={__('LBC')}
|
postfix={__('LBC')}
|
||||||
min="0"
|
error={errorMessage}
|
||||||
step="any"
|
|
||||||
type="number"
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
helper={
|
helper={
|
||||||
<span>
|
<span>
|
||||||
{`${__('This will appear as a tip for "%s" located at %s.', title, uri)} `}
|
{__(`This will appear as a tip for ${title} located at ${uri}.`)}
|
||||||
<Link label={__('Learn more')} href="https://lbry.io/faq/tipping" />
|
{" "}
|
||||||
|
<Button label={__('Learn more')} fakeLink href="https://lbry.io/faq/tipping" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placeholder="1.00"
|
render={() => (
|
||||||
onChange={event => this.handleSupportPriceChange(event)}
|
<input
|
||||||
|
min="0"
|
||||||
|
step="any"
|
||||||
|
type="number"
|
||||||
|
placeholder="1.00"
|
||||||
|
onChange={event => this.handleSupportPriceChange(event)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="form-row-submit">
|
<div className="card__actions">
|
||||||
<Link
|
<Button
|
||||||
label={__('Send')}
|
label={__('Send')}
|
||||||
button="primary"
|
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={this.handleSendButtonClicked.bind(this)}
|
onClick={this.handleSendButtonClicked}
|
||||||
/>
|
/>
|
||||||
<Link label={__('Cancel')} button="alt" navigate="/show" navigateParams={{ uri }} />
|
<Button alt label={__('Cancel')} onClick={onCancel} navigateParams={{ uri }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,4 +95,3 @@ class WalletSendTip extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WalletSendTip;
|
export default WalletSendTip;
|
||||||
/* eslint-enable */
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ class WunderBar extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e: SyntheticInputEvent<*>) {
|
handleChange(e: SyntheticInputEvent<*>) {
|
||||||
const { updateSearchQuery, getSearchSuggestions } = this.props;
|
const { updateSearchQuery } = this.props;
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
|
|
||||||
updateSearchQuery(value);
|
updateSearchQuery(value);
|
||||||
|
@ -74,7 +74,6 @@ class WunderBar extends React.PureComponent<Props> {
|
||||||
|
|
||||||
input: ?HTMLInputElement;
|
input: ?HTMLInputElement;
|
||||||
throttledGetSearchSuggestions: string => void;
|
throttledGetSearchSuggestions: string => void;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { searchQuery, isActive, address, suggestions } = this.props;
|
const { searchQuery, isActive, address, suggestions } = this.props;
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,4 @@ export const TRANSACTION_FAILED = 'transaction_failed';
|
||||||
export const REWARD_APPROVAL_REQUIRED = 'reward_approval_required';
|
export const REWARD_APPROVAL_REQUIRED = 'reward_approval_required';
|
||||||
export const AFFIRM_PURCHASE = 'affirm_purchase';
|
export const AFFIRM_PURCHASE = 'affirm_purchase';
|
||||||
export const CONFIRM_CLAIM_REVOKE = 'confirmClaimRevoke';
|
export const CONFIRM_CLAIM_REVOKE = 'confirmClaimRevoke';
|
||||||
|
export const SEND_TIP = 'sendTip';
|
||||||
|
|
|
@ -1,26 +1,36 @@
|
||||||
import React from 'react';
|
// @flow
|
||||||
import PropTypes from 'prop-types';
|
/* eslint-disable react/no-multi-comp */
|
||||||
|
// These should probably just be combined into one modal component
|
||||||
|
import * as React from 'react';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import Link from 'component/link/index';
|
import Button from 'component/link/index';
|
||||||
import app from 'app';
|
import app from 'app';
|
||||||
|
|
||||||
export class Modal extends React.PureComponent {
|
type ModalProps = {
|
||||||
static propTypes = {
|
type: string,
|
||||||
type: PropTypes.oneOf(['alert', 'confirm', 'custom']),
|
overlay: boolean,
|
||||||
overlay: PropTypes.bool,
|
confirmButtonLabel: string,
|
||||||
onConfirmed: PropTypes.func,
|
abortButtonLabel: string,
|
||||||
onAborted: PropTypes.func,
|
confirmButtonDisabled: boolean,
|
||||||
confirmButtonLabel: PropTypes.string,
|
abortButtonDisabled: boolean,
|
||||||
abortButtonLabel: PropTypes.string,
|
onConfirmed?: any => any,
|
||||||
confirmButtonDisabled: PropTypes.bool,
|
onAborted?: any => any,
|
||||||
abortButtonDisabled: PropTypes.bool,
|
className?: string,
|
||||||
};
|
overlayClassName?: string,
|
||||||
|
children?: React.Node,
|
||||||
|
extraContent?: React.Node,
|
||||||
|
expandButtonLabel?: string,
|
||||||
|
hideButtonLabel?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Modal extends React.PureComponent<ModalProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
type: 'alert',
|
type: 'alert',
|
||||||
overlay: true,
|
overlay: true,
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
confirmButtonLabel: app.i18n.__('OK'),
|
confirmButtonLabel: app.i18n.__('OK'),
|
||||||
abortButtonLabel: app.i18n.__('Cancel'),
|
abortButtonLabel: app.i18n.__('Cancel'),
|
||||||
|
/* eslint-enable no-underscore-dangle */
|
||||||
confirmButtonDisabled: false,
|
confirmButtonDisabled: false,
|
||||||
abortButtonDisabled: false,
|
abortButtonDisabled: false,
|
||||||
};
|
};
|
||||||
|
@ -38,20 +48,17 @@ export class Modal extends React.PureComponent {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>{this.props.children}</div>
|
<div>{this.props.children}</div>
|
||||||
{this.props.type == 'custom' ? null : ( // custom modals define their own buttons
|
{this.props.type === 'custom' ? null : ( // custom modals define their own buttons
|
||||||
<div className="modal__buttons">
|
<div className="card__actions card__actions--center">
|
||||||
<Link
|
<Button
|
||||||
button="primary"
|
|
||||||
label={this.props.confirmButtonLabel}
|
label={this.props.confirmButtonLabel}
|
||||||
className="modal__button"
|
|
||||||
disabled={this.props.confirmButtonDisabled}
|
disabled={this.props.confirmButtonDisabled}
|
||||||
onClick={this.props.onConfirmed}
|
onClick={this.props.onConfirmed}
|
||||||
/>
|
/>
|
||||||
{this.props.type == 'confirm' ? (
|
{this.props.type === 'confirm' ? (
|
||||||
<Link
|
<Button
|
||||||
button="alt"
|
alt
|
||||||
label={this.props.abortButtonLabel}
|
label={this.props.abortButtonLabel}
|
||||||
className="modal__button"
|
|
||||||
disabled={this.props.abortButtonDisabled}
|
disabled={this.props.abortButtonDisabled}
|
||||||
onClick={this.props.onAborted}
|
onClick={this.props.onAborted}
|
||||||
/>
|
/>
|
||||||
|
@ -63,19 +70,20 @@ export class Modal extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpandableModal extends React.PureComponent {
|
type State = {
|
||||||
static propTypes = {
|
expanded: boolean,
|
||||||
expandButtonLabel: PropTypes.string,
|
};
|
||||||
extraContent: PropTypes.element,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export class ExpandableModal extends React.PureComponent<ModalProps, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
confirmButtonLabel: app.i18n.__('OK'),
|
confirmButtonLabel: app.i18n.__('OK'),
|
||||||
expandButtonLabel: app.i18n.__('Show More...'),
|
expandButtonLabel: app.i18n.__('Show More...'),
|
||||||
hideButtonLabel: app.i18n.__('Show Less'),
|
hideButtonLabel: app.i18n.__('Show Less'),
|
||||||
|
/* eslint-enable no-underscore-dangle */
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: ModalProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -95,13 +103,13 @@ export class ExpandableModal extends React.PureComponent {
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
{this.state.expanded ? this.props.extraContent : null}
|
{this.state.expanded ? this.props.extraContent : null}
|
||||||
<div className="modal__buttons">
|
<div className="modal__buttons">
|
||||||
<Link
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
label={this.props.confirmButtonLabel}
|
label={this.props.confirmButtonLabel}
|
||||||
className="modal__button"
|
className="modal__button"
|
||||||
onClick={this.props.onConfirmed}
|
onClick={this.props.onConfirmed}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
|
label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
|
||||||
className="modal__button"
|
className="modal__button"
|
||||||
|
@ -116,3 +124,4 @@ export class ExpandableModal extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Modal;
|
export default Modal;
|
||||||
|
/* eslint-enable react/no-multi-comp */
|
|
@ -1,26 +1,49 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal } from 'modal/modal';
|
import { Modal } from 'modal/modal';
|
||||||
import FormField from 'component/formField/index';
|
import { FormRow, FormField } from 'component/common/form';
|
||||||
|
|
||||||
class ModalRemoveFile extends React.PureComponent {
|
type Props = {
|
||||||
constructor(props) {
|
claimIsMine: boolean,
|
||||||
|
closeModal: () => void,
|
||||||
|
deleteFile: (string, boolean, boolean) => void,
|
||||||
|
title: string,
|
||||||
|
fileInfo: {
|
||||||
|
outpoint: string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
deleteChecked: boolean,
|
||||||
|
abandonClaimChecked: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModalRemoveFile extends React.PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
deleteChecked: false,
|
deleteChecked: true,
|
||||||
abandonClaimChecked: false,
|
abandonClaimChecked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(this: any).handleDeleteCheckboxClicked = this.handleDeleteCheckboxClicked.bind(this);
|
||||||
|
(this: any).handleAbandonClaimCheckboxClicked = this.handleAbandonClaimCheckboxClicked.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteCheckboxClicked(event) {
|
handleDeleteCheckboxClicked() {
|
||||||
|
const { deleteChecked } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
deleteChecked: event.target.checked,
|
deleteChecked: !deleteChecked,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAbandonClaimCheckboxClicked(event) {
|
handleAbandonClaimCheckboxClicked() {
|
||||||
|
const { abandonClaimChecked } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
abandonClaimChecked: event.target.checked,
|
abandonClaimChecked: !abandonClaimChecked,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,26 +61,34 @@ class ModalRemoveFile extends React.PureComponent {
|
||||||
onAborted={closeModal}
|
onAborted={closeModal}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{__("Are you sure you'd like to remove")} <cite>{title}</cite> {__('from LBRY?')}
|
{__("Are you sure you'd like to remove")} <cite>{title}</cite> {__('from the LBRY app?')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section>
|
<FormRow padded>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
prefix={__('Also delete this file from my computer')}
|
||||||
checked={deleteChecked}
|
render={() => (
|
||||||
onClick={this.handleDeleteCheckboxClicked.bind(this)}
|
<input
|
||||||
label={__('Delete this file from my computer')}
|
type="checkbox"
|
||||||
|
checked={deleteChecked}
|
||||||
|
onChange={this.handleDeleteCheckboxClicked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</FormRow>
|
||||||
{claimIsMine && (
|
{!claimIsMine && (
|
||||||
<section>
|
<FormRow>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
prefix={__('Abandon the claim for this URI')}
|
||||||
checked={abandonClaimChecked}
|
render={() => (
|
||||||
onClick={this.handleAbandonClaimCheckboxClicked.bind(this)}
|
<input
|
||||||
label={__('Abandon the claim for this URI')}
|
type="checkbox"
|
||||||
|
checked={abandonClaimChecked}
|
||||||
|
onChange={this.handleAbandonClaimCheckboxClicked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</FormRow>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import ModalAffirmPurchase from 'modal/modalAffirmPurchase';
|
||||||
import ModalRevokeClaim from 'modal/modalRevokeClaim';
|
import ModalRevokeClaim from 'modal/modalRevokeClaim';
|
||||||
import ModalEmailCollection from '../modalEmailCollection';
|
import ModalEmailCollection from '../modalEmailCollection';
|
||||||
import ModalPhoneCollection from '../modalPhoneCollection';
|
import ModalPhoneCollection from '../modalPhoneCollection';
|
||||||
|
import ModalSendTip from '../modalSendTip';
|
||||||
import * as modals from 'constants/modal_types';
|
import * as modals from 'constants/modal_types';
|
||||||
|
|
||||||
class ModalRouter extends React.PureComponent {
|
class ModalRouter extends React.PureComponent {
|
||||||
|
@ -129,6 +130,8 @@ class ModalRouter extends React.PureComponent {
|
||||||
return <ModalPhoneCollection {...modalProps} />;
|
return <ModalPhoneCollection {...modalProps} />;
|
||||||
case modals.EMAIL_COLLECTION:
|
case modals.EMAIL_COLLECTION:
|
||||||
return <ModalEmailCollection {...modalProps} />;
|
return <ModalEmailCollection {...modalProps} />;
|
||||||
|
case modals.SEND_TIP:
|
||||||
|
return <ModalSendTip {...modalProps} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
9
src/renderer/modal/modalSendTip/index.js
Normal file
9
src/renderer/modal/modalSendTip/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doCloseModal } from 'redux/actions/app';
|
||||||
|
import ModalSendTip from './view';
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(ModalSendTip);
|
23
src/renderer/modal/modalSendTip/view.jsx
Normal file
23
src/renderer/modal/modalSendTip/view.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
import SendTip from 'component/walletSendTip';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closeModal: () => void,
|
||||||
|
uri: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModalSendTip extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { closeModal, uri } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen type="custom">
|
||||||
|
<SendTip uri={uri} onCancel={closeModal} sendTipCallback={closeModal} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalSendTip;
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import { doNavigate } from 'redux/actions/navigation';
|
import { doNavigate } from 'redux/actions/navigation';
|
||||||
import { doFetchFileInfo } from 'redux/actions/file_info';
|
import { doFetchFileInfo } from 'redux/actions/file_info';
|
||||||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||||
import { selectRewardContentClaimIds } from 'redux/selectors/content';
|
import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content';
|
||||||
import { doFetchCostInfoForUri } from 'redux/actions/cost_info';
|
import { doFetchCostInfoForUri } from 'redux/actions/cost_info';
|
||||||
import {
|
import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
|
@ -11,8 +11,9 @@ import {
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
|
import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
|
||||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
|
import { selectMediaPaused } from 'redux/selectors/media';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import FilePage from './view';
|
import FilePage from './view';
|
||||||
import { makeSelectCurrentParam } from 'redux/selectors/navigation';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
@ -20,15 +21,17 @@ const select = (state, props) => ({
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
tab: makeSelectCurrentParam('tab')(state),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||||
|
playingUri: selectPlayingUri(state),
|
||||||
|
isPaused: selectMediaPaused(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||||
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)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(FilePage);
|
export default connect(select, perform)(FilePage);
|
||||||
|
|
|
@ -1,37 +1,69 @@
|
||||||
/* eslint-disable */
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbry from 'lbry';
|
import lbry from 'lbry';
|
||||||
import { buildURI, normalizeURI } from 'lbryURI';
|
import { buildURI, normalizeURI } from 'lbryURI';
|
||||||
import Video from 'component/video';
|
import Video from 'component/video';
|
||||||
import { Thumbnail } from 'component/common';
|
import Thumbnail from 'component/common/thumbnail';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import FileDetails from 'component/fileDetails';
|
import FileDetails from 'component/fileDetails';
|
||||||
|
import FileActions from 'component/fileActions';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import WalletSendTip from 'component/walletSendTip';
|
import WalletSendTip from 'component/walletSendTip';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import player from 'render-media';
|
||||||
|
import * as modals from 'constants/modal_types';
|
||||||
|
|
||||||
class FilePage extends React.PureComponent {
|
type Props = {
|
||||||
|
claim: {
|
||||||
|
claim_id: string,
|
||||||
|
height: number,
|
||||||
|
channel_name: string,
|
||||||
|
value: {
|
||||||
|
publisherSignature: ?{
|
||||||
|
certificateId: ?string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fileInfo: {},
|
||||||
|
metadata: {
|
||||||
|
title: string,
|
||||||
|
thumbnail: string,
|
||||||
|
nsfw: boolean
|
||||||
|
},
|
||||||
|
contentType: string,
|
||||||
|
uri: string,
|
||||||
|
rewardedContentClaimIds: Array<string>,
|
||||||
|
obscureNsfw: boolean,
|
||||||
|
playingUri: ?string,
|
||||||
|
isPaused: boolean,
|
||||||
|
openModal: (string, any) => void,
|
||||||
|
fetchFileInfo: (string) => void,
|
||||||
|
fetchCostInfo: (string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilePage extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchFileInfo(this.props);
|
this.fetchFileInfo(this.props);
|
||||||
this.fetchCostInfo(this.props);
|
this.fetchCostInfo(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
this.fetchFileInfo(nextProps);
|
this.fetchFileInfo(nextProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFileInfo(props) {
|
fetchFileInfo(props: Props) {
|
||||||
if (props.fileInfo === undefined) {
|
if (props.fileInfo === undefined) {
|
||||||
props.fetchFileInfo(props.uri);
|
props.fetchFileInfo(props.uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCostInfo(props) {
|
fetchCostInfo(props: Props) {
|
||||||
if (props.costInfo === undefined) {
|
if (props.costInfo === undefined) {
|
||||||
props.fetchCostInfo(props.uri);
|
props.fetchCostInfo(props.uri);
|
||||||
}
|
}
|
||||||
|
@ -43,74 +75,85 @@ class FilePage extends React.PureComponent {
|
||||||
fileInfo,
|
fileInfo,
|
||||||
metadata,
|
metadata,
|
||||||
contentType,
|
contentType,
|
||||||
tab,
|
|
||||||
uri,
|
uri,
|
||||||
rewardedContentClaimIds,
|
rewardedContentClaimIds,
|
||||||
|
obscureNsfw,
|
||||||
|
playingUri,
|
||||||
|
isPaused,
|
||||||
|
openModal,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showTipBox = tab == 'tip';
|
// This should be included below in the page
|
||||||
|
// Come back to me
|
||||||
if (!claim || !metadata) {
|
if (!claim || !metadata) {
|
||||||
return <span className="empty">{__('Empty claim or metadata info.')}</span>;
|
return <span className="empty">{__('Empty claim or metadata info.')}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File info
|
||||||
const title = metadata.title;
|
const title = metadata.title;
|
||||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||||
|
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
|
||||||
|
const thumbnail = metadata.thumbnail;
|
||||||
|
const { height, channel_name: channelName, value } = claim;
|
||||||
const mediaType = lbry.getMediaType(contentType);
|
const mediaType = lbry.getMediaType(contentType);
|
||||||
const player = require('render-media');
|
|
||||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
|
||||||
const isPlayable =
|
const isPlayable =
|
||||||
Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio';
|
Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio';
|
||||||
const { height, channel_name: channelName, value } = claim;
|
|
||||||
const channelClaimId =
|
const channelClaimId =
|
||||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||||
|
|
||||||
let subscriptionUri;
|
let subscriptionUri;
|
||||||
if (channelName && channelClaimId) {
|
if (channelName && channelClaimId) {
|
||||||
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
|
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPlaying = playingUri === uri && !isPaused;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
|
<section className="card">
|
||||||
<div className="show-page-media">
|
<div>
|
||||||
{isPlayable ? (
|
{isPlayable ? (
|
||||||
<Video className="video-embedded" uri={uri} />
|
<Video className="video__embedded" uri={uri} />
|
||||||
) : metadata && metadata.thumbnail ? (
|
|
||||||
<Thumbnail src={metadata.thumbnail} />
|
|
||||||
) : (
|
) : (
|
||||||
<Thumbnail />
|
<Thumbnail
|
||||||
|
shouldObscure={shouldObscureThumbnail}
|
||||||
|
className="video__embedded"
|
||||||
|
src={thumbnail}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
{!isPlaying && (
|
||||||
<div className="card__inner">
|
<div className="card-media__internal-links">
|
||||||
{(!tab || tab === 'details') && (
|
<FileActions uri={uri} vertical />
|
||||||
<div>
|
|
||||||
{' '}
|
|
||||||
<div className="card__title-identity">
|
|
||||||
{!fileInfo || fileInfo.written_bytes <= 0 ? (
|
|
||||||
<span style={{ float: 'right' }}>
|
|
||||||
<FilePrice uri={normalizeURI(uri)} />
|
|
||||||
{isRewardContent && (
|
|
||||||
<span>
|
|
||||||
{' '}
|
|
||||||
<Icon icon={icons.FEATURED} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<h1>{title}</h1>
|
|
||||||
<div className="card__subtitle card--file-subtitle">
|
|
||||||
<UriIndicator uri={uri} link />
|
|
||||||
<span className="card__publish-date">
|
|
||||||
Published on <DateTime block={height} show={DateTime.SHOW_DATE} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
|
||||||
<FileDetails uri={uri} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
</div>
|
||||||
|
<div className="card--content">
|
||||||
|
<div className="card__title-identity--file">
|
||||||
|
<h1 className="card__title">{title}</h1>
|
||||||
|
<div className="card__title-identity-icons">
|
||||||
|
<FilePrice uri={normalizeURI(uri)} />
|
||||||
|
{isRewardContent && <Icon icon={icons.FEATURED} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="card__subtitle card__subtitle--file">
|
||||||
|
{__('Published on')} <DateTime block={height} show={DateTime.SHOW_DATE} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="card__channel-info">
|
||||||
|
<UriIndicator uri={uri} link />
|
||||||
|
<div className="card__actions card__actions--no-margin">
|
||||||
|
<Button
|
||||||
|
alt
|
||||||
|
iconRight="Send"
|
||||||
|
label={__('Enjoy this? Send a tip')}
|
||||||
|
onClick={() => openModal(modals.SEND_TIP, { uri })}
|
||||||
|
/>
|
||||||
|
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card--content">
|
||||||
|
<FileDetails uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -119,4 +162,3 @@ class FilePage extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilePage;
|
export default FilePage;
|
||||||
/* eslint-enable */
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as actions from 'constants/action_types';
|
|
||||||
import type { Dispatch } from 'redux/reducers/video';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const setVideoPause = (data: boolean) => (dispatch: Dispatch) =>
|
|
||||||
dispatch({
|
|
||||||
type: actions.SET_VIDEO_PAUSE,
|
|
||||||
data,
|
|
||||||
});
|
|
|
@ -3,6 +3,7 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
playingUri: null,
|
playingUri: null,
|
||||||
|
currentlyIsPlaying: false,
|
||||||
rewardedContentClaimIds: [],
|
rewardedContentClaimIds: [],
|
||||||
channelClaimCounts: {},
|
channelClaimCounts: {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as ACTIONS from 'constants/action_types';
|
|
||||||
import { handleActions } from 'util/redux-utils';
|
|
||||||
|
|
||||||
export type VideoState = { videoPause: boolean };
|
|
||||||
|
|
||||||
type setVideoPause = {
|
|
||||||
type: ACTIONS.SET_VIDEO_PAUSE,
|
|
||||||
data: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Action = setVideoPause;
|
|
||||||
export type Dispatch = (action: Action) => any;
|
|
||||||
|
|
||||||
const defaultState = { videoPause: false };
|
|
||||||
|
|
||||||
export default handleActions(
|
|
||||||
{
|
|
||||||
[ACTIONS.SET_VIDEO_PAUSE]: (state: VideoState, action: setVideoPause): VideoState => ({
|
|
||||||
...state,
|
|
||||||
videoPause: action.data,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
defaultState
|
|
||||||
);
|
|
|
@ -42,15 +42,12 @@ export const selectActiveHistoryEntry = createSelector(
|
||||||
state => state.stack[state.index]
|
state => state.stack[state.index]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectPageTitle = createSelector(
|
export const selectPageTitle = createSelector(selectCurrentPage, page => {
|
||||||
selectCurrentPage,
|
switch (page) {
|
||||||
(page) => {
|
default:
|
||||||
switch (page) {
|
return '';
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
export const selectNavLinks = createSelector(
|
export const selectNavLinks = createSelector(
|
||||||
selectCurrentPage,
|
selectCurrentPage,
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { selectCurrentPage, selectCurrentParams } from 'redux/selectors/navigation';
|
||||||
selectCurrentPage,
|
|
||||||
selectCurrentParams,
|
|
||||||
selectPageTitle,
|
|
||||||
} from 'redux/selectors/navigation';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
export const selectState = state => state.search || {};
|
export const selectState = state => state.search || {};
|
||||||
|
@ -26,13 +22,13 @@ export const makeSelectSearchUris = query =>
|
||||||
|
|
||||||
export const selectWunderBarAddress = createSelector(
|
export const selectWunderBarAddress = createSelector(
|
||||||
selectCurrentPage,
|
selectCurrentPage,
|
||||||
selectPageTitle,
|
|
||||||
selectSearchQuery,
|
selectSearchQuery,
|
||||||
(page, title, query) => {
|
selectCurrentParams,
|
||||||
|
(page, query, params) => {
|
||||||
// only populate the wunderbar address if we are on the file/channel pages
|
// only populate the wunderbar address if we are on the file/channel pages
|
||||||
// or show the search query
|
// or show the search query
|
||||||
if (page === 'show') {
|
if (page === 'show') {
|
||||||
return title;
|
return params.uri;
|
||||||
} else if (page === 'search') {
|
} else if (page === 'search') {
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
const selectState = state => state.video || {};
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const selectVideoPause = createSelector(selectState, state => state.videoPause);
|
|
|
@ -6,6 +6,14 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
src: url('../../../static/font/metropolis/Metropolis-Medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Metropolis';
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
src: url('../../../static/font/metropolis/Metropolis-Regular.woff2') format('woff2');
|
src: url('../../../static/font/metropolis/Metropolis-Regular.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +94,15 @@ ul {
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
cursor: pointer;
|
||||||
cursor: text;
|
|
||||||
border-bottom: var(--input-border-size) solid var(--input-border-color);
|
border-bottom: var(--input-border-size) solid var(--input-border-color);
|
||||||
color: var(--input-color);
|
color: var(--input-color);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
|
&[type='text'] {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
&.input-copyable {
|
&.input-copyable {
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
color: var(--input-disabled-color);
|
color: var(--input-disabled-color);
|
||||||
|
@ -101,8 +112,28 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
dl {
|
||||||
*/
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
float: left;
|
||||||
|
width: 20%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
float: left;
|
||||||
|
width: 80%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: var(--header-height) calc(100vh - var(--header-height));
|
grid-template-rows: var(--header-height) calc(100vh - var(--header-height));
|
||||||
|
@ -176,11 +207,11 @@ input {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.7em;
|
font-size: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit-amount--free {
|
.credit-amount--free {
|
||||||
color: var(--color-black);
|
color: var(--color-primary);
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +235,11 @@ input {
|
||||||
// font-weight: 700;
|
// font-weight: 700;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
.divider__horizontal {
|
||||||
|
border-top: var(--color-divider);
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -259,16 +295,16 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-section {
|
// .sort-section {
|
||||||
display: block;
|
// display: block;
|
||||||
margin-bottom: $spacing-vertical * 2/3;
|
// margin-bottom: $spacing-vertical * 2/3;
|
||||||
|
//
|
||||||
text-align: right;
|
// text-align: right;
|
||||||
line-height: 1;
|
// line-height: 1;
|
||||||
font-size: 0.85em;
|
// font-size: 0.85em;
|
||||||
color: var(--color-help);
|
// color: var(--color-help);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
section.section-spaced {
|
// section.section-spaced {
|
||||||
margin-bottom: $spacing-vertical;
|
// margin-bottom: $spacing-vertical;
|
||||||
}
|
// }
|
||||||
|
|
|
@ -68,7 +68,7 @@ select {
|
||||||
border: 0 none;
|
border: 0 none;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
width: auto\9;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-ms-interpolation-mode: bicubic;
|
-ms-interpolation-mode: bicubic;
|
||||||
|
|
|
@ -18,7 +18,7 @@ $width-page-constrained: 800px;
|
||||||
|
|
||||||
--text-color: var(--color-black);
|
--text-color: var(--color-black);
|
||||||
--color-brand: #155b4a;
|
--color-brand: #155b4a;
|
||||||
// --color-dark-overlay: rgba(32, 32, 32, 0.9);
|
--color-dark-overlay: rgba(32, 32, 32, 0.9);
|
||||||
--color-help: rgba(0, 0, 0, 0.54);
|
--color-help: rgba(0, 0, 0, 0.54);
|
||||||
// --color-notice: #8a6d3b;
|
// --color-notice: #8a6d3b;
|
||||||
--color-error: #a94442;
|
--color-error: #a94442;
|
||||||
|
@ -32,14 +32,18 @@ $width-page-constrained: 800px;
|
||||||
--color-placeholder: #ececec;
|
--color-placeholder: #ececec;
|
||||||
--color-nav-bg: #f6f6f6;
|
--color-nav-bg: #f6f6f6;
|
||||||
|
|
||||||
/* Misc */
|
/* Video */
|
||||||
|
--height-video-embedded: 450px;
|
||||||
|
--height-video-embedded-min: 325px;
|
||||||
|
--width-video-embedded: 700px;
|
||||||
|
|
||||||
|
// --width-video-embedded: var(--height-video-embedded) * 16/9;
|
||||||
// --content-max-width: 1000px;
|
// --content-max-width: 1000px;
|
||||||
// --nsfw-blur-intensity: 20px;
|
// --nsfw-blur-intensity: 20px;
|
||||||
// --height-video-embedded: $width-page-constrained * 9 / 16;
|
|
||||||
|
|
||||||
/* Font */
|
/* Font */
|
||||||
--font-size: 16px;
|
--font-size: 16px;
|
||||||
--font-line-height: 1.3333;
|
--font-line-height: 1.7;
|
||||||
--font-size-subtext-multiple: 0.82;
|
--font-size-subtext-multiple: 0.82;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows */
|
||||||
|
|
|
@ -21,11 +21,9 @@
|
||||||
@import 'component/_pagination.scss';
|
@import 'component/_pagination.scss';
|
||||||
@import 'component/_markdown-editor.scss';
|
@import 'component/_markdown-editor.scss';
|
||||||
@import 'component/_scrollbar.scss';
|
@import 'component/_scrollbar.scss';
|
||||||
@import 'component/_tabs.scss';
|
|
||||||
@import 'component/_divider.scss';
|
@import 'component/_divider.scss';
|
||||||
@import 'component/_checkbox.scss';
|
@import 'component/_checkbox.scss';
|
||||||
@import 'component/_radio.scss';
|
@import 'component/_radio.scss';
|
||||||
@import 'component/_shapeshift.scss';
|
@import 'component/_shapeshift.scss';
|
||||||
@import 'component/_spinner.scss';
|
@import 'component/_spinner.scss';
|
||||||
@import 'component/_nav.scss';
|
@import 'component/_nav.scss';
|
||||||
@import 'page/_show.scss';
|
|
||||||
|
|
|
@ -1,8 +1,29 @@
|
||||||
/*
|
// This will go away
|
||||||
TODO:
|
// It's for the download progress "button"
|
||||||
Determine [disabled] or .disabled
|
.faux-button-block {
|
||||||
Add <a> support (probably just get rid of button prefix)
|
display: inline-block;
|
||||||
*/
|
height: var(--button-height);
|
||||||
|
line-height: var(--button-height);
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0 none;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: var(--button-radius);
|
||||||
|
text-transform: uppercase;
|
||||||
|
.icon {
|
||||||
|
top: 0em;
|
||||||
|
}
|
||||||
|
.icon:first-child {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.icon:last-child {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
.icon:only-child {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -19,6 +40,7 @@ Add <a> support (probably just get rid of button prefix)
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
fill: currentColor; // for proper icon color
|
fill: currentColor; // for proper icon color
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--box-shadow-layer);
|
box-shadow: var(--box-shadow-layer);
|
||||||
|
@ -71,6 +93,11 @@ Add <a> support (probably just get rid of button prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn--secondary {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.btn--no-style {
|
.btn--no-style {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
user-select: text;
|
user-select: text;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--section {
|
.card--section {
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
padding: $spacing-vertical;
|
padding: $spacing-vertical;
|
||||||
margin-top: $spacing-vertical * 2/3;
|
margin-top: $spacing-vertical * 2/3;
|
||||||
|
@ -51,8 +51,27 @@
|
||||||
margin-top: $spacing-vertical * 1/3;
|
margin-top: $spacing-vertical * 1/3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: regular .card__title
|
.card__title-identity--file {
|
||||||
// maybe not needed?
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.credit-amount,
|
||||||
|
.icon {
|
||||||
|
margin-left: $spacing-vertical * 2/3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title-identity-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: $spacing-vertical / 3 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card__title--small {
|
.card__title--small {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
@ -64,10 +83,26 @@
|
||||||
padding-top: $spacing-vertical * 1/3;
|
padding-top: $spacing-vertical * 1/3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__subtitle--file {
|
||||||
|
font-size: 1em;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card-media__internal-links {
|
.card-media__internal-links {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: $spacing-vertical * 2/3;
|
||||||
right: 5px;
|
right: $spacing-vertical * 2/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel info with buttons on the right side
|
||||||
|
.card__channel-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.card__actions .btn:not(:first-of-type) {
|
||||||
|
margin-left: $spacing-vertical / 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__content {
|
.card__content {
|
||||||
|
@ -75,11 +110,47 @@
|
||||||
margin-bottom: var(--card-margin);
|
margin-bottom: var(--card-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__subtext-title {
|
||||||
|
color: var(--color-black);
|
||||||
|
margin-top: $spacing-vertical * 3/2;
|
||||||
|
font-size: calc(var(--font-size-subtext-multiple) * 1.5em);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__subtext {
|
||||||
|
color: var(--color-grey-dark);
|
||||||
|
font-size: calc(var(--font-size-subtext-multiple) * 1em);
|
||||||
|
padding-top: $spacing-vertical * 1/3;
|
||||||
|
word-break: break-word;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
.card__actions {
|
.card__actions {
|
||||||
margin-top: var(--card-margin);
|
margin-top: var(--card-margin);
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__actions--no-margin {
|
||||||
|
magin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__actions--vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
.btn:not(:first-child) {
|
||||||
|
margin-top: $spacing-vertical * 2/3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__actions--center {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0 $spacing-vertical / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
.card-row is used on the discover/subscriptions page
|
.card-row is used on the discover/subscriptions page
|
||||||
It is a list of cards that extend past the right edge of the screen
|
It is a list of cards that extend past the right edge of the screen
|
||||||
|
@ -135,3 +206,9 @@
|
||||||
margin-right: $spacing-vertical * 2/3;
|
margin-right: $spacing-vertical * 2/3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: come back to this
|
||||||
|
// Do we want an opacity over the image?
|
||||||
|
.card--obscured {
|
||||||
|
background-color: #e09898;
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,21 @@
|
||||||
.form-field:not(:first-of-type) {
|
.form-field:not(:first-of-type) {
|
||||||
padding-left: $spacing-vertical;
|
padding-left: $spacing-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.form-row--padded {
|
||||||
|
padding: $spacing-vertical * 2/3 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__wrapper {
|
.form-field__wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: $spacing-vertical / 3 0;
|
padding: $spacing-vertical / 3 0;
|
||||||
|
|
||||||
|
// Hmm, not sure about this
|
||||||
|
// without this the checkbox isn't on the same line as other form-field text
|
||||||
|
input[type='checkbox'] {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__error {
|
.form-field__error {
|
||||||
|
|
|
@ -1,58 +1,43 @@
|
||||||
.spinner {
|
.spinner {
|
||||||
position: relative;
|
margin: $spacing-vertical * 1/3;
|
||||||
width: 11em;
|
width: 50px;
|
||||||
height: 11em;
|
height: 40px;
|
||||||
margin: 20px auto;
|
text-align: center;
|
||||||
font-size: 3px;
|
font-size: 10px;
|
||||||
border-radius: 50%;
|
}
|
||||||
|
|
||||||
background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0) 50%);
|
.spinner > div {
|
||||||
animation: spin 1.4s infinite linear;
|
display: inline-block;
|
||||||
transform: translateZ(0);
|
height: 100%;
|
||||||
|
width: 6px;
|
||||||
|
margin: 0 2px;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||||
|
|
||||||
@keyframes spin {
|
&.rect2 {
|
||||||
from {
|
animation-delay: -1.1s;
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before,
|
&.rect3 {
|
||||||
&:after {
|
animation-delay: -1s;
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&.rect4 {
|
||||||
width: 50%;
|
animation-delay: -0.9s;
|
||||||
height: 50%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 100% 0 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&.rect5 {
|
||||||
height: 75%;
|
animation-delay: -0.8s;
|
||||||
width: 75%;
|
|
||||||
margin: auto;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner.spinner--dark {
|
@keyframes sk-stretchdelay {
|
||||||
background: linear-gradient(to right, var(--button-primary-bg) 10%, var(--color-bg) 50%);
|
0%,
|
||||||
|
40%,
|
||||||
&:before {
|
100% {
|
||||||
background: var(--button-primary-bg);
|
transform: scaleY(0.4);
|
||||||
}
|
}
|
||||||
|
20% {
|
||||||
&:after {
|
transform: scaleY(1);
|
||||||
background: var(--color-bg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
/* Tabs */
|
|
||||||
|
|
||||||
nav.sub-header {
|
|
||||||
text-transform: uppercase;
|
|
||||||
max-width: $width-page-constrained;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
border-bottom: var(--divider);
|
|
||||||
user-select: none;
|
|
||||||
> a {
|
|
||||||
height: 38px;
|
|
||||||
line-height: 38px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: baseline;
|
|
||||||
margin: 0 12px;
|
|
||||||
padding: 0 8px;
|
|
||||||
color: var(--tab-color);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
&.sub-header-selected {
|
|
||||||
color: var(--tab-active-color);
|
|
||||||
&:before {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--tab-border-size);
|
|
||||||
background: var(--tab-active-color);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
content: '';
|
|
||||||
animation-name: activeTab;
|
|
||||||
animation-duration: var(--animation-duration);
|
|
||||||
animation-timing-function: var(--animation-style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color: var(--tab-active-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sub-header--full-width {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sub-header--small-margin {
|
|
||||||
margin-bottom: $spacing-vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes activeTab {
|
|
||||||
from {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +1,11 @@
|
||||||
$height-video-embedded: $width-page-constrained * 9 / 16;
|
.video__embedded {
|
||||||
|
|
||||||
video {
|
|
||||||
object-fit: contain;
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
background: #000;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-embedded {
|
|
||||||
max-width: $width-page-constrained;
|
|
||||||
max-height: $height-video-embedded;
|
|
||||||
height: $height-video-embedded;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: var(--color-black);
|
||||||
|
min-height: var(--height-video-embedded-min);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
video {
|
video {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -27,57 +13,36 @@ video {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
&.video--hidden {
|
|
||||||
height: $height-video-embedded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.video--obscured .video__cover {
|
|
||||||
position: relative;
|
// Video thumbnail with play/download button
|
||||||
filter: blur(var(--nsfw-blur-intensity));
|
.video__cover {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-size: auto 100%;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:not(.card--obscured) {
|
||||||
|
background-color: var(--color-black);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video__loading-screen {
|
.video__loading-screen {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video__loading-status {
|
.video__loading-text {
|
||||||
padding-top: 20px;
|
color: var(--color-white);
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video__cover {
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-size: auto 100%;
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
position: relative;
|
|
||||||
.video__play-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video__play-button {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
display: none;
|
|
||||||
font-size: $spacing-vertical * 3;
|
|
||||||
color: white;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--color-dark-overlay);
|
|
||||||
opacity: 0.6;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity var(--transition-duration) var(--transition-type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
.show-page-media {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 500px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -96,7 +96,7 @@ const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
||||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||||
|
|
||||||
const persistOptions = {
|
const persistOptions = {
|
||||||
whitelist: ['claims', 'subscriptions'],
|
whitelist: ['claims', 'subscriptions', 'navigation'],
|
||||||
// Order is important. Needs to be compressed last or other transforms can't
|
// Order is important. Needs to be compressed last or other transforms can't
|
||||||
// read the data
|
// read the data
|
||||||
transforms: [saveClaimsFilter, subscriptionsFilter, compressor],
|
transforms: [saveClaimsFilter, subscriptionsFilter, compressor],
|
||||||
|
@ -106,7 +106,7 @@ const persistOptions = {
|
||||||
|
|
||||||
window.cacheStore = persistStore(store, persistOptions, err => {
|
window.cacheStore = persistStore(store, persistOptions, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Unable to load saved SETTINGS');
|
console.error('Unable to load saved settings'); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue
Changed the spinner to not actually be a spinner, but might as well keep the name as is