Merge pull request #963 from lbryio/redesign-wip

[redesign] File page
This commit is contained in:
Sean Yesmunt 2018-02-01 14:19:38 -08:00 committed by GitHub
commit dc2f7f172a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 831 additions and 689 deletions

View file

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

@ -0,0 +1,3 @@
declare module 'react-markdown' {
declare module.exports: any;
}

3
flow-typed/render-media.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'render-media' {
declare module.exports: any;
}

View file

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

View file

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

View file

@ -2,34 +2,42 @@
/* 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,
label?: string, label?: string,
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 */
} }
} }

View file

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

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

View file

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

View file

@ -1,15 +1,26 @@
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) { if (!claim || !metadata) {
return ( return (
@ -26,45 +37,37 @@ class FileDetails extends React.PureComponent {
return ( return (
<div> <div>
<div className="divider__horizontal" /> <div className="card__content">
<FileActions uri={uri} /> <div className="card__subtext-title">About</div>
<div className="divider__horizontal" /> <div className="card__subtext">
<div className="card__content card__subtext card__subtext--allow-newlines">
<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>
</tr>
<tr>
<td>{__('License')}</td>
<td>{license}</td>
</tr>
{downloadPath && ( {downloadPath && (
<tr> <React.Fragment>
<td>{__('Downloaded to')}</td> <dt>{__('Downloaded to')}</dt>
<td> <dd>
<Link onClick={() => openFolder(downloadPath)}>{downloadPath}</Link> <Button fakeLink onClick={() => openFolder(downloadPath)} label={downloadPath} />
</td> </dd>
</tr> </React.Fragment>
)} )}
</tbody> </dl>
</table> </div>
</div> </div>
</div> </div>
); );
} };
}
export default FileDetails; export default FileDetails;

View file

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

View file

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

View file

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

View file

@ -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,10 +26,9 @@ 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({
@ -22,6 +37,5 @@ export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannel
}) })
} }
/> />
</div>
) : null; ) : null;
}; };

View file

@ -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 = {
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 className="video__loading-screen">
<div>
{spinner && <Spinner />} {spinner && <Spinner />}
<div className="video__loading-status">{status}</div> <span className="video__loading-text">{status}</span>
</div>
</div> </div>
); );
}
}
export default LoadingScreen; export default LoadingScreen;

View file

@ -1,31 +1,25 @@
// @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,
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;
/* /*
TODO: Add title back to button
title={ title={
isLoading ? "Video is Loading" : isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." : !costInfo ? "Waiting on cost info..." :
@ -34,14 +28,15 @@ class VideoPlayButton extends React.PureComponent {
*/ */
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()}
/> />

View file

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

View file

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

View file

@ -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);
// 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')}
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" min="0"
step="any" step="any"
type="number" 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" placeholder="1.00"
onChange={event => this.handleSupportPriceChange(event)} 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> </div>
</div> </div>
@ -69,4 +95,3 @@ class WalletSendTip extends React.PureComponent {
} }
export default WalletSendTip; export default WalletSendTip;
/* eslint-enable */

View file

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

View file

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

View file

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

View file

@ -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
prefix={__('Also delete this file from my computer')}
render={() => (
<input
type="checkbox" type="checkbox"
checked={deleteChecked} checked={deleteChecked}
onClick={this.handleDeleteCheckboxClicked.bind(this)} onChange={this.handleDeleteCheckboxClicked}
label={__('Delete this file from my computer')}
/> />
</section> )}
{claimIsMine && ( />
<section> </FormRow>
{!claimIsMine && (
<FormRow>
<FormField <FormField
prefix={__('Abandon the claim for this URI')}
render={() => (
<input
type="checkbox" type="checkbox"
checked={abandonClaimChecked} checked={abandonClaimChecked}
onClick={this.handleAbandonClaimCheckboxClicked.bind(this)} onChange={this.handleAbandonClaimCheckboxClicked}
label={__('Abandon the claim for this URI')}
/> />
</section> )}
/>
</FormRow>
)} )}
</Modal> </Modal>
); );

View file

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

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

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

View file

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

View file

@ -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">
{isPlayable ? (
<Video className="video-embedded" uri={uri} />
) : metadata && metadata.thumbnail ? (
<Thumbnail src={metadata.thumbnail} />
) : (
<Thumbnail />
)}
</div>
<div className="card__inner">
{(!tab || tab === 'details') && (
<div> <div>
{' '} {isPlayable ? (
<div className="card__title-identity"> <Video className="video__embedded" uri={uri} />
{!fileInfo || fileInfo.written_bytes <= 0 ? ( ) : (
<span style={{ float: 'right' }}> <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)} /> <FilePrice uri={normalizeURI(uri)} />
{isRewardContent && ( {isRewardContent && <Icon icon={icons.FEATURED} />}
<span> </div>
{' '} </div>
<Icon icon={icons.FEATURED} /> <span className="card__subtitle card__subtitle--file">
{__('Published on')} <DateTime block={height} show={DateTime.SHOW_DATE} />
</span> </span>
)}
</span> <div className="card__channel-info">
) : null}
<h1>{title}</h1>
<div className="card__subtitle card--file-subtitle">
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
<span className="card__publish-date"> <div className="card__actions card__actions--no-margin">
Published on <DateTime block={height} show={DateTime.SHOW_DATE} /> <Button
</span> alt
</div> iconRight="Send"
</div> label={__('Enjoy this? Send a tip')}
onClick={() => openModal(modals.SEND_TIP, { uri })}
/>
<SubscribeButton uri={subscriptionUri} channelName={channelName} /> <SubscribeButton uri={subscriptionUri} channelName={channelName} />
<FileDetails uri={uri} />
</div> </div>
)} </div>
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />} </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 */

View file

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

View file

@ -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: {},
}; };

View file

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

View file

@ -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,
(page) => {
switch (page) { switch (page) {
default: default:
return ''; return '';
} }
} });
);
export const selectNavLinks = createSelector( export const selectNavLinks = createSelector(
selectCurrentPage, selectCurrentPage,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%);
animation: spin 1.4s infinite linear;
transform: translateZ(0);
@keyframes spin {
from {
transform: rotate(0deg);
} }
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, @keyframes sk-stretchdelay {
&:after { 0%,
content: ''; 40%,
position: absolute; 100% {
top: 0; transform: scaleY(0.4);
left: 0;
} }
20% {
&:before { transform: scaleY(1);
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);
} }
} }

View file

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

View file

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

View file

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

View file

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