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='^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
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, currentPageAttributes } = this.props;
if (this.mainContent && currentStackIndex !== prevStackIndex) {
if (this.mainContent && currentStackIndex !== prevStackIndex && currentPageAttributes) {
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
}
}

View file

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

View file

@ -2,15 +2,23 @@
/* 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,
@ -18,18 +26,18 @@ type FormFieldProps = {
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 */
}
}

View file

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

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

View file

@ -1,70 +1,73 @@
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;
if (!claim || !metadata) {
return (
<div className="card__content">
<span className="empty">{__('Empty claim or metadata info.')}</span>
</div>
);
}
const { description, language, license } = metadata;
const mediaType = lbry.getMediaType(contentType);
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
const FileDetails = (props: Props) => {
const { claim, contentType, fileInfo, metadata, openFolder } = props;
if (!claim || !metadata) {
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">
<span className="empty">{__('Empty claim or metadata info.')}</span>
</div>
);
}
const { description, language, license } = metadata;
const mediaType = lbry.getMediaType(contentType);
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
return (
<div>
<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>
{downloadPath && (
<tr>
<td>{__('Downloaded to')}</td>
<td>
<Link onClick={() => openFolder(downloadPath)}>{downloadPath}</Link>
</td>
</tr>
)}
</tbody>
</table>
<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 && (
<React.Fragment>
<dt>{__('Downloaded to')}</dt>
<dd>
<Button fakeLink onClick={() => openFolder(downloadPath)} label={downloadPath} />
</dd>
</React.Fragment>
)}
</dl>
</div>
</div>
);
}
}
</div>
);
};
export default FileDetails;

View file

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

View file

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

View file

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

View file

@ -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,18 +26,16 @@ 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'}
label={subscriptionLabel}
onClick={() =>
subscriptionHandler({
channelName,
uri,
})
}
/>
</div>
<Button
iconRight="AtSign"
alt={isSubscribed}
label={subscriptionLabel}
onClick={() =>
subscriptionHandler({
channelName,
uri,
})
}
/>
) : null;
};

View file

@ -1,14 +1,27 @@
// @flow
import React from 'react';
import Spinner from 'component/common/spinner';
const LoadingScreen = ({ status, spinner = true }) => (
<div className="video__loading-screen">
<div>
{spinner && <Spinner />}
type Props = {
spinner: boolean,
status: string,
};
<div className="video__loading-status">{status}</div>
</div>
</div>
);
class LoadingScreen extends React.PureComponent<Props> {
static defaultProps = {
spinner: true,
};
render() {
const { status, spinner } = this.props;
return (
<div className="video__loading-screen">
{spinner && <Spinner />}
<span className="video__loading-text">{status}</span>
</div>
);
}
}
export default LoadingScreen;

View file

@ -1,47 +1,42 @@
// @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;
/*
title={
isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." :
fileInfo === undefined ? "Waiting on file info..." : ""
}
TODO: Add title back to button
title={
isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." :
fileInfo === undefined ? "Waiting on file info..." : ""
}
*/
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()}
/>

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

View file

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

View file

@ -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')}
min="0"
step="any"
type="number"
errorMessage={errorMessage}
error={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" />
{__(`This will appear as a tip for ${title} located at ${uri}.`)}
{" "}
<Button label={__('Learn more')} fakeLink href="https://lbry.io/faq/tipping" />
</span>
}
placeholder="1.00"
onChange={event => this.handleSupportPriceChange(event)}
render={() => (
<input
min="0"
step="any"
type="number"
placeholder="1.00"
onChange={event => this.handleSupportPriceChange(event)}
/>
)}
/>
<div className="form-row-submit">
<Link
<div className="card__actions">
<Button
label={__('Send')}
button="primary"
disabled={isPending}
onClick={this.handleSendButtonClicked.bind(this)}
onClick={this.handleSendButtonClicked}
/>
<Link label={__('Cancel')} button="alt" navigate="/show" navigateParams={{ uri }} />
<Button alt label={__('Cancel')} onClick={onCancel} navigateParams={{ uri }} />
</div>
</div>
</div>
@ -69,4 +95,3 @@ class WalletSendTip extends React.PureComponent {
}
export default WalletSendTip;
/* eslint-enable */

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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">
<section className="card">
<div>
{isPlayable ? (
<Video className="video-embedded" uri={uri} />
) : metadata && metadata.thumbnail ? (
<Thumbnail src={metadata.thumbnail} />
<Video className="video__embedded" uri={uri} />
) : (
<Thumbnail />
<Thumbnail
shouldObscure={shouldObscureThumbnail}
className="video__embedded"
src={thumbnail}
/>
)}
</div>
<div className="card__inner">
{(!tab || tab === 'details') && (
<div>
{' '}
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0 ? (
<span style={{ float: 'right' }}>
<FilePrice uri={normalizeURI(uri)} />
{isRewardContent && (
<span>
{' '}
<Icon icon={icons.FEATURED} />
</span>
)}
</span>
) : null}
<h1>{title}</h1>
<div className="card__subtitle card--file-subtitle">
<UriIndicator uri={uri} link />
<span className="card__publish-date">
Published on <DateTime block={height} show={DateTime.SHOW_DATE} />
</span>
</div>
</div>
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
<FileDetails uri={uri} />
{!isPlaying && (
<div className="card-media__internal-links">
<FileActions uri={uri} vertical />
</div>
)}
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div>
<div className="card--content">
<div className="card__title-identity--file">
<h1 className="card__title">{title}</h1>
<div className="card__title-identity-icons">
<FilePrice uri={normalizeURI(uri)} />
{isRewardContent && <Icon icon={icons.FEATURED} />}
</div>
</div>
<span className="card__subtitle card__subtitle--file">
{__('Published on')} <DateTime block={height} show={DateTime.SHOW_DATE} />
</span>
<div className="card__channel-info">
<UriIndicator uri={uri} link />
<div className="card__actions card__actions--no-margin">
<Button
alt
iconRight="Send"
label={__('Enjoy this? Send a tip')}
onClick={() => openModal(modals.SEND_TIP, { uri })}
/>
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
</div>
</div>
</div>
<div className="card--content">
<FileDetails uri={uri} />
</div>
</section>
</Page>
@ -119,4 +162,3 @@ class FilePage extends React.PureComponent {
}
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 defaultState = {
playingUri: null,
currentlyIsPlaying: false,
rewardedContentClaimIds: [],
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]
);
export const selectPageTitle = createSelector(
selectCurrentPage,
(page) => {
switch (page) {
default:
return '';
}
export const selectPageTitle = createSelector(selectCurrentPage, page => {
switch (page) {
default:
return '';
}
);
});
export const selectNavLinks = createSelector(
selectCurrentPage,

View file

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

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

View file

@ -68,7 +68,7 @@ select {
border: 0 none;
}
img {
width: auto\9;
width: auto;
height: auto;
vertical-align: middle;
-ms-interpolation-mode: bicubic;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,58 +1,43 @@
.spinner {
position: relative;
width: 11em;
height: 11em;
margin: 20px auto;
font-size: 3px;
border-radius: 50%;
margin: $spacing-vertical * 1/3;
width: 50px;
height: 40px;
text-align: center;
font-size: 10px;
}
background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0) 50%);
animation: spin 1.4s infinite linear;
transform: translateZ(0);
.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;
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
&.rect2 {
animation-delay: -1.1s;
}
&:before,
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
&.rect3 {
animation-delay: -1s;
}
&:before {
width: 50%;
height: 50%;
background: #fff;
border-radius: 100% 0 0 0;
&.rect4 {
animation-delay: -0.9s;
}
&:after {
height: 75%;
width: 75%;
margin: auto;
bottom: 0;
right: 0;
background: #000;
border-radius: 50%;
&.rect5 {
animation-delay: -0.8s;
}
}
.spinner.spinner--dark {
background: linear-gradient(to right, var(--button-primary-bg) 10%, var(--color-bg) 50%);
&:before {
background: var(--button-primary-bg);
@keyframes sk-stretchdelay {
0%,
40%,
100% {
transform: scaleY(0.4);
}
&:after {
background: var(--color-bg);
20% {
transform: scaleY(1);
}
}

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 {
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--obscured .video__cover {
position: relative;
filter: blur(var(--nsfw-blur-intensity));
// 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__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);
}

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