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