commit
dc2f7f172a
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='^rewards\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/rewards\1'
|
||||
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
|
||||
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/app\1'
|
||||
|
||||
[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, currentPageAttributes } = this.props;
|
||||
|
||||
if (this.mainContent && currentStackIndex !== prevStackIndex) {
|
||||
if (this.mainContent && currentStackIndex !== prevStackIndex && currentPageAttributes) {
|
||||
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,62 +43,4 @@ export class CurrencySymbol extends React.PureComponent {
|
|||
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 */
|
||||
|
|
|
@ -2,34 +2,42 @@
|
|||
/* eslint-disable react/no-multi-comp */
|
||||
import * as React from 'react';
|
||||
import Button from 'component/link';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type FormRowProps = {
|
||||
children: React.Node,
|
||||
padded?: boolean,
|
||||
};
|
||||
|
||||
export const FormRow = (props: FormRowProps) => {
|
||||
const { children } = props;
|
||||
return <div className="form-row">{children}</div>;
|
||||
export class FormRow extends React.PureComponent<FormRowProps> {
|
||||
static defaultProps = {
|
||||
padded: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, padded } = this.props;
|
||||
return <div className={classnames('form-row', { 'form-row--padded': padded })}>{children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
type FormFieldProps = {
|
||||
render: () => React.Node,
|
||||
label?: string,
|
||||
prefix?: string,
|
||||
postfix?: string,
|
||||
error?: string | boolean,
|
||||
helper?: string | React.Node,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<FormFieldProps> {
|
||||
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 (
|
||||
<div className="form-field">
|
||||
{label && (
|
||||
<label className="form-field__label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{label && <label className="form-field__label">{label}</label>}
|
||||
<div className="form-field__wrapper">
|
||||
{prefix && <span className="form-field__prefix">{prefix}</span>}
|
||||
{render()}
|
||||
|
@ -40,8 +48,10 @@ export class FormField extends React.PureComponent<FormFieldProps> {
|
|||
{typeof error === 'string' ? error : __('There was an error')}
|
||||
</div>
|
||||
)}
|
||||
{helper && <div className="form-field__help">{helper}</div>}
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/label-has-for */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default ({ dark, className }) => (
|
||||
<div
|
||||
className={classnames(
|
||||
'spinner',
|
||||
{
|
||||
'spinner--dark': dark,
|
||||
},
|
||||
className
|
||||
)}
|
||||
/>
|
||||
const Spinner = () => (
|
||||
<div className="spinner">
|
||||
<div className="rect1" />
|
||||
<div className="rect2" />
|
||||
<div className="rect3" />
|
||||
<div className="rect4" />
|
||||
<div className="rect5" />
|
||||
</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 Link from 'component/link';
|
||||
import Button from 'component/link';
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
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() {
|
||||
const { fileInfo, uri, openModal, claimIsMine } = this.props;
|
||||
const { fileInfo, uri, openModal, claimIsMine, vertical } = this.props;
|
||||
|
||||
const claimId = fileInfo ? fileInfo.claim_id : null,
|
||||
showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
||||
const claimId = fileInfo ? fileInfo.claim_id : '';
|
||||
// showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
||||
|
||||
const showDelete = true;
|
||||
return (
|
||||
<section className="card__actions">
|
||||
<section className={classnames('card__actions', { 'card__actions--vertical': vertical })}>
|
||||
<FileDownloadLink uri={uri} />
|
||||
{showDelete && (
|
||||
<Link
|
||||
button="text"
|
||||
icon="icon-trash"
|
||||
<Button
|
||||
alt
|
||||
icon="Trash"
|
||||
label={__('Remove')}
|
||||
className="no-underline"
|
||||
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
|
||||
/>
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
<Link
|
||||
button="text"
|
||||
icon="icon-flag"
|
||||
<Button
|
||||
alt
|
||||
icon="Flag"
|
||||
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 && (
|
||||
<Link
|
||||
button="alt"
|
||||
icon="icon-edit"
|
||||
<Button
|
||||
icon="Edit3"
|
||||
label={__('Edit')}
|
||||
navigate="/publish"
|
||||
className="card__action--right"
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import React from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import lbry from 'lbry.js';
|
||||
import FileActions from 'component/fileActions';
|
||||
import Link from 'component/link';
|
||||
import DateTime from 'component/dateTime';
|
||||
import lbry from 'lbry';
|
||||
import Button from 'component/link';
|
||||
import path from 'path';
|
||||
|
||||
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 {
|
||||
render() {
|
||||
const { claim, contentType, fileInfo, metadata, openFolder, uri } = this.props;
|
||||
const FileDetails = (props: Props) => {
|
||||
const { claim, contentType, fileInfo, metadata, openFolder } = props;
|
||||
|
||||
if (!claim || !metadata) {
|
||||
return (
|
||||
|
@ -26,45 +37,37 @@ class FileDetails extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="divider__horizontal" />
|
||||
<FileActions uri={uri} />
|
||||
<div className="divider__horizontal" />
|
||||
<div className="card__content card__subtext card__subtext--allow-newlines">
|
||||
<div className="card__content">
|
||||
<div className="card__subtext-title">About</div>
|
||||
<div className="card__subtext">
|
||||
<ReactMarkdown
|
||||
source={description || ''}
|
||||
escapeHtml
|
||||
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<table className="table-standard table-stretch">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{__('Content-Type')}</td>
|
||||
<td>{mediaType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{__('Language')}</td>
|
||||
<td>{language}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{__('License')}</td>
|
||||
<td>{license}</td>
|
||||
</tr>
|
||||
<div className="card__subtext-title">Info</div>
|
||||
<div className="card__subtext">
|
||||
<dl>
|
||||
<dt>{__('Content-Type')}</dt>
|
||||
<dd>{mediaType}</dd>
|
||||
<dt>{__('Language')}</dt>
|
||||
<dd>{language}</dd>
|
||||
<dt>{__('License')}</dt>
|
||||
<dd>{license}</dd>
|
||||
{downloadPath && (
|
||||
<tr>
|
||||
<td>{__('Downloaded to')}</td>
|
||||
<td>
|
||||
<Link onClick={() => openFolder(downloadPath)}>{downloadPath}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<React.Fragment>
|
||||
<dt>{__('Downloaded to')}</dt>
|
||||
<dd>
|
||||
<Button fakeLink onClick={() => openFolder(downloadPath)} label={downloadPath} />
|
||||
</dd>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default FileDetails;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// I'll come back to this
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { BusyMessage } from 'component/common';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -81,9 +83,8 @@ class FileDownloadLink extends React.PureComponent {
|
|||
}
|
||||
return (
|
||||
<Link
|
||||
button="text"
|
||||
label={__('Download')}
|
||||
icon="icon-download"
|
||||
icon="DownloadCloud"
|
||||
className="no-underline"
|
||||
onClick={() => {
|
||||
purchaseUri(uri);
|
||||
|
@ -91,15 +92,7 @@ class FileDownloadLink extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<Link
|
||||
label={__('Open')}
|
||||
button="text"
|
||||
icon="icon-external-link-square"
|
||||
className="no-underline"
|
||||
onClick={() => openFile()}
|
||||
/>
|
||||
);
|
||||
return <Link label={__('Open')} icon="BookOpen" onClick={() => openFile()} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -107,3 +100,4 @@ class FileDownloadLink extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default FileDownloadLink;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -38,7 +38,7 @@ class FilePrice extends React.PureComponent<Props> {
|
|||
const isEstimate = costInfo ? !costInfo.includesData : false;
|
||||
|
||||
if (!costInfo) {
|
||||
return <span className="credit-amount">???</span>;
|
||||
return <span className="credit-amount">PRICE</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -25,6 +25,7 @@ type Props = {
|
|||
noStyle: ?boolean,
|
||||
noUnderline: ?boolean,
|
||||
description: ?string,
|
||||
secondary: ?boolean,
|
||||
};
|
||||
|
||||
const Button = (props: Props) => {
|
||||
|
@ -49,6 +50,7 @@ const Button = (props: Props) => {
|
|||
description,
|
||||
noStyle,
|
||||
noUnderline,
|
||||
secondary,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
@ -58,7 +60,8 @@ const Button = (props: Props) => {
|
|||
? 'btn--no-style'
|
||||
: {
|
||||
'btn--link': fakeLink,
|
||||
'btn--primary': !alt && !fakeLink,
|
||||
'btn--primary': !alt && !fakeLink && !secondary, // default to primary
|
||||
'btn--secondary': secondary,
|
||||
'btn--alt': alt,
|
||||
'btn--inverse': inverse,
|
||||
'btn--disabled': disabled,
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
// @flow
|
||||
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 =
|
||||
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
|
||||
|
||||
|
@ -10,10 +26,9 @@ export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannel
|
|||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||
|
||||
return channelName && uri ? (
|
||||
<div className="card__actions">
|
||||
<Link
|
||||
iconRight={isSubscribed ? '' : 'at'}
|
||||
button={isSubscribed ? 'alt' : 'primary'}
|
||||
<Button
|
||||
iconRight="AtSign"
|
||||
alt={isSubscribed}
|
||||
label={subscriptionLabel}
|
||||
onClick={() =>
|
||||
subscriptionHandler({
|
||||
|
@ -22,6 +37,5 @@ export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannel
|
|||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Spinner from 'component/common/spinner';
|
||||
|
||||
const LoadingScreen = ({ status, spinner = true }) => (
|
||||
type Props = {
|
||||
spinner: boolean,
|
||||
status: string,
|
||||
};
|
||||
|
||||
class LoadingScreen extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
spinner: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { status, spinner } = this.props;
|
||||
return (
|
||||
<div className="video__loading-screen">
|
||||
<div>
|
||||
{spinner && <Spinner />}
|
||||
|
||||
<div className="video__loading-status">{status}</div>
|
||||
</div>
|
||||
<span className="video__loading-text">{status}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingScreen;
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/link';
|
||||
|
||||
class VideoPlayButton extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.keyDownListener = this.onKeyDown.bind(this);
|
||||
document.addEventListener('keydown', this.keyDownListener);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.keyDownListener);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.target.tagName.toLowerCase() !== 'input' && event.code === 'Space') {
|
||||
event.preventDefault();
|
||||
this.watch();
|
||||
}
|
||||
}
|
||||
type Props = {
|
||||
play: string => void,
|
||||
isLoading: boolean,
|
||||
uri: string,
|
||||
mediaType: string,
|
||||
fileInfo: ?{},
|
||||
};
|
||||
|
||||
class VideoPlayButton extends React.PureComponent<Props> {
|
||||
watch() {
|
||||
this.props.play(this.props.uri);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { button, label, isLoading, fileInfo, mediaType } = this.props;
|
||||
const { isLoading, fileInfo, mediaType } = this.props;
|
||||
|
||||
/*
|
||||
TODO: Add title back to button
|
||||
title={
|
||||
isLoading ? "Video is Loading" :
|
||||
!costInfo ? "Waiting on cost info..." :
|
||||
|
@ -34,14 +28,15 @@ class VideoPlayButton extends React.PureComponent {
|
|||
*/
|
||||
|
||||
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 (
|
||||
<Link
|
||||
button={button || null}
|
||||
<Button
|
||||
secondary
|
||||
disabled={disabled}
|
||||
label={label || ''}
|
||||
className="video__play-button"
|
||||
label={label}
|
||||
icon={icon}
|
||||
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 { remote } from 'electron';
|
||||
import { Thumbnail } from 'component/common';
|
||||
import player from 'render-media';
|
||||
import fs from 'fs';
|
||||
|
@ -21,9 +23,9 @@ class VideoPlayer extends React.PureComponent {
|
|||
this.togglePlayListener = this.togglePlay.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
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() {
|
||||
|
@ -171,7 +173,7 @@ class VideoPlayer extends React.PureComponent {
|
|||
const needsMetadata = this.playableType();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
{['audio', 'application'].indexOf(mediaType) !== -1 &&
|
||||
(!this.playableType() || hasMetadata) &&
|
||||
!unplayable && <Thumbnail src={poster} className="video-embedded" />}
|
||||
|
@ -180,9 +182,10 @@ class VideoPlayer extends React.PureComponent {
|
|||
!unplayable && <LoadingScreen status={noMetadataMessage} />}
|
||||
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
|
||||
<div ref="media" className="media" />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoPlayer;
|
||||
/* eslint-disable */
|
||||
|
|
|
@ -1,23 +1,48 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import lbry from 'lbry';
|
||||
import classnames from 'classnames';
|
||||
import VideoPlayer from './internal/player';
|
||||
import VideoPlayButton from './internal/play-button';
|
||||
import LoadingScreen from './internal/loading-screen';
|
||||
import NsfwOverlay from 'component/nsfwOverlay';
|
||||
|
||||
class Video extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showNsfwHelp: false,
|
||||
type Props = {
|
||||
cancelPlay: () => void,
|
||||
fileInfo: {
|
||||
outpoint: string,
|
||||
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() {
|
||||
this.props.cancelPlay();
|
||||
}
|
||||
|
||||
isMediaSame(nextProps) {
|
||||
isMediaSame(nextProps: Props) {
|
||||
return (
|
||||
this.props.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() {
|
||||
const {
|
||||
metadata,
|
||||
|
@ -58,11 +67,14 @@ class Video extends React.PureComponent {
|
|||
savePosition,
|
||||
mediaPaused,
|
||||
mediaPosition,
|
||||
className,
|
||||
obscureNsfw,
|
||||
play,
|
||||
} = this.props;
|
||||
|
||||
const isPlaying = playingUri === uri;
|
||||
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);
|
||||
|
||||
let loadStatusMessage = '';
|
||||
|
@ -77,23 +89,10 @@ class Video extends React.PureComponent {
|
|||
loadStatusMessage = __('Downloading stream... not long left now!');
|
||||
}
|
||||
|
||||
const klasses = [];
|
||||
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;
|
||||
const poster = metadata && metadata.thumbnail;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={klasses.join(' ')}
|
||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
||||
>
|
||||
<div className={classnames('video', {}, className)}>
|
||||
{isPlaying &&
|
||||
(!isReadyToPlay ? (
|
||||
<LoadingScreen status={loadStatusMessage} />
|
||||
|
@ -117,11 +116,19 @@ class Video extends React.PureComponent {
|
|||
/>
|
||||
))}
|
||||
{!isPlaying && (
|
||||
<div className="video__cover" style={{ backgroundImage: `url("${metadata.thumbnail}")` }}>
|
||||
<VideoPlayButton {...this.props} mediaType={mediaType} />
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
{this.state.showNsfwHelp && <NsfwOverlay />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,66 +1,92 @@
|
|||
// I'll come back to this
|
||||
/* eslint-disable */
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
import { FormRow } from 'component/common/form';
|
||||
import Button from 'component/link';
|
||||
import { FormField } from 'component/common/form';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
|
||||
class WalletSendTip extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type 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);
|
||||
|
||||
this.state = {
|
||||
amount: 0.0,
|
||||
amount: 0,
|
||||
};
|
||||
|
||||
(this: any).handleSendButtonClicked = this.handleSendButtonClicked.bind(this);
|
||||
}
|
||||
|
||||
handleSendButtonClicked() {
|
||||
const { claim_id, uri } = this.props;
|
||||
const amount = this.state.amount;
|
||||
this.props.sendSupport(amount, claim_id, uri);
|
||||
const { claim_id: claimId, uri, sendSupport, sendTipCallback } = this.props;
|
||||
const { amount } = this.state;
|
||||
|
||||
sendSupport(amount, claimId, uri);
|
||||
|
||||
// ex: close modal
|
||||
if (sendTipCallback) {
|
||||
sendTipCallback();
|
||||
}
|
||||
}
|
||||
|
||||
handleSupportPriceChange(event) {
|
||||
handleSupportPriceChange(event: SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
amount: Number(event.target.value),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorMessage, isPending, title, uri } = this.props;
|
||||
const { errorMessage, isPending, title, uri, onCancel } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="card__title-primary">
|
||||
<h1>
|
||||
{__('Support')} <UriIndicator uri={uri} />
|
||||
{__('Send a tip to')} <UriIndicator uri={uri} />
|
||||
</h1>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
<FormField
|
||||
label={__('Amount')}
|
||||
postfix={__('LBC')}
|
||||
error={errorMessage}
|
||||
helper={
|
||||
<span>
|
||||
{__(`This will appear as a tip for ${title} located at ${uri}.`)}
|
||||
{" "}
|
||||
<Button label={__('Learn more')} fakeLink href="https://lbry.io/faq/tipping" />
|
||||
</span>
|
||||
}
|
||||
render={() => (
|
||||
<input
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
errorMessage={errorMessage}
|
||||
helper={
|
||||
<span>
|
||||
{`${__('This will appear as a tip for "%s" located at %s.', title, uri)} `}
|
||||
<Link label={__('Learn more')} href="https://lbry.io/faq/tipping" />
|
||||
</span>
|
||||
}
|
||||
placeholder="1.00"
|
||||
onChange={event => this.handleSupportPriceChange(event)}
|
||||
/>
|
||||
<div className="form-row-submit">
|
||||
<Link
|
||||
label={__('Send')}
|
||||
button="primary"
|
||||
disabled={isPending}
|
||||
onClick={this.handleSendButtonClicked.bind(this)}
|
||||
)}
|
||||
/>
|
||||
<Link label={__('Cancel')} button="alt" navigate="/show" navigateParams={{ uri }} />
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
label={__('Send')}
|
||||
disabled={isPending}
|
||||
onClick={this.handleSendButtonClicked}
|
||||
/>
|
||||
<Button alt label={__('Cancel')} onClick={onCancel} navigateParams={{ uri }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,4 +95,3 @@ class WalletSendTip extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default WalletSendTip;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -29,7 +29,7 @@ class WunderBar extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
handleChange(e: SyntheticInputEvent<*>) {
|
||||
const { updateSearchQuery, getSearchSuggestions } = this.props;
|
||||
const { updateSearchQuery } = this.props;
|
||||
const { value } = e.target;
|
||||
|
||||
updateSearchQuery(value);
|
||||
|
@ -74,7 +74,6 @@ class WunderBar extends React.PureComponent<Props> {
|
|||
|
||||
input: ?HTMLInputElement;
|
||||
throttledGetSearchSuggestions: string => void;
|
||||
|
||||
render() {
|
||||
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 AFFIRM_PURCHASE = 'affirm_purchase';
|
||||
export const CONFIRM_CLAIM_REVOKE = 'confirmClaimRevoke';
|
||||
export const SEND_TIP = 'sendTip';
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
/* 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 Link from 'component/link/index';
|
||||
import Button from 'component/link/index';
|
||||
import app from 'app';
|
||||
|
||||
export class Modal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
type: PropTypes.oneOf(['alert', 'confirm', 'custom']),
|
||||
overlay: PropTypes.bool,
|
||||
onConfirmed: PropTypes.func,
|
||||
onAborted: PropTypes.func,
|
||||
confirmButtonLabel: PropTypes.string,
|
||||
abortButtonLabel: PropTypes.string,
|
||||
confirmButtonDisabled: PropTypes.bool,
|
||||
abortButtonDisabled: PropTypes.bool,
|
||||
type ModalProps = {
|
||||
type: string,
|
||||
overlay: boolean,
|
||||
confirmButtonLabel: string,
|
||||
abortButtonLabel: string,
|
||||
confirmButtonDisabled: boolean,
|
||||
abortButtonDisabled: boolean,
|
||||
onConfirmed?: any => any,
|
||||
onAborted?: any => any,
|
||||
className?: string,
|
||||
overlayClassName?: string,
|
||||
children?: React.Node,
|
||||
extraContent?: React.Node,
|
||||
expandButtonLabel?: string,
|
||||
hideButtonLabel?: string,
|
||||
};
|
||||
|
||||
export class Modal extends React.PureComponent<ModalProps> {
|
||||
static defaultProps = {
|
||||
type: 'alert',
|
||||
overlay: true,
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
confirmButtonLabel: app.i18n.__('OK'),
|
||||
abortButtonLabel: app.i18n.__('Cancel'),
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
confirmButtonDisabled: false,
|
||||
abortButtonDisabled: false,
|
||||
};
|
||||
|
@ -38,20 +48,17 @@ export class Modal extends React.PureComponent {
|
|||
}
|
||||
>
|
||||
<div>{this.props.children}</div>
|
||||
{this.props.type == 'custom' ? null : ( // custom modals define their own buttons
|
||||
<div className="modal__buttons">
|
||||
<Link
|
||||
button="primary"
|
||||
{this.props.type === 'custom' ? null : ( // custom modals define their own buttons
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button
|
||||
label={this.props.confirmButtonLabel}
|
||||
className="modal__button"
|
||||
disabled={this.props.confirmButtonDisabled}
|
||||
onClick={this.props.onConfirmed}
|
||||
/>
|
||||
{this.props.type == 'confirm' ? (
|
||||
<Link
|
||||
button="alt"
|
||||
{this.props.type === 'confirm' ? (
|
||||
<Button
|
||||
alt
|
||||
label={this.props.abortButtonLabel}
|
||||
className="modal__button"
|
||||
disabled={this.props.abortButtonDisabled}
|
||||
onClick={this.props.onAborted}
|
||||
/>
|
||||
|
@ -63,19 +70,20 @@ export class Modal extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export class ExpandableModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
expandButtonLabel: PropTypes.string,
|
||||
extraContent: PropTypes.element,
|
||||
type State = {
|
||||
expanded: boolean,
|
||||
};
|
||||
|
||||
export class ExpandableModal extends React.PureComponent<ModalProps, State> {
|
||||
static defaultProps = {
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
confirmButtonLabel: app.i18n.__('OK'),
|
||||
expandButtonLabel: app.i18n.__('Show More...'),
|
||||
hideButtonLabel: app.i18n.__('Show Less'),
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: ModalProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -95,13 +103,13 @@ export class ExpandableModal extends React.PureComponent {
|
|||
{this.props.children}
|
||||
{this.state.expanded ? this.props.extraContent : null}
|
||||
<div className="modal__buttons">
|
||||
<Link
|
||||
<Button
|
||||
button="primary"
|
||||
label={this.props.confirmButtonLabel}
|
||||
className="modal__button"
|
||||
onClick={this.props.onConfirmed}
|
||||
/>
|
||||
<Link
|
||||
<Button
|
||||
button="alt"
|
||||
label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
|
||||
className="modal__button"
|
||||
|
@ -116,3 +124,4 @@ export class ExpandableModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default Modal;
|
||||
/* eslint-enable react/no-multi-comp */
|
|
@ -1,26 +1,49 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import FormField from 'component/formField/index';
|
||||
import { FormRow, FormField } from 'component/common/form';
|
||||
|
||||
class ModalRemoveFile extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type 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);
|
||||
|
||||
this.state = {
|
||||
deleteChecked: false,
|
||||
deleteChecked: true,
|
||||
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({
|
||||
deleteChecked: event.target.checked,
|
||||
deleteChecked: !deleteChecked,
|
||||
});
|
||||
}
|
||||
|
||||
handleAbandonClaimCheckboxClicked(event) {
|
||||
handleAbandonClaimCheckboxClicked() {
|
||||
const { abandonClaimChecked } = this.state;
|
||||
this.setState({
|
||||
abandonClaimChecked: event.target.checked,
|
||||
abandonClaimChecked: !abandonClaimChecked,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,26 +61,34 @@ class ModalRemoveFile extends React.PureComponent {
|
|||
onAborted={closeModal}
|
||||
>
|
||||
<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>
|
||||
|
||||
<section>
|
||||
<FormRow padded>
|
||||
<FormField
|
||||
prefix={__('Also delete this file from my computer')}
|
||||
render={() => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteChecked}
|
||||
onClick={this.handleDeleteCheckboxClicked.bind(this)}
|
||||
label={__('Delete this file from my computer')}
|
||||
onChange={this.handleDeleteCheckboxClicked}
|
||||
/>
|
||||
</section>
|
||||
{claimIsMine && (
|
||||
<section>
|
||||
)}
|
||||
/>
|
||||
</FormRow>
|
||||
{!claimIsMine && (
|
||||
<FormRow>
|
||||
<FormField
|
||||
prefix={__('Abandon the claim for this URI')}
|
||||
render={() => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={abandonClaimChecked}
|
||||
onClick={this.handleAbandonClaimCheckboxClicked.bind(this)}
|
||||
label={__('Abandon the claim for this URI')}
|
||||
onChange={this.handleAbandonClaimCheckboxClicked}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
</FormRow>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import ModalAffirmPurchase from 'modal/modalAffirmPurchase';
|
|||
import ModalRevokeClaim from 'modal/modalRevokeClaim';
|
||||
import ModalEmailCollection from '../modalEmailCollection';
|
||||
import ModalPhoneCollection from '../modalPhoneCollection';
|
||||
import ModalSendTip from '../modalSendTip';
|
||||
import * as modals from 'constants/modal_types';
|
||||
|
||||
class ModalRouter extends React.PureComponent {
|
||||
|
@ -129,6 +130,8 @@ class ModalRouter extends React.PureComponent {
|
|||
return <ModalPhoneCollection {...modalProps} />;
|
||||
case modals.EMAIL_COLLECTION:
|
||||
return <ModalEmailCollection {...modalProps} />;
|
||||
case modals.SEND_TIP:
|
||||
return <ModalSendTip {...modalProps} />;
|
||||
default:
|
||||
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 { doFetchFileInfo } from 'redux/actions/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 {
|
||||
makeSelectClaimForUri,
|
||||
|
@ -11,8 +11,9 @@ import {
|
|||
} from 'redux/selectors/claims';
|
||||
import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import { selectMediaPaused } from 'redux/selectors/media';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import FilePage from './view';
|
||||
import { makeSelectCurrentParam } from 'redux/selectors/navigation';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
|
@ -20,15 +21,17 @@ const select = (state, props) => ({
|
|||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
tab: makeSelectCurrentParam('tab')(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
playingUri: selectPlayingUri(state),
|
||||
isPaused: selectMediaPaused(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
||||
|
|
|
@ -1,37 +1,69 @@
|
|||
/* eslint-disable */
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import lbry from 'lbry';
|
||||
import { buildURI, normalizeURI } from 'lbryURI';
|
||||
import Video from 'component/video';
|
||||
import { Thumbnail } from 'component/common';
|
||||
import Thumbnail from 'component/common/thumbnail';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import FileActions from 'component/fileActions';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import Icon from 'component/common/icon';
|
||||
import WalletSendTip from 'component/walletSendTip';
|
||||
import DateTime from 'component/dateTime';
|
||||
import * as icons from 'constants/icons';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/link';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
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() {
|
||||
this.fetchFileInfo(this.props);
|
||||
this.fetchCostInfo(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.fetchFileInfo(nextProps);
|
||||
}
|
||||
|
||||
fetchFileInfo(props) {
|
||||
fetchFileInfo(props: Props) {
|
||||
if (props.fileInfo === undefined) {
|
||||
props.fetchFileInfo(props.uri);
|
||||
}
|
||||
}
|
||||
|
||||
fetchCostInfo(props) {
|
||||
fetchCostInfo(props: Props) {
|
||||
if (props.costInfo === undefined) {
|
||||
props.fetchCostInfo(props.uri);
|
||||
}
|
||||
|
@ -43,74 +75,85 @@ class FilePage extends React.PureComponent {
|
|||
fileInfo,
|
||||
metadata,
|
||||
contentType,
|
||||
tab,
|
||||
uri,
|
||||
rewardedContentClaimIds,
|
||||
obscureNsfw,
|
||||
playingUri,
|
||||
isPaused,
|
||||
openModal,
|
||||
} = this.props;
|
||||
|
||||
const showTipBox = tab == 'tip';
|
||||
|
||||
// This should be included below in the page
|
||||
// Come back to me
|
||||
if (!claim || !metadata) {
|
||||
return <span className="empty">{__('Empty claim or metadata info.')}</span>;
|
||||
}
|
||||
|
||||
// File info
|
||||
const title = metadata.title;
|
||||
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 player = require('render-media');
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isPlayable =
|
||||
Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio';
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
|
||||
let subscriptionUri;
|
||||
if (channelName && channelClaimId) {
|
||||
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
|
||||
}
|
||||
|
||||
const isPlaying = playingUri === uri && !isPaused;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
|
||||
<div className="show-page-media">
|
||||
{isPlayable ? (
|
||||
<Video className="video-embedded" uri={uri} />
|
||||
) : metadata && metadata.thumbnail ? (
|
||||
<Thumbnail src={metadata.thumbnail} />
|
||||
) : (
|
||||
<Thumbnail />
|
||||
)}
|
||||
</div>
|
||||
<div className="card__inner">
|
||||
{(!tab || tab === 'details') && (
|
||||
<section className="card">
|
||||
<div>
|
||||
{' '}
|
||||
<div className="card__title-identity">
|
||||
{!fileInfo || fileInfo.written_bytes <= 0 ? (
|
||||
<span style={{ float: 'right' }}>
|
||||
{isPlayable ? (
|
||||
<Video className="video__embedded" uri={uri} />
|
||||
) : (
|
||||
<Thumbnail
|
||||
shouldObscure={shouldObscureThumbnail}
|
||||
className="video__embedded"
|
||||
src={thumbnail}
|
||||
/>
|
||||
)}
|
||||
{!isPlaying && (
|
||||
<div className="card-media__internal-links">
|
||||
<FileActions uri={uri} vertical />
|
||||
</div>
|
||||
)}
|
||||
</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 && (
|
||||
<span>
|
||||
{' '}
|
||||
<Icon icon={icons.FEATURED} />
|
||||
{isRewardContent && <Icon icon={icons.FEATURED} />}
|
||||
</div>
|
||||
</div>
|
||||
<span className="card__subtitle card__subtitle--file">
|
||||
{__('Published on')} <DateTime block={height} show={DateTime.SHOW_DATE} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<h1>{title}</h1>
|
||||
<div className="card__subtitle card--file-subtitle">
|
||||
|
||||
<div className="card__channel-info">
|
||||
<UriIndicator uri={uri} link />
|
||||
<span className="card__publish-date">
|
||||
Published on <DateTime block={height} show={DateTime.SHOW_DATE} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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} />
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card--content">
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
|
@ -119,4 +162,3 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
|
||||
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 defaultState = {
|
||||
playingUri: null,
|
||||
currentlyIsPlaying: false,
|
||||
rewardedContentClaimIds: [],
|
||||
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]
|
||||
);
|
||||
|
||||
export const selectPageTitle = createSelector(
|
||||
selectCurrentPage,
|
||||
(page) => {
|
||||
export const selectPageTitle = createSelector(selectCurrentPage, page => {
|
||||
switch (page) {
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const selectNavLinks = createSelector(
|
||||
selectCurrentPage,
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
selectCurrentPage,
|
||||
selectCurrentParams,
|
||||
selectPageTitle,
|
||||
} from 'redux/selectors/navigation';
|
||||
import { selectCurrentPage, selectCurrentParams } from 'redux/selectors/navigation';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const selectState = state => state.search || {};
|
||||
|
@ -26,13 +22,13 @@ export const makeSelectSearchUris = query =>
|
|||
|
||||
export const selectWunderBarAddress = createSelector(
|
||||
selectCurrentPage,
|
||||
selectPageTitle,
|
||||
selectSearchQuery,
|
||||
(page, title, query) => {
|
||||
selectCurrentParams,
|
||||
(page, query, params) => {
|
||||
// only populate the wunderbar address if we are on the file/channel pages
|
||||
// or show the search query
|
||||
if (page === 'show') {
|
||||
return title;
|
||||
return params.uri;
|
||||
} else if (page === 'search') {
|
||||
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-style: normal;
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -86,12 +94,15 @@ ul {
|
|||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
cursor: pointer;
|
||||
border-bottom: var(--input-border-size) solid var(--input-border-color);
|
||||
color: var(--input-color);
|
||||
line-height: 1;
|
||||
|
||||
&[type='text'] {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&.input-copyable {
|
||||
background: var(--input-bg);
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-rows: var(--header-height) calc(100vh - var(--header-height));
|
||||
|
@ -176,11 +207,11 @@ input {
|
|||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
font-weight: 700;
|
||||
font-size: 0.7em;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.credit-amount--free {
|
||||
color: var(--color-black);
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
|
@ -204,6 +235,11 @@ input {
|
|||
// font-weight: 700;
|
||||
// }
|
||||
|
||||
.divider__horizontal {
|
||||
border-top: var(--color-divider);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -259,16 +295,16 @@ input {
|
|||
}
|
||||
}
|
||||
|
||||
.sort-section {
|
||||
display: block;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
|
||||
text-align: right;
|
||||
line-height: 1;
|
||||
font-size: 0.85em;
|
||||
color: var(--color-help);
|
||||
}
|
||||
|
||||
section.section-spaced {
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
// .sort-section {
|
||||
// display: block;
|
||||
// margin-bottom: $spacing-vertical * 2/3;
|
||||
//
|
||||
// text-align: right;
|
||||
// line-height: 1;
|
||||
// font-size: 0.85em;
|
||||
// color: var(--color-help);
|
||||
// }
|
||||
//
|
||||
// section.section-spaced {
|
||||
// margin-bottom: $spacing-vertical;
|
||||
// }
|
||||
|
|
|
@ -68,7 +68,7 @@ select {
|
|||
border: 0 none;
|
||||
}
|
||||
img {
|
||||
width: auto\9;
|
||||
width: auto;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
|
|
|
@ -18,7 +18,7 @@ $width-page-constrained: 800px;
|
|||
|
||||
--text-color: var(--color-black);
|
||||
--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-notice: #8a6d3b;
|
||||
--color-error: #a94442;
|
||||
|
@ -32,14 +32,18 @@ $width-page-constrained: 800px;
|
|||
--color-placeholder: #ececec;
|
||||
--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;
|
||||
// --nsfw-blur-intensity: 20px;
|
||||
// --height-video-embedded: $width-page-constrained * 9 / 16;
|
||||
|
||||
/* Font */
|
||||
--font-size: 16px;
|
||||
--font-line-height: 1.3333;
|
||||
--font-line-height: 1.7;
|
||||
--font-size-subtext-multiple: 0.82;
|
||||
|
||||
/* Shadows */
|
||||
|
|
|
@ -21,11 +21,9 @@
|
|||
@import 'component/_pagination.scss';
|
||||
@import 'component/_markdown-editor.scss';
|
||||
@import 'component/_scrollbar.scss';
|
||||
@import 'component/_tabs.scss';
|
||||
@import 'component/_divider.scss';
|
||||
@import 'component/_checkbox.scss';
|
||||
@import 'component/_radio.scss';
|
||||
@import 'component/_shapeshift.scss';
|
||||
@import 'component/_spinner.scss';
|
||||
@import 'component/_nav.scss';
|
||||
@import 'page/_show.scss';
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
/*
|
||||
TODO:
|
||||
Determine [disabled] or .disabled
|
||||
Add <a> support (probably just get rid of button prefix)
|
||||
*/
|
||||
// This will go away
|
||||
// It's for the download progress "button"
|
||||
.faux-button-block {
|
||||
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 {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
|
@ -19,6 +40,7 @@ Add <a> support (probably just get rid of button prefix)
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
fill: currentColor; // for proper icon color
|
||||
font-size: 0.8em;
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
user-select: text;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card--section {
|
||||
flex-direction: column;
|
||||
background-color: var(--color-white);
|
||||
padding: $spacing-vertical;
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
|
@ -51,8 +51,27 @@
|
|||
margin-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
// TODO: regular .card__title
|
||||
// maybe not needed?
|
||||
.card__title-identity--file {
|
||||
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 {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
|
@ -64,10 +83,26 @@
|
|||
padding-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
.card__subtitle--file {
|
||||
font-size: 1em;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.card-media__internal-links {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
top: $spacing-vertical * 2/3;
|
||||
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 {
|
||||
|
@ -75,11 +110,47 @@
|
|||
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 {
|
||||
margin-top: var(--card-margin);
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
padding-left: $spacing-vertical;
|
||||
}
|
||||
|
||||
&.form-row--padded {
|
||||
padding: $spacing-vertical * 2/3 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__wrapper {
|
||||
display: flex;
|
||||
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 {
|
||||
|
|
|
@ -1,58 +1,43 @@
|
|||
.spinner {
|
||||
position: relative;
|
||||
width: 11em;
|
||||
height: 11em;
|
||||
margin: 20px auto;
|
||||
font-size: 3px;
|
||||
border-radius: 50%;
|
||||
|
||||
background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0) 50%);
|
||||
animation: spin 1.4s infinite linear;
|
||||
transform: translateZ(0);
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
margin: $spacing-vertical * 1/3;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
|
||||
.spinner > div {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
margin: 0 2px;
|
||||
background-color: var(--color-white);
|
||||
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
||||
|
||||
&.rect2 {
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
&.rect3 {
|
||||
animation-delay: -1s;
|
||||
}
|
||||
|
||||
&.rect4 {
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
&.rect5 {
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@keyframes sk-stretchdelay {
|
||||
0%,
|
||||
40%,
|
||||
100% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
|
||||
&:before {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background: #fff;
|
||||
border-radius: 100% 0 0 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
height: 75%;
|
||||
width: 75%;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner.spinner--dark {
|
||||
background: linear-gradient(to right, var(--button-primary-bg) 10%, var(--color-bg) 50%);
|
||||
|
||||
&:before {
|
||||
background: var(--button-primary-bg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
background: var(--color-bg);
|
||||
20% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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;
|
||||
.video__embedded {
|
||||
position: relative;
|
||||
background-color: var(--color-black);
|
||||
min-height: var(--height-video-embedded-min);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -27,57 +13,36 @@ video {
|
|||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&.video--hidden {
|
||||
height: $height-video-embedded;
|
||||
}
|
||||
|
||||
// Video thumbnail with play/download button
|
||||
.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--obscured .video__cover {
|
||||
position: relative;
|
||||
filter: blur(var(--nsfw-blur-intensity));
|
||||
}
|
||||
|
||||
.video__loading-screen {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video__loading-status {
|
||||
padding-top: 20px;
|
||||
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);
|
||||
}
|
||||
.video__loading-text {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
|
|
@ -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 persistOptions = {
|
||||
whitelist: ['claims', 'subscriptions'],
|
||||
whitelist: ['claims', 'subscriptions', 'navigation'],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, compressor],
|
||||
|
@ -106,7 +106,7 @@ const persistOptions = {
|
|||
|
||||
window.cacheStore = persistStore(store, persistOptions, 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