Merge branch 'master' into patch-2
This commit is contained in:
commit
b16d5e87de
55 changed files with 622 additions and 990 deletions
|
@ -40,6 +40,8 @@
|
|||
"react/require-default-props": 0,
|
||||
"react/jsx-closing-tag-location": 0,
|
||||
"jsx-a11y/no-noninteractive-element-to-interactive-role": 0,
|
||||
"class-methods-use-this": 0
|
||||
"class-methods-use-this": 0,
|
||||
"jsx-a11y/interactive-supports-focus": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0
|
||||
}
|
||||
}
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -6,14 +6,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
* Allow typing of encryption password without clicking entry box ([#1977](https://github.com/lbryio/lbry-desktop/pull/1977))
|
||||
|
||||
* Allow typing of encryption password without clicking entry box ([#1977](https://github.com/lbryio/lbry-desktop/pull/1977))
|
||||
* Focus on search bar with {cmd,ctrl} + "l" ([#2003](https://github.com/lbryio/lbry-desktop/pull/2003))
|
||||
### Changed
|
||||
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))
|
||||
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))
|
||||
* Change channel pages to have 48 items instead of 10 ([#2002](https://github.com/lbryio/lbry-desktop/pull/2002))
|
||||
* Update to https ([#2016](https://github.com/lbryio/lbry-desktop/pull/2016))
|
||||
* Simplify FileCard and FileTile component styling ([#2011](https://github.com/lbryio/lbry-desktop/pull/2011))
|
||||
|
||||
### Fixed
|
||||
* Invite table cutoff with large number of invites ([#1985](https://github.com/lbryio/lbry-desktop/pull/1985))
|
||||
* Fixed Transactions filter menu collides with transaction table ([#2005](https://github.com/lbryio/lbry-desktop/pull/2005))
|
||||
* Fixed Transactions filter menu collides with transaction table ([#2005](https://github.com/lbryio/lbry-desktop/pull/2005))
|
||||
* Invite table cutoff with large number of invites ([#1985](https://github.com/lbryio/lbry-desktop/pull/1985))
|
||||
* History styling on large screens and link issue with claims ([#1999](https://github.com/lbryio/lbry-desktop/pull/1999))
|
||||
* Satisfy console warnings in publishForm and validation messaging ([#2010](https://github.com/lbryio/lbry-desktop/pull/2010))
|
||||
|
||||
|
||||
|
||||
## [0.25.1] - 2018-09-18
|
||||
|
|
|
@ -23,12 +23,10 @@
|
|||
"extract-langs": "node build/extractLocals.js",
|
||||
"compile": "electron-webpack && yarn extract-langs",
|
||||
"build": "yarn compile && electron-builder build",
|
||||
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
|
||||
"dev": "electron-webpack dev",
|
||||
"lint": "eslint 'src/**/*.{js,jsx}' --fix && flow",
|
||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
|
||||
"flow-defs": "flow-typed install",
|
||||
"release": "yarn compile && electron-builder build",
|
||||
"precommit": "lint-staged",
|
||||
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
|
||||
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
|
||||
|
@ -50,7 +48,7 @@
|
|||
"formik": "^0.10.4",
|
||||
"hast-util-sanitize": "^1.1.2",
|
||||
"keytar": "^4.2.1",
|
||||
"lbry-redux": "lbryio/lbry-redux#c079b108c3bc4ba2b4fb85fb112b52cfc040c301",
|
||||
"lbry-redux": "lbryio/lbry-redux#4ee6c376e5f2c3e3e96d199a56970e2621a84af1",
|
||||
"lbryinc": "lbryio/lbryinc#de7ff055605b02a24821f0f9bab1d206eb7f235d",
|
||||
"localforage": "^1.7.1",
|
||||
"mammoth": "^1.4.6",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
|
@ -7,11 +7,9 @@ let scriptLoading = false;
|
|||
let scriptLoaded = false;
|
||||
let scriptDidError = false;
|
||||
|
||||
class CardVerify extends React.Component {
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
label: PropTypes.string,
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
label: ?string,
|
||||
|
||||
// =====================================================
|
||||
// Required by stripe
|
||||
|
@ -21,16 +19,17 @@ class CardVerify extends React.Component {
|
|||
|
||||
// Your publishable key (test or live).
|
||||
// can't use "key" as a prop in react, so have to change the keyname
|
||||
stripeKey: PropTypes.string.isRequired,
|
||||
stripeKey: string,
|
||||
|
||||
// The callback to invoke when the Checkout process is complete.
|
||||
// function(token)
|
||||
// token is the token object created.
|
||||
// token.id can be used to create a charge or customer.
|
||||
// token.email contains the email address entered by the user.
|
||||
token: PropTypes.func.isRequired,
|
||||
};
|
||||
token: string,
|
||||
};
|
||||
|
||||
class CardVerify extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
|
@ -60,32 +60,13 @@ class ChannelTile extends React.PureComponent<Props> {
|
|||
>
|
||||
<CardMedia title={channelName} thumbnail={null} />
|
||||
<div className="file-tile__info">
|
||||
{isResolvingUri && (
|
||||
<div
|
||||
className={classnames({
|
||||
'card__title--small': size !== 'large',
|
||||
'card__title--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
{__('Loading...')}
|
||||
</div>
|
||||
)}
|
||||
{isResolvingUri && <div className="file-tile__title">{__('Loading...')}</div>}
|
||||
{!isResolvingUri && (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={classnames({
|
||||
'card__title--file': size === 'regular',
|
||||
'card__title--x-small': size === 'small',
|
||||
'card__title--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
<div className="file-tile__title">
|
||||
<TruncatedText text={channelName || uri} lines={1} />
|
||||
</div>
|
||||
<div
|
||||
className={classnames('card__subtitle', {
|
||||
'card__subtitle--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
<div className="card__subtitle">
|
||||
{totalItems > 0 && (
|
||||
<span>
|
||||
{totalItems} {totalItems === 1 ? 'file' : 'files'}
|
||||
|
|
|
@ -14,6 +14,7 @@ type Props = {
|
|||
icon: string,
|
||||
tooltip?: string, // tooltip direction
|
||||
iconColor?: string,
|
||||
size?: number,
|
||||
};
|
||||
|
||||
class IconComponent extends React.PureComponent<Props> {
|
||||
|
@ -42,7 +43,7 @@ class IconComponent extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { icon, tooltip, iconColor } = this.props;
|
||||
const { icon, tooltip, iconColor, size } = this.props;
|
||||
const Icon = FeatherIcons[icon];
|
||||
|
||||
if (!Icon) {
|
||||
|
@ -54,16 +55,17 @@ class IconComponent extends React.PureComponent<Props> {
|
|||
color = this.getIconColor(iconColor);
|
||||
}
|
||||
|
||||
let size = 14;
|
||||
let iconSize = size || 14;
|
||||
// Arrow icons are quite a bit smaller than the other icons we use
|
||||
if (icon === icons.ARROW_LEFT || icon === icons.ARROW_RIGHT) {
|
||||
size = 20;
|
||||
iconSize = 20;
|
||||
}
|
||||
|
||||
let tooltipText;
|
||||
if (tooltip) {
|
||||
tooltipText = this.getTooltip(icon);
|
||||
}
|
||||
const inner = <Icon size={size} className="icon" color={color} />;
|
||||
const inner = <Icon size={iconSize} className="icon" color={color} />;
|
||||
|
||||
return tooltipText ? (
|
||||
<Tooltip icon body={tooltipText} direction={tooltip}>
|
||||
|
|
10
src/renderer/component/copyableText/index.js
Normal file
10
src/renderer/component/copyableText/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import CopyableText from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{
|
||||
doNotify,
|
||||
}
|
||||
)(CopyableText);
|
63
src/renderer/component/copyableText/view.jsx
Normal file
63
src/renderer/component/copyableText/view.jsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { clipboard } from 'electron';
|
||||
import { FormRow } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
/*
|
||||
noSnackbar added due to issue 1945
|
||||
https://github.com/lbryio/lbry-desktop/issues/1945
|
||||
"Snackbars and modals can't be displayed at the same time"
|
||||
*/
|
||||
type Props = {
|
||||
copyable: string,
|
||||
noSnackbar: boolean,
|
||||
doNotify: ({ message: string, displayType: Array<string> }) => void,
|
||||
};
|
||||
|
||||
export default class CopyableText extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.input = null;
|
||||
}
|
||||
|
||||
input: ?HTMLInputElement;
|
||||
|
||||
render() {
|
||||
const { copyable, doNotify, noSnackbar } = this.props;
|
||||
|
||||
return (
|
||||
<FormRow verticallyCentered padded stretch>
|
||||
<input
|
||||
className="input-copyable form-field__input"
|
||||
readOnly
|
||||
value={copyable || ''}
|
||||
ref={input => {
|
||||
this.input = input;
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (this.input) {
|
||||
this.input.select();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
noPadding
|
||||
button="secondary"
|
||||
icon={icons.CLIPBOARD}
|
||||
onClick={() => {
|
||||
clipboard.writeText(copyable);
|
||||
if (!noSnackbar) {
|
||||
doNotify({
|
||||
message: __('Text copied'),
|
||||
displayType: ['snackbar'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -91,8 +91,7 @@ class FileCard extends React.PureComponent<Props> {
|
|||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<CardMedia thumbnail={thumbnail} />
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title--small card__title--file-card">
|
||||
<div className="card__title card__title--file-card">
|
||||
<TruncatedText text={title} lines={2} />
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
|
@ -103,7 +102,6 @@ class FileCard extends React.PureComponent<Props> {
|
|||
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
|
||||
{fileInfo && <Icon icon={icons.LOCAL} />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||
|
|
|
@ -108,36 +108,14 @@ class FileTile extends React.PureComponent<Props> {
|
|||
>
|
||||
<CardMedia title={title || name} thumbnail={thumbnail} />
|
||||
<div className="file-tile__info">
|
||||
{isResolvingUri && (
|
||||
<div
|
||||
className={classnames({
|
||||
'card__title--small': size !== 'large',
|
||||
'card__title--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
{__('Loading...')}
|
||||
</div>
|
||||
)}
|
||||
{isResolvingUri && <div className="file-tile__title">{__('Loading...')}</div>}
|
||||
{!isResolvingUri && (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={classnames({
|
||||
'card__title--file': size === 'regular',
|
||||
'card__title--x-small': size === 'small',
|
||||
'card__title--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
<div className="file-tile__title">
|
||||
<TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} />
|
||||
</div>
|
||||
<div
|
||||
className={classnames('card__subtitle', {
|
||||
'card__subtitle--x-small': size === 'small',
|
||||
'card__subtitle--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
<span className="file-tile__channel">
|
||||
<div className="card__subtitle">
|
||||
{showUri ? uri : <UriIndicator uri={uri} link />}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card__file-properties">
|
||||
<FilePrice hideFree uri={uri} />
|
||||
|
@ -145,12 +123,7 @@ class FileTile extends React.PureComponent<Props> {
|
|||
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />}
|
||||
</div>
|
||||
{displayDescription && (
|
||||
<div
|
||||
className={classnames('card__subtext', {
|
||||
'card__subtext--small': size !== 'small',
|
||||
'card__subtext--large': size === 'large',
|
||||
})}
|
||||
>
|
||||
<div className="card__subtext">
|
||||
<TruncatedText text={description} lines={size === 'large' ? 4 : 3} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import FormField from './view';
|
||||
|
||||
export default connect(null, null, null, { withRef: true })(FormField);
|
|
@ -1,200 +0,0 @@
|
|||
// This file is going to die
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import { formFieldNestedLabelTypes, formFieldId } from 'component/common/form';
|
||||
import style from 'react-simplemde-editor/dist/simplemde.min.css';
|
||||
|
||||
const formFieldFileSelectorTypes = ['file', 'directory'];
|
||||
|
||||
class FormField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
prefix: PropTypes.string,
|
||||
postfix: PropTypes.string,
|
||||
hasError: PropTypes.bool,
|
||||
trim: PropTypes.bool,
|
||||
regexp: PropTypes.oneOfType([PropTypes.instanceOf(RegExp), PropTypes.string]),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
trim: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._fieldRequiredText = __('This field is required');
|
||||
this._type = null;
|
||||
this._element = null;
|
||||
this._extraElementProps = {};
|
||||
|
||||
this.state = {
|
||||
isError: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
|
||||
this._element = 'input';
|
||||
this._type = this.props.type;
|
||||
} else if (this.props.type == 'text-number') {
|
||||
this._element = 'input';
|
||||
this._type = 'text';
|
||||
} else if (this.props.type == 'SimpleMDE') {
|
||||
this._element = SimpleMDE;
|
||||
this._type = 'textarea';
|
||||
this._extraElementProps.options = {
|
||||
placeholder: this.props.placeholder,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
};
|
||||
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
|
||||
this._element = 'input';
|
||||
this._type = 'hidden';
|
||||
} else {
|
||||
// Non <input> field, e.g. <select>, <textarea>
|
||||
this._element = this.props.type;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
/**
|
||||
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
|
||||
* https://github.com/facebook/react/issues/3468
|
||||
*/
|
||||
if (this.props.type == 'directory') {
|
||||
this.refs.field.webkitdirectory = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChosen(path) {
|
||||
this.refs.field.value = path;
|
||||
if (this.props.onChange) {
|
||||
// Updating inputs programmatically doesn't generate an event, so we have to make our own
|
||||
const event = new Event('change', { bubbles: true });
|
||||
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
|
||||
this.props.onChange(event);
|
||||
}
|
||||
}
|
||||
|
||||
showError(text) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorMessage: text,
|
||||
});
|
||||
}
|
||||
|
||||
clearError() {
|
||||
this.setState({
|
||||
isError: false,
|
||||
errorMessage: '',
|
||||
});
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (this.props.type == 'checkbox') {
|
||||
return this.refs.field.checked;
|
||||
} else if (this.props.type == 'SimpleMDE') {
|
||||
return this.refs.field.simplemde.value();
|
||||
}
|
||||
return this.props.trim ? this.refs.field.value.trim() : this.refs.field.value;
|
||||
}
|
||||
|
||||
getSelectedElement() {
|
||||
return this.refs.field.options[this.refs.field.selectedIndex];
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.refs.field.options;
|
||||
}
|
||||
|
||||
validate() {
|
||||
if ('regexp' in this.props) {
|
||||
if (!this.getValue().match(this.props.regexp)) {
|
||||
this.showError(__('Invalid format.'));
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
}
|
||||
this.props.onBlur && this.props.onBlur();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
// Pass all unhandled props to the field element
|
||||
const otherProps = Object.assign({}, this.props),
|
||||
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
|
||||
elementId = this.props.elementId ? this.props.elementId : formFieldId(),
|
||||
renderElementInsideLabel =
|
||||
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
|
||||
|
||||
delete otherProps.type;
|
||||
delete otherProps.label;
|
||||
delete otherProps.hasError;
|
||||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
delete otherProps.dispatch;
|
||||
delete otherProps.regexp;
|
||||
delete otherProps.trim;
|
||||
|
||||
const element = (
|
||||
<this._element
|
||||
id={elementId}
|
||||
type={this._type}
|
||||
name={this.props.name}
|
||||
ref="field"
|
||||
placeholder={this.props.placeholder}
|
||||
onBlur={() => this.validate()}
|
||||
onFocus={() => this.props.onFocus && this.props.onFocus()}
|
||||
className={`form-field__input form-field__input-${this.props.type} ${this.props.className ||
|
||||
''}${isError ? 'form-field__input--error' : ''}`}
|
||||
{...otherProps}
|
||||
{...this._extraElementProps}
|
||||
>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`form-field form-field--${this.props.type}`}>
|
||||
{this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : ''}
|
||||
{element}
|
||||
{renderElementInsideLabel && (
|
||||
<label
|
||||
htmlFor={elementId}
|
||||
className={`form-field__label ${isError ? 'form-field__label--error' : ''}`}
|
||||
>
|
||||
{this.props.label}
|
||||
</label>
|
||||
)}
|
||||
{formFieldFileSelectorTypes.includes(this.props.type) ? (
|
||||
<FileSelector
|
||||
type={this.props.type}
|
||||
onFileChosen={this.handleFileChosen.bind(this)}
|
||||
{...(this.props.defaultValue ? { initPath: this.props.defaultValue } : {})}
|
||||
/>
|
||||
) : null}
|
||||
{this.props.postfix ? (
|
||||
<span className="form-field__postfix">{this.props.postfix}</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{isError && this.state.errorMessage ? (
|
||||
<div className="form-field__error">{this.state.errorMessage}</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormField;
|
||||
/* eslint-enable */
|
|
@ -5,13 +5,11 @@ import { CC_LICENSES, COPYRIGHT, OTHER, PUBLIC_DOMAIN, NONE } from 'constants/li
|
|||
|
||||
type Props = {
|
||||
licenseType: string,
|
||||
copyrightNotice: ?string,
|
||||
licenseUrl: ?string,
|
||||
otherLicenseDescription: ?string,
|
||||
handleLicenseChange: (string, string) => void,
|
||||
handleLicenseDescriptionChange: (SyntheticInputEvent<*>) => void,
|
||||
handleLicenseUrlChange: (SyntheticInputEvent<*>) => void,
|
||||
handleCopyrightNoticeChange: (SyntheticInputEvent<*>) => void,
|
||||
};
|
||||
|
||||
class LicenseType extends React.PureComponent<Props> {
|
||||
|
@ -38,11 +36,8 @@ class LicenseType extends React.PureComponent<Props> {
|
|||
licenseType,
|
||||
otherLicenseDescription,
|
||||
licenseUrl,
|
||||
copyrightNotice,
|
||||
handleLicenseChange,
|
||||
handleLicenseDescriptionChange,
|
||||
handleLicenseUrlChange,
|
||||
handleCopyrightNoticeChange,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -72,8 +67,8 @@ class LicenseType extends React.PureComponent<Props> {
|
|||
label={__('Copyright notice')}
|
||||
type="text"
|
||||
name="copyright-notice"
|
||||
value={copyrightNotice}
|
||||
onChange={handleCopyrightNoticeChange}
|
||||
value={otherLicenseDescription}
|
||||
onChange={handleLicenseDescriptionChange}
|
||||
/>
|
||||
</FormRow>
|
||||
)}
|
||||
|
|
|
@ -37,7 +37,6 @@ type Props = {
|
|||
name: ?string,
|
||||
tosAccepted: boolean,
|
||||
updatePublishForm: UpdatePublishFormData => void,
|
||||
bid: number,
|
||||
nameError: ?string,
|
||||
isResolvingUri: boolean,
|
||||
winningBidForClaimUri: number,
|
||||
|
@ -45,7 +44,6 @@ type Props = {
|
|||
licenseType: string,
|
||||
otherLicenseDescription: ?string,
|
||||
licenseUrl: ?string,
|
||||
copyrightNotice: ?string,
|
||||
uri: ?string,
|
||||
bidError: ?string,
|
||||
publishing: boolean,
|
||||
|
@ -200,7 +198,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
handlePublish() {
|
||||
const {
|
||||
filePath,
|
||||
copyrightNotice,
|
||||
licenseType,
|
||||
licenseUrl,
|
||||
otherLicenseDescription,
|
||||
|
@ -211,8 +208,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
let publishingLicense;
|
||||
switch (licenseType) {
|
||||
case COPYRIGHT:
|
||||
publishingLicense = copyrightNotice;
|
||||
break;
|
||||
case OTHER:
|
||||
publishingLicense = otherLicenseDescription;
|
||||
break;
|
||||
|
@ -233,7 +228,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
license: publishingLicense,
|
||||
licenseUrl: publishingLicenseUrl,
|
||||
otherLicenseDescription,
|
||||
copyrightNotice,
|
||||
name: this.props.name,
|
||||
contentIsFree: this.props.contentIsFree,
|
||||
price: this.props.price,
|
||||
|
@ -301,7 +295,7 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
{!title && <div>{__('A title is required')}</div>}
|
||||
{!name && <div>{__('A URL is required')}</div>}
|
||||
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
|
||||
{!bid && <div>{__('A bid amount is required')}</div>}
|
||||
{!bid && <div>{__('A deposit amount is required')}</div>}
|
||||
{!!bid && bidError && <div>{bidError}</div>}
|
||||
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS && (
|
||||
<div>{__('Please wait for thumbnail to finish uploading')}</div>
|
||||
|
@ -339,7 +333,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
licenseType,
|
||||
otherLicenseDescription,
|
||||
licenseUrl,
|
||||
copyrightNotice,
|
||||
uri,
|
||||
bidError,
|
||||
publishing,
|
||||
|
@ -535,7 +528,7 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
step="any"
|
||||
label={__('Deposit')}
|
||||
postfix="LBC"
|
||||
value={bid}
|
||||
value={bid || ''}
|
||||
error={bidError}
|
||||
min="0"
|
||||
disabled={!name}
|
||||
|
@ -585,7 +578,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
licenseType={licenseType}
|
||||
otherLicenseDescription={otherLicenseDescription}
|
||||
licenseUrl={licenseUrl}
|
||||
copyrightNotice={copyrightNotice}
|
||||
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
|
||||
updatePublishForm({
|
||||
licenseType: newLicenseType,
|
||||
|
@ -600,9 +592,6 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
handleLicenseUrlChange={event =>
|
||||
updatePublishForm({ licenseUrl: event.target.value })
|
||||
}
|
||||
handleCopyrightNoticeChange={event =>
|
||||
updatePublishForm({ copyrightNotice: event.target.value })
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectUnclaimedRewardValue, selectFetchingRewards, doRewardList } from 'lbryinc';
|
||||
import { doFetchRewardedContent } from 'redux/actions/content';
|
||||
import {
|
||||
selectUnclaimedRewardValue,
|
||||
selectFetchingRewards,
|
||||
doRewardList,
|
||||
doFetchRewardedContent,
|
||||
} from 'lbryinc';
|
||||
import RewardSummary from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
|
|
@ -89,7 +89,7 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
|
|||
type="text"
|
||||
name="content_thumbnail"
|
||||
label="URL"
|
||||
placeholder="http://spee.ch/mylogo"
|
||||
placeholder="https://spee.ch/mylogo"
|
||||
value={thumbnail}
|
||||
disabled={formDisabled}
|
||||
onChange={this.handleThumbnailChange}
|
||||
|
|
|
@ -101,7 +101,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
)}
|
||||
|
||||
{shiftState === statuses.RECEIVED && (
|
||||
<div className="card__content--extra-vertical-space">
|
||||
<div>
|
||||
<p>
|
||||
{__('ShapeShift has received your payment! Sending the funds to your LBRY wallet.')}
|
||||
</p>
|
||||
|
@ -110,7 +110,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
)}
|
||||
|
||||
{shiftState === statuses.COMPLETE && (
|
||||
<div className="card__content--extra-vertical-space">
|
||||
<div>
|
||||
<p>{__('Transaction complete! You should see the new LBC in your wallet.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -3,12 +3,13 @@ import React from 'react';
|
|||
import type { Claim } from 'types/claim';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import Address from 'component/address';
|
||||
import CopyableText from 'component/copyableText';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
|
||||
type Props = {
|
||||
claim: Claim,
|
||||
onDone: () => void,
|
||||
speechShareable: boolean,
|
||||
};
|
||||
|
||||
class SocialShare extends React.PureComponent<Props> {
|
||||
|
@ -27,20 +28,27 @@ class SocialShare extends React.PureComponent<Props> {
|
|||
channel_name: channelName,
|
||||
value,
|
||||
} = this.props.claim;
|
||||
const { speechShareable, onDone } = this.props;
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
const { onDone } = this.props;
|
||||
const speechPrefix = 'http://spee.ch/';
|
||||
const speechPrefix = 'https://spee.ch/';
|
||||
const lbryPrefix = 'https://open.lbry.io/';
|
||||
|
||||
const speechURL =
|
||||
channelName && channelClaimId
|
||||
? `${speechPrefix}${channelName}:${channelClaimId}/${claimName}`
|
||||
: `${speechPrefix}${claimName}#${claimId}`;
|
||||
|
||||
const lbryURL = `${lbryPrefix}${claimName}#${claimId}`;
|
||||
|
||||
return (
|
||||
<section className="card__content">
|
||||
<Address address={speechURL} noSnackbar />
|
||||
{speechShareable && (
|
||||
<div className="card__content">
|
||||
<label className="card__subtitle">{__('Web link')}</label>
|
||||
<CopyableText copyable={speechURL} noSnackbar />
|
||||
<div className="card__actions card__actions--center">
|
||||
<Tooltip onComponent body={__('Facebook')}>
|
||||
<ToolTip onComponent body={__('Facebook')}>
|
||||
<Button
|
||||
iconColor="blue"
|
||||
icon={icons.FACEBOOK}
|
||||
|
@ -48,8 +56,8 @@ class SocialShare extends React.PureComponent<Props> {
|
|||
label={__('')}
|
||||
href={`https://facebook.com/sharer/sharer.php?u=${speechURL}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip onComponent body={__('Twitter')}>
|
||||
</ToolTip>
|
||||
<ToolTip onComponent body={__('Twitter')}>
|
||||
<Button
|
||||
iconColor="blue"
|
||||
icon={icons.TWITTER}
|
||||
|
@ -57,8 +65,8 @@ class SocialShare extends React.PureComponent<Props> {
|
|||
label={__('')}
|
||||
href={`https://twitter.com/home?status=${speechURL}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip onComponent body={__('View on Spee.ch')}>
|
||||
</ToolTip>
|
||||
<ToolTip onComponent body={__('View on Spee.ch')}>
|
||||
<Button
|
||||
icon={icons.GLOBE}
|
||||
iconColor="blue"
|
||||
|
@ -66,7 +74,33 @@ class SocialShare extends React.PureComponent<Props> {
|
|||
label={__('')}
|
||||
href={`${speechURL}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="card__content">
|
||||
<label className="card__subtitle">{__('LBRY App link')}</label>
|
||||
<CopyableText copyable={lbryURL} noSnackbar />
|
||||
<div className="card__actions card__actions--center">
|
||||
<ToolTip onComponent body={__('Facebook')}>
|
||||
<Button
|
||||
iconColor="blue"
|
||||
icon={icons.FACEBOOK}
|
||||
button="alt"
|
||||
label={__('')}
|
||||
href={`https://facebook.com/sharer/sharer.php?u=${lbryURL}`}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip onComponent body={__('Twitter')}>
|
||||
<Button
|
||||
iconColor="blue"
|
||||
icon={icons.TWITTER}
|
||||
button="alt"
|
||||
label={__('')}
|
||||
href={`https://twitter.com/home?status=${lbryURL}`}
|
||||
/>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<Button button="link" label={__('Done')} onClick={onDone} />
|
||||
|
|
|
@ -24,6 +24,10 @@ class TransactionListRecent extends React.PureComponent<Props> {
|
|||
return (
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{__('Recent Transactions')}</div>
|
||||
<div className="card__subtitle">
|
||||
{__('To view all of your transactions, navigate to the')}{' '}
|
||||
<Button button="link" navigate="/history" label={__('transactions page')} />.
|
||||
</div>
|
||||
{fetchingTransactions && (
|
||||
<div className="card__content">
|
||||
<BusyIndicator message={__('Loading transactions')} />
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import TruncatedMarkdown from './view';
|
||||
|
||||
export default connect()(TruncatedMarkdown);
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
class TruncatedMarkdown extends React.PureComponent {
|
||||
static propTypes = {
|
||||
lines: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
lines: null,
|
||||
};
|
||||
|
||||
transformMarkdown(text) {
|
||||
// render markdown to html string then trim html tag
|
||||
const htmlString = ReactDOMServer.renderToStaticMarkup(
|
||||
<ReactMarkdown source={this.props.children} />
|
||||
);
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = htmlString;
|
||||
return txt.value.replace(/<(?:.|\n)*?>/gm, '');
|
||||
}
|
||||
|
||||
render() {
|
||||
const content =
|
||||
this.props.children && typeof this.props.children === 'string'
|
||||
? this.transformMarkdown(this.props.children)
|
||||
: this.props.children;
|
||||
return (
|
||||
<span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TruncatedMarkdown;
|
|
@ -2,7 +2,6 @@
|
|||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { buildURI } from 'lbry-redux';
|
||||
import classnames from 'classnames';
|
||||
import type { Claim } from 'types/claim';
|
||||
|
||||
type Props = {
|
||||
|
@ -59,17 +58,7 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
channelLink = link ? buildURI({ channelName, claimId: channelClaimId }) : false;
|
||||
}
|
||||
|
||||
const inner = (
|
||||
<span>
|
||||
<span
|
||||
className={classnames('channel-name', {
|
||||
'button-text no-underline': link,
|
||||
})}
|
||||
>
|
||||
{channelName}
|
||||
</span>{' '}
|
||||
</span>
|
||||
);
|
||||
const inner = <span className="channel-name">{channelName}</span>;
|
||||
|
||||
if (!channelLink) {
|
||||
return inner;
|
||||
|
|
|
@ -99,7 +99,8 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
|
||||
const allSelected = Object.keys(itemsSelected).length === history.length;
|
||||
const selectHandler = allSelected ? this.unselectAll : this.selectAll;
|
||||
return (
|
||||
|
||||
return history.length ? (
|
||||
<React.Fragment>
|
||||
<div className="card__actions card__actions--between">
|
||||
{Object.keys(itemsSelected).length ? (
|
||||
|
@ -109,7 +110,6 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
{/* Using an empty span so spacing stays the same if the button isn't rendered */}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button
|
||||
button="link"
|
||||
label={allSelected ? __('Cancel') : __('Select All')}
|
||||
|
@ -117,8 +117,7 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
/>
|
||||
</div>
|
||||
{!!history.length && (
|
||||
<table className="card--section table table--stretch table--history">
|
||||
<tbody>
|
||||
<section className="item-list">
|
||||
{history.map(item => (
|
||||
<UserHistoryItem
|
||||
key={item.uri}
|
||||
|
@ -130,8 +129,7 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)}
|
||||
{pageCount > 1 && (
|
||||
<FormRow padded verticallyCentered centered>
|
||||
|
@ -161,6 +159,13 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
</FormRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className="page__empty">
|
||||
{__("You don't have anything saved in history yet, go check out some content on LBRY!")}
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,18 +36,16 @@ class UserHistoryItem extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
<div
|
||||
role="button"
|
||||
onClick={onSelect}
|
||||
className={classnames({
|
||||
history__selected: selected,
|
||||
className={classnames('item-list__item', {
|
||||
'item-list__item--selected': selected,
|
||||
})}
|
||||
>
|
||||
<td>
|
||||
<input checked={selected} type="checkbox" onClick={onSelect} />
|
||||
</td>
|
||||
<td>{moment(lastViewed).from(moment())}</td>
|
||||
<td>{title}</td>
|
||||
<td>
|
||||
<span className="time time--ago">{moment(lastViewed).from(moment())}</span>
|
||||
<span className="item-list__item--cutoff">{title}</span>
|
||||
<Button
|
||||
tourniquet
|
||||
button="link"
|
||||
|
@ -55,8 +53,7 @@ class UserHistoryItem extends React.PureComponent<Props> {
|
|||
navigate="/show"
|
||||
navigateParams={{ uri }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import CardVerify from 'component/cardVerify';
|
||||
import Lbryio from 'lbryinc';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -19,7 +19,7 @@ export default (props: Props) => {
|
|||
icon={icons.GLOBE}
|
||||
button="alt"
|
||||
label={__('Share')}
|
||||
href={`http://spee.ch/${speechURL}`}
|
||||
href={`https://spee.ch/${speechURL}`}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ https://github.com/reactjs/react-autocomplete/issues/239
|
|||
/* eslint-disable */
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const { findDOMNode } = require('react-dom');
|
||||
const scrollIntoView = require('dom-scroll-into-view');
|
||||
|
||||
|
@ -45,129 +44,129 @@ function getScrollOffset() {
|
|||
}
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* The items to display in the dropdown menu
|
||||
*/
|
||||
items: PropTypes.array.isRequired,
|
||||
/**
|
||||
* The value to display in the input field
|
||||
*/
|
||||
value: PropTypes.any,
|
||||
/**
|
||||
* Arguments: `event: Event, value: String`
|
||||
*
|
||||
* Invoked every time the user changes the input's value.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `value: String, item: Any`
|
||||
*
|
||||
* Invoked when the user selects an item from the dropdown menu.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `item: Any, value: String`
|
||||
*
|
||||
* Invoked for each entry in `items` and its return value is used to
|
||||
* determine whether or not it should be displayed in the dropdown menu.
|
||||
* By default all items are always rendered.
|
||||
*/
|
||||
shouldItemRender: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `itemA: Any, itemB: Any, value: String`
|
||||
*
|
||||
* The function which is used to sort `items` before display.
|
||||
*/
|
||||
sortItems: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `item: Any`
|
||||
*
|
||||
* Used to read the display value from each entry in `items`.
|
||||
*/
|
||||
getItemValue: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
|
||||
*
|
||||
* Invoked for each entry in `items` that also passes `shouldItemRender` to
|
||||
* generate the render tree for each item in the dropdown menu. `styles` is
|
||||
* an optional set of styles that can be applied to improve the look/feel
|
||||
* of the items in the dropdown menu.
|
||||
*/
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Arguments: `items: Array<Any>, value: String, styles: Object`
|
||||
*
|
||||
* Invoked to generate the render tree for the dropdown menu. Ensure the
|
||||
* returned tree includes every entry in `items` or else the highlight order
|
||||
* and keyboard navigation logic will break. `styles` will contain
|
||||
* { top, left, minWidth } which are the coordinates of the top-left corner
|
||||
* and the width of the dropdown menu.
|
||||
*/
|
||||
renderMenu: PropTypes.func,
|
||||
/**
|
||||
* Styles that are applied to the dropdown menu in the default `renderMenu`
|
||||
* implementation. If you override `renderMenu` and you want to use
|
||||
* `menuStyle` you must manually apply them (`this.props.menuStyle`).
|
||||
*/
|
||||
menuStyle: PropTypes.object,
|
||||
/**
|
||||
* Arguments: `props: Object`
|
||||
*
|
||||
* Invoked to generate the input element. The `props` argument is the result
|
||||
* of merging `props.inputProps` with a selection of props that are required
|
||||
* both for functionality and accessibility. At the very least you need to
|
||||
* apply `props.ref` and all `props.on<event>` event handlers. Failing to do
|
||||
* this will cause `Autocomplete` to behave unexpectedly.
|
||||
*/
|
||||
renderInput: PropTypes.func,
|
||||
/**
|
||||
* Props passed to `props.renderInput`. By default these props will be
|
||||
* applied to the `<input />` element rendered by `Autocomplete`, unless you
|
||||
* have specified a custom value for `props.renderInput`. Any properties
|
||||
* supported by `HTMLInputElement` can be specified, apart from the
|
||||
* following which are set by `Autocomplete`: value, autoComplete, role,
|
||||
* aria-autocomplete. `inputProps` is commonly used for (but not limited to)
|
||||
* placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
|
||||
*/
|
||||
inputProps: PropTypes.object,
|
||||
/**
|
||||
* Props that are applied to the element which wraps the `<input />` and
|
||||
* dropdown menu elements rendered by `Autocomplete`.
|
||||
*/
|
||||
wrapperProps: PropTypes.object,
|
||||
/**
|
||||
* This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
|
||||
* Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
|
||||
* will win if it contains a `style` entry.
|
||||
*/
|
||||
wrapperStyle: PropTypes.object,
|
||||
/**
|
||||
* Whether or not to automatically highlight the top match in the dropdown
|
||||
* menu.
|
||||
*/
|
||||
autoHighlight: PropTypes.bool,
|
||||
/**
|
||||
* Whether or not to automatically select the highlighted item when the
|
||||
* `<input>` loses focus.
|
||||
*/
|
||||
selectOnBlur: PropTypes.bool,
|
||||
/**
|
||||
* Arguments: `isOpen: Boolean`
|
||||
*
|
||||
* Invoked every time the dropdown menu's visibility changes (i.e. every
|
||||
* time it is displayed/hidden).
|
||||
*/
|
||||
onMenuVisibilityChange: PropTypes.func,
|
||||
/**
|
||||
* Used to override the internal logic which displays/hides the dropdown
|
||||
* menu. This is useful if you want to force a certain state based on your
|
||||
* UX/business logic. Use it together with `onMenuVisibilityChange` for
|
||||
* fine-grained control over the dropdown menu dynamics.
|
||||
*/
|
||||
open: PropTypes.bool,
|
||||
debug: PropTypes.bool,
|
||||
};
|
||||
// static propTypes = {
|
||||
// /**
|
||||
// * The items to display in the dropdown menu
|
||||
// */
|
||||
// items: PropTypes.array.isRequired,
|
||||
// /**
|
||||
// * The value to display in the input field
|
||||
// */
|
||||
// value: PropTypes.any,
|
||||
// /**
|
||||
// * Arguments: `event: Event, value: String`
|
||||
// *
|
||||
// * Invoked every time the user changes the input's value.
|
||||
// */
|
||||
// onChange: PropTypes.func,
|
||||
// /**
|
||||
// * Arguments: `value: String, item: Any`
|
||||
// *
|
||||
// * Invoked when the user selects an item from the dropdown menu.
|
||||
// */
|
||||
// onSelect: PropTypes.func,
|
||||
// /**
|
||||
// * Arguments: `item: Any, value: String`
|
||||
// *
|
||||
// * Invoked for each entry in `items` and its return value is used to
|
||||
// * determine whether or not it should be displayed in the dropdown menu.
|
||||
// * By default all items are always rendered.
|
||||
// */
|
||||
// shouldItemRender: PropTypes.func,
|
||||
// /**
|
||||
// * Arguments: `itemA: Any, itemB: Any, value: String`
|
||||
// *
|
||||
// * The function which is used to sort `items` before display.
|
||||
// */
|
||||
// sortItems: PropTypes.func,
|
||||
// /**
|
||||
// * Arguments: `item: Any`
|
||||
// *
|
||||
// * Used to read the display value from each entry in `items`.
|
||||
// */
|
||||
// getItemValue: PropTypes.func.isRequired,
|
||||
// /**
|
||||
// * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
|
||||
// *
|
||||
// * Invoked for each entry in `items` that also passes `shouldItemRender` to
|
||||
// * generate the render tree for each item in the dropdown menu. `styles` is
|
||||
// * an optional set of styles that can be applied to improve the look/feel
|
||||
// * of the items in the dropdown menu.
|
||||
// */
|
||||
// renderItem: PropTypes.func.isRequired,
|
||||
// /**
|
||||
// * Arguments: `items: Array<Any>, value: String, styles: Object`
|
||||
// *
|
||||
// * Invoked to generate the render tree for the dropdown menu. Ensure the
|
||||
// * returned tree includes every entry in `items` or else the highlight order
|
||||
// * and keyboard navigation logic will break. `styles` will contain
|
||||
// * { top, left, minWidth } which are the coordinates of the top-left corner
|
||||
// * and the width of the dropdown menu.
|
||||
// */
|
||||
// renderMenu: PropTypes.func,
|
||||
// /**
|
||||
// * Styles that are applied to the dropdown menu in the default `renderMenu`
|
||||
// * implementation. If you override `renderMenu` and you want to use
|
||||
// * `menuStyle` you must manually apply them (`this.props.menuStyle`).
|
||||
// */
|
||||
// menuStyle: PropTypes.object,
|
||||
// /**
|
||||
// * Arguments: `props: Object`
|
||||
// *
|
||||
// * Invoked to generate the input element. The `props` argument is the result
|
||||
// * of merging `props.inputProps` with a selection of props that are required
|
||||
// * both for functionality and accessibility. At the very least you need to
|
||||
// * apply `props.ref` and all `props.on<event>` event handlers. Failing to do
|
||||
// * this will cause `Autocomplete` to behave unexpectedly.
|
||||
// */
|
||||
// renderInput: PropTypes.func,
|
||||
// /**
|
||||
// * Props passed to `props.renderInput`. By default these props will be
|
||||
// * applied to the `<input />` element rendered by `Autocomplete`, unless you
|
||||
// * have specified a custom value for `props.renderInput`. Any properties
|
||||
// * supported by `HTMLInputElement` can be specified, apart from the
|
||||
// * following which are set by `Autocomplete`: value, autoComplete, role,
|
||||
// * aria-autocomplete. `inputProps` is commonly used for (but not limited to)
|
||||
// * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
|
||||
// */
|
||||
// inputProps: PropTypes.object,
|
||||
// /**
|
||||
// * Props that are applied to the element which wraps the `<input />` and
|
||||
// * dropdown menu elements rendered by `Autocomplete`.
|
||||
// */
|
||||
// wrapperProps: PropTypes.object,
|
||||
// /**
|
||||
// * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
|
||||
// * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
|
||||
// * will win if it contains a `style` entry.
|
||||
// */
|
||||
// wrapperStyle: PropTypes.object,
|
||||
// /**
|
||||
// * Whether or not to automatically highlight the top match in the dropdown
|
||||
// * menu.
|
||||
// */
|
||||
// autoHighlight: PropTypes.bool,
|
||||
// /**
|
||||
// * Whether or not to automatically select the highlighted item when the
|
||||
// * `<input>` loses focus.
|
||||
// */
|
||||
// selectOnBlur: PropTypes.bool,
|
||||
// /**
|
||||
// * Arguments: `isOpen: Boolean`
|
||||
// *
|
||||
// * Invoked every time the dropdown menu's visibility changes (i.e. every
|
||||
// * time it is displayed/hidden).
|
||||
// */
|
||||
// onMenuVisibilityChange: PropTypes.func,
|
||||
// /**
|
||||
// * Used to override the internal logic which displays/hides the dropdown
|
||||
// * menu. This is useful if you want to force a certain state based on your
|
||||
// * UX/business logic. Use it together with `onMenuVisibilityChange` for
|
||||
// * fine-grained control over the dropdown menu dynamics.
|
||||
// */
|
||||
// open: PropTypes.bool,
|
||||
// debug: PropTypes.bool,
|
||||
// };
|
||||
|
||||
static defaultProps = {
|
||||
value: '',
|
||||
|
|
|
@ -7,24 +7,36 @@ import { parseQueryParams } from 'util/query_params';
|
|||
import * as icons from 'constants/icons';
|
||||
import Autocomplete from './internal/autocomplete';
|
||||
|
||||
const L_KEY_CODE = 76;
|
||||
const ESC_KEY_CODE = 27;
|
||||
|
||||
type Props = {
|
||||
updateSearchQuery: string => void,
|
||||
onSearch: string => void,
|
||||
onSearch: (string, ?number) => void,
|
||||
onSubmit: (string, {}) => void,
|
||||
wunderbarValue: ?string,
|
||||
suggestions: Array<string>,
|
||||
doFocus: () => void,
|
||||
doBlur: () => void,
|
||||
resultCount: number,
|
||||
focused: boolean,
|
||||
};
|
||||
|
||||
class WunderBar extends React.PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
(this: any).handleSubmit = this.handleSubmit.bind(this);
|
||||
(this: any).handleChange = this.handleChange.bind(this);
|
||||
this.input = undefined;
|
||||
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
getSuggestionIcon = (type: string) => {
|
||||
|
@ -38,6 +50,31 @@ class WunderBar extends React.PureComponent<Props> {
|
|||
}
|
||||
};
|
||||
|
||||
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
|
||||
const { ctrlKey, metaKey, keyCode } = event;
|
||||
const { doFocus, doBlur, focused } = this.props;
|
||||
|
||||
if (!this.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (focused && keyCode === ESC_KEY_CODE) {
|
||||
doBlur();
|
||||
this.input.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldFocus =
|
||||
process.platform === 'darwin'
|
||||
? keyCode === L_KEY_CODE && metaKey
|
||||
: keyCode === L_KEY_CODE && ctrlKey;
|
||||
|
||||
if (shouldFocus) {
|
||||
doFocus();
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e: SyntheticInputEvent<*>) {
|
||||
const { updateSearchQuery } = this.props;
|
||||
const { value } = e.target;
|
||||
|
@ -106,6 +143,10 @@ class WunderBar extends React.PureComponent<Props> {
|
|||
renderInput={props => (
|
||||
<input
|
||||
{...props}
|
||||
ref={el => {
|
||||
props.ref(el);
|
||||
this.input = el;
|
||||
}}
|
||||
className="wunderbar__input"
|
||||
placeholder="Enter LBRY URL here or search for videos, music, games and more"
|
||||
/>
|
||||
|
|
|
@ -6,14 +6,15 @@ import SocialShare from 'component/socialShare';
|
|||
type Props = {
|
||||
closeModal: () => void,
|
||||
uri: string,
|
||||
speechShareable: boolean,
|
||||
};
|
||||
|
||||
class ModalSocialShare extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { closeModal, uri } = this.props;
|
||||
const { closeModal, uri, speechShareable } = this.props;
|
||||
return (
|
||||
<Modal isOpen onAborted={closeModal} type="custom" title={__('Share')}>
|
||||
<SocialShare uri={uri} onDone={closeModal} />
|
||||
<SocialShare uri={uri} onDone={closeModal} speechShareable={speechShareable} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -80,17 +80,17 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<Page notContained>
|
||||
<section className="card__channel-info card__channel-info--large">
|
||||
<section>
|
||||
<h1>
|
||||
{name}
|
||||
{fetching && <BusyIndicator />}
|
||||
</h1>
|
||||
<div className="card__actions card__actions--no-margin">
|
||||
</section>
|
||||
<div className="card__actions">
|
||||
<SubscribeButton uri={permanentUrl} channelName={name} />
|
||||
<ViewOnWebButton claimId={claimId} claimName={name} />
|
||||
</div>
|
||||
</section>
|
||||
<section>{contentList}</section>
|
||||
<section className="card__content">{contentList}</section>
|
||||
{(!fetching || (claimsInChannel && claimsInChannel.length)) &&
|
||||
totalPages > 1 && (
|
||||
<FormRow verticallyCentered centered>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchFeaturedUris, doFetchRewardedContent } from 'redux/actions/content';
|
||||
import { selectFeaturedUris, selectFetchingFeaturedUris } from 'redux/selectors/content';
|
||||
import { selectFeaturedUris, selectFetchingFeaturedUris, doFetchFeaturedUris } from 'lbry-redux';
|
||||
import { doFetchRewardedContent } from 'lbryinc';
|
||||
import DiscoverPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
|
|
@ -145,12 +145,11 @@ class FilePage extends React.Component<Props> {
|
|||
if (channelName && channelClaimId) {
|
||||
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
|
||||
}
|
||||
const speechSharable =
|
||||
const speechShareable =
|
||||
costInfo &&
|
||||
costInfo.cost === 0 &&
|
||||
contentType &&
|
||||
['video', 'image'].includes(contentType.split('/')[0]);
|
||||
|
||||
// We want to use the short form uri for editing
|
||||
// This is what the user is used to seeing, they don't care about the claim id
|
||||
// We will select the claim id before they publish
|
||||
|
@ -184,21 +183,20 @@ class FilePage extends React.Component<Props> {
|
|||
))}
|
||||
|
||||
<div className="card__content">
|
||||
<div className="card__title-identity--file">
|
||||
<h1 className="card__title card__title--file">{title}</h1>
|
||||
<div className="card__title__space-between">
|
||||
<h1>{title}</h1>
|
||||
<div className="card__title-identity-icons">
|
||||
{isRewardContent && <Icon iconColor="red" tooltip="bottom" icon={icons.FEATURED} />}
|
||||
{isRewardContent && (
|
||||
<Icon size={20} iconColor="red" tooltip="bottom" icon={icons.FEATURED} />
|
||||
)}
|
||||
<FilePrice filePage uri={normalizeURI(uri)} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="card__subtitle card__subtitle--file">
|
||||
{__('Published on')}
|
||||
<span className="card__subtitle">
|
||||
<UriIndicator uri={uri} link /> {__('published on')}{' '}
|
||||
<DateTime block={height} show={DateTime.SHOW_DATE} />
|
||||
</span>
|
||||
{metadata.nsfw && <div>NSFW</div>}
|
||||
<div className="card__channel-info">
|
||||
<UriIndicator uri={uri} link />
|
||||
</div>
|
||||
<div className="card__actions card__actions--no-margin card__actions--between">
|
||||
<div className="card__actions">
|
||||
{claimIsMine ? (
|
||||
|
@ -222,7 +220,7 @@ class FilePage extends React.Component<Props> {
|
|||
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
|
||||
/>
|
||||
)}
|
||||
{speechSharable && (
|
||||
{speechShareable && (
|
||||
<Button
|
||||
button="alt"
|
||||
icon={icons.GLOBE}
|
||||
|
@ -237,7 +235,7 @@ class FilePage extends React.Component<Props> {
|
|||
<FileActions uri={uri} claimId={claim.claim_id} />
|
||||
</div>
|
||||
</div>
|
||||
<FormRow padded>
|
||||
<FormRow>
|
||||
<ToolTip direction="right" body={__('Automatically download and play free content.')}>
|
||||
<FormField
|
||||
name="autoplay"
|
||||
|
@ -248,7 +246,7 @@ class FilePage extends React.Component<Props> {
|
|||
/>
|
||||
</ToolTip>
|
||||
</FormRow>
|
||||
<div className="card__content--extra-padding">
|
||||
<div className="card__content">
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -199,7 +199,7 @@ class HelpPage extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
|
||||
{this.state.uiVersion && ver ? (
|
||||
<table className="table table--stretch table--help">
|
||||
<table className="card__content table table--stretch table--help">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{__('App')}</td>
|
||||
|
|
|
@ -42,8 +42,8 @@ class ShowPage extends React.PureComponent<Props> {
|
|||
if ((isResolvingUri && !claim) || !claim) {
|
||||
const { claimName } = parseURI(uri);
|
||||
innerContent = (
|
||||
<Page>
|
||||
<section className="card">
|
||||
<Page notContained>
|
||||
<section>
|
||||
<h1>{claimName}</h1>
|
||||
<div className="card__content">
|
||||
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
doHideNotification,
|
||||
} from 'lbry-redux';
|
||||
import Native from 'native';
|
||||
import { doFetchRewardedContent } from 'redux/actions/content';
|
||||
import { doFetchDaemonSettings } from 'redux/actions/settings';
|
||||
import { doAuthNavigate } from 'redux/actions/navigation';
|
||||
import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
|
||||
|
@ -27,7 +26,7 @@ import {
|
|||
selectRemoteVersion,
|
||||
selectUpgradeTimer,
|
||||
} from 'redux/selectors/app';
|
||||
import { doAuthenticate } from 'lbryinc';
|
||||
import { doAuthenticate, doFetchRewardedContent } from 'lbryinc';
|
||||
import { lbrySettings as config, version as appVersion } from 'package.json';
|
||||
|
||||
const { autoUpdater } = remote.require('electron-updater');
|
||||
|
|
|
@ -6,7 +6,6 @@ import { doNavigate } from 'redux/actions/navigation';
|
|||
import {
|
||||
setSubscriptionLatest,
|
||||
setSubscriptionNotification,
|
||||
setSubscriptionNotifications,
|
||||
} from 'redux/actions/subscriptions';
|
||||
import { selectNotifications } from 'redux/selectors/subscriptions';
|
||||
import { selectBadgeNumber } from 'redux/selectors/app';
|
||||
|
@ -16,8 +15,6 @@ import {
|
|||
Lbry,
|
||||
Lbryapi,
|
||||
buildURI,
|
||||
batchActions,
|
||||
doResolveUris,
|
||||
doFetchClaimListMine,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
|
@ -31,74 +28,9 @@ import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/sel
|
|||
import setBadge from 'util/setBadge';
|
||||
import setProgressBar from 'util/setProgressBar';
|
||||
import analytics from 'analytics';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||
|
||||
export function doFetchFeaturedUris() {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_FEATURED_CONTENT_STARTED,
|
||||
});
|
||||
|
||||
const success = ({ Uris }) => {
|
||||
const urisToResolve = Object.keys(Uris).reduce(
|
||||
(resolve, category) => [...resolve, ...Uris[category]],
|
||||
[]
|
||||
);
|
||||
|
||||
const actions = [
|
||||
doResolveUris(urisToResolve),
|
||||
{
|
||||
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||
data: {
|
||||
uris: Uris,
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
dispatch(batchActions(...actions));
|
||||
};
|
||||
|
||||
const failure = () => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||
data: {
|
||||
uris: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Lbryio.call('file', 'list_homepage').then(success, failure);
|
||||
};
|
||||
}
|
||||
|
||||
export function doFetchRewardedContent() {
|
||||
return dispatch => {
|
||||
const success = nameToClaimId => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
|
||||
data: {
|
||||
claimIds: Object.values(nameToClaimId),
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const failure = () => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
|
||||
data: {
|
||||
claimIds: [],
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Lbryio.call('reward', 'list_featured').then(success, failure);
|
||||
};
|
||||
}
|
||||
|
||||
export function doUpdateLoadStatus(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
Lbry.file_list({
|
||||
|
@ -360,13 +292,13 @@ export function doPurchaseUri(uri, specificCostInfo, shouldRecordViewEvent) {
|
|||
}
|
||||
|
||||
export function doFetchClaimsByChannel(uri, page) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
|
||||
data: { uri, page },
|
||||
});
|
||||
|
||||
Lbry.claim_list_by_channel({ uri, page: page || 1 }).then(result => {
|
||||
Lbry.claim_list_by_channel({ uri, page: page || 1, page_size: 48 }).then(result => {
|
||||
const claimResult = result[uri] || {};
|
||||
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;
|
||||
|
||||
|
|
|
@ -8,6 +8,14 @@ export function doNavigate(path, params = {}, options = {}) {
|
|||
return;
|
||||
}
|
||||
|
||||
// ensure uri always has "lbry://" prefix
|
||||
const navigationParams = params;
|
||||
if (path === '/show') {
|
||||
if (navigationParams.uri && !navigationParams.uri.startsWith('lbry://')) {
|
||||
navigationParams.uri = `lbry://${navigationParams.uri}`;
|
||||
}
|
||||
}
|
||||
|
||||
let url = path;
|
||||
if (params && Object.values(params).length) {
|
||||
url += `?${toQueryString(params)}`;
|
||||
|
|
|
@ -18,25 +18,13 @@ import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
|||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { CC_LICENSES, COPYRIGHT, OTHER } from 'constants/licenses';
|
||||
|
||||
type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH };
|
||||
type PromiseAction = Promise<Action>;
|
||||
type Dispatch = (action: Action | PromiseAction | Array<Action>) => any;
|
||||
type GetState = () => {};
|
||||
|
||||
export const doClearPublish = () => (dispatch: Dispatch): PromiseAction => {
|
||||
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
|
||||
return dispatch(doResetThumbnailStatus());
|
||||
};
|
||||
|
||||
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (
|
||||
dispatch: Dispatch
|
||||
): UpdatePublishFormAction =>
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: { ...publishFormValue },
|
||||
});
|
||||
|
||||
export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction => {
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
|
@ -73,6 +61,19 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction
|
|||
);
|
||||
};
|
||||
|
||||
export const doClearPublish = () => (dispatch: Dispatch): PromiseAction => {
|
||||
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
|
||||
return dispatch(doResetThumbnailStatus());
|
||||
};
|
||||
|
||||
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (
|
||||
dispatch: Dispatch
|
||||
): UpdatePublishFormAction =>
|
||||
dispatch({
|
||||
type: ACTIONS.UPDATE_PUBLISH_FORM,
|
||||
data: { ...publishFormValue },
|
||||
});
|
||||
|
||||
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
|
||||
const thumbnail = fs.readFileSync(filePath);
|
||||
const fileExt = path.extname(filePath);
|
||||
|
@ -164,15 +165,27 @@ export const doPrepareEdit = (claim: any, uri: string) => (dispatch: Dispatch) =
|
|||
description,
|
||||
fee,
|
||||
language,
|
||||
licenseType: license,
|
||||
licenseUrl,
|
||||
nsfw,
|
||||
thumbnail,
|
||||
title,
|
||||
uri,
|
||||
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
|
||||
licenseUrl,
|
||||
};
|
||||
|
||||
// Make sure custom liscence's are mapped properly
|
||||
if (!CC_LICENSES.some(({ value }) => value === license)) {
|
||||
if (!licenseUrl) {
|
||||
publishData.licenseType = COPYRIGHT;
|
||||
} else {
|
||||
publishData.licenseType = OTHER;
|
||||
}
|
||||
|
||||
publishData.otherLicenseDescription = license;
|
||||
} else {
|
||||
publishData.licenseType = license;
|
||||
}
|
||||
|
||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||
};
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ type PublishState = {
|
|||
bidError: ?string,
|
||||
otherLicenseDescription: string,
|
||||
licenseUrl: string,
|
||||
copyrightNotice: string,
|
||||
pendingPublishes: Array<any>,
|
||||
};
|
||||
|
||||
|
@ -54,7 +53,6 @@ export type UpdatePublishFormData = {
|
|||
bidError?: string,
|
||||
otherLicenseDescription?: string,
|
||||
licenseUrl?: string,
|
||||
copyrightNotice?: string,
|
||||
};
|
||||
|
||||
export type UpdatePublishFormAction = {
|
||||
|
@ -114,9 +112,8 @@ const defaultState: PublishState = {
|
|||
bid: 0.1,
|
||||
bidError: undefined,
|
||||
licenseType: 'None',
|
||||
otherLicenseDescription: '',
|
||||
otherLicenseDescription: 'All rights reserved',
|
||||
licenseUrl: '',
|
||||
copyrightNotice: 'All rights reserved',
|
||||
publishing: false,
|
||||
publishSuccess: false,
|
||||
publishError: undefined,
|
||||
|
|
|
@ -4,13 +4,6 @@ import { HISTORY_ITEMS_PER_PAGE } from 'constants/content';
|
|||
|
||||
export const selectState = state => state.content || {};
|
||||
|
||||
export const selectFeaturedUris = createSelector(selectState, state => state.featuredUris);
|
||||
|
||||
export const selectFetchingFeaturedUris = createSelector(
|
||||
selectState,
|
||||
state => state.fetchingFeaturedContent
|
||||
);
|
||||
|
||||
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||
|
||||
export const selectChannelClaimCounts = createSelector(
|
||||
|
|
|
@ -39,7 +39,7 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: 'metropolis-semibold';
|
||||
font-family: 'metropolis-medium';
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
|
@ -74,7 +74,6 @@ input {
|
|||
line-height: 1;
|
||||
cursor: text;
|
||||
background-color: transparent;
|
||||
font-family: 'metropolis-medium';
|
||||
|
||||
&[type='radio'],
|
||||
&[type='checkbox'],
|
||||
|
@ -92,6 +91,7 @@ input {
|
|||
color: var(--input-copyable-color);
|
||||
padding: 10px 16px;
|
||||
border: 1px dashed var(--input-copyable-border);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:not(.input-copyable):not(.wunderbar__input):not(:placeholder-shown):not(:disabled) {
|
||||
|
@ -109,7 +109,6 @@ input {
|
|||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'metropolis-medium';
|
||||
border: 1px solid var(--color-divider);
|
||||
font-size: 0.8em;
|
||||
width: 100%;
|
||||
|
@ -152,8 +151,6 @@ dd {
|
|||
}
|
||||
|
||||
p {
|
||||
font-family: 'metropolis-medium';
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
@ -227,7 +224,6 @@ p {
|
|||
.page__empty {
|
||||
margin-top: 200px;
|
||||
text-align: center;
|
||||
font-family: 'metropolis-medium';
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -262,7 +258,7 @@ p {
|
|||
}
|
||||
|
||||
.credit-amount {
|
||||
font-size: 10px;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -300,7 +296,6 @@ p {
|
|||
color: inherit;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
font-family: 'metropolis-medium';
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -333,10 +328,6 @@ p {
|
|||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.busy-indicator {
|
||||
font-family: 'metropolis-medium';
|
||||
}
|
||||
|
||||
.busy-indicator__loader {
|
||||
background: url('../../../static/img/busy.gif') no-repeat center center;
|
||||
display: inline-block;
|
||||
|
@ -355,7 +346,6 @@ p {
|
|||
|
||||
.help {
|
||||
font-size: 12px;
|
||||
font-family: 'metropolis-medium';
|
||||
color: var(--color-help);
|
||||
}
|
||||
|
||||
|
@ -364,7 +354,6 @@ p {
|
|||
}
|
||||
|
||||
.meta {
|
||||
font-family: 'metropolis-medium';
|
||||
font-size: 0.8em;
|
||||
color: var(--color-meta-light);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ $large-breakpoint: 1921px;
|
|||
--color-search-placeholder: var(--color-placeholder);
|
||||
--color-credit-free: var(--color-dark-blue);
|
||||
--color-credit-price: var(--card-text-color);
|
||||
--color-text-black: #444;
|
||||
--color-text-white: #efefef;
|
||||
|
||||
/* Shadows */
|
||||
--box-shadow-layer: transparent; // 0 2px 4px rgba(0,0,0,0.25);
|
||||
|
@ -60,8 +62,8 @@ $large-breakpoint: 1921px;
|
|||
--box-shadow-header: 0px 6px 20px 1px rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* Text */
|
||||
--text-color: var(--color-black);
|
||||
--text-color-inverse: var(--color-white);
|
||||
--text-color: var(--color-text-black);
|
||||
--text-color-inverse: var(--color-text-white);
|
||||
--text-help-color: var(--color-help);
|
||||
--text-max-width: 660px;
|
||||
--text-link-padding: 4px;
|
||||
|
|
|
@ -29,3 +29,6 @@
|
|||
@import 'component/_toggle.scss';
|
||||
@import 'component/_search.scss';
|
||||
@import 'component/_dat-gui.scss';
|
||||
@import 'component/_item-list.scss';
|
||||
@import 'component/_time.scss';
|
||||
@import 'component/_icon.scss';
|
||||
|
|
|
@ -19,7 +19,6 @@ button:disabled {
|
|||
fill: currentColor; // for proper icon color
|
||||
font-size: 12px;
|
||||
transition: all var(--animation-duration) var(--animation-style);
|
||||
font-family: 'metropolis-medium';
|
||||
|
||||
&:not(:disabled) {
|
||||
box-shadow: var(--box-shadow-button);
|
||||
|
@ -161,6 +160,7 @@ button:disabled {
|
|||
|
||||
.btn--uri-indicator {
|
||||
transition: color var(--animation-duration) var(--animation-style);
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: var(--btn-color-inverse);
|
||||
|
@ -172,7 +172,6 @@ button:disabled {
|
|||
}
|
||||
|
||||
.btn.btn--header-balance {
|
||||
font-family: 'metropolis-medium';
|
||||
font-size: 14px;
|
||||
color: var(--header-primary-color);
|
||||
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: var(--card-max-width);
|
||||
}
|
||||
white-space: normal;
|
||||
|
||||
.card > h1 {
|
||||
word-wrap: break-word;
|
||||
.card__media {
|
||||
padding-top: var(--video-aspect-ratio);
|
||||
}
|
||||
|
||||
// Text that is shown if a piece of content has no thumbnail
|
||||
// We need to do this because the thumbnail uses padding-top: var(--video-aspect-ratio); for dynamic height
|
||||
// this lets the text sit in the middle instead of the bottom
|
||||
.card__media-text {
|
||||
margin-top: calc(var(--video-aspect-ratio) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
.card--section {
|
||||
|
@ -20,25 +28,25 @@
|
|||
}
|
||||
|
||||
.card--small {
|
||||
white-space: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card__media {
|
||||
padding-top: var(--video-aspect-ratio);
|
||||
}
|
||||
.card__media {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: var(--color-placeholder);
|
||||
}
|
||||
|
||||
.card__media-text {
|
||||
// for the weird padding required for dynamic height
|
||||
// this lets the text sit in the middle instead of the bottom
|
||||
margin-top: calc(var(--video-aspect-ratio) * -1);
|
||||
}
|
||||
.card__media--no-img {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 12px;
|
||||
|
||||
@media only screen and (min-width: $medium-breakpoint) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.card__media--nsfw {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.card--link {
|
||||
|
@ -66,31 +74,29 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card__media {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: var(--color-placeholder);
|
||||
}
|
||||
|
||||
.card__media--no-img {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card__media--nsfw {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.card__title-identity {
|
||||
.card__title {
|
||||
font-size: 1.5em;
|
||||
color: var(--text-color);
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
|
||||
@media (min-width: $large-breakpoint) {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
}
|
||||
|
||||
.card__title-identity--file {
|
||||
.card__title--file-card {
|
||||
// FileCard is slightly different where the title is only slightly bigger than the subtitle
|
||||
// Slightly bigger than 2 lines for consistent channel placement
|
||||
font-size: 1.1em;
|
||||
height: 4em;
|
||||
|
||||
@media only screen and (min-width: $large-breakpoint) {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.card__title__space-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon {
|
||||
|
@ -104,73 +110,26 @@
|
|||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
font-size: 18px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card__title--small {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
|
||||
@media only screen and (min-width: $medium-breakpoint) {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.card__title--x-small {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.card__title--large {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.card__title--file {
|
||||
font-family: 'metropolis-bold';
|
||||
font-size: 28px;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card__title--file-card {
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
// height is the same height that two lines of title fill
|
||||
// doing this so content below the title is inline accross the row
|
||||
height: 30px;
|
||||
|
||||
@media only screen and (min-width: $medium-breakpoint) {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-family: 'metropolis-medium';
|
||||
color: var(--card-text-color);
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
|
||||
@media (min-width: $large-breakpoint) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtitle--x-small {
|
||||
font-size: 12px;
|
||||
.card__subtext-title {
|
||||
font-size: 1.1em;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: $spacing-vertical * 3/2;
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtitle--large {
|
||||
font-size: 18px;
|
||||
padding-bottom: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
.card__subtitle-price {
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
.card__title--small + .card__subtitle,
|
||||
.card__title--x-small + .card__subtitle {
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
.card__subtext {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.card__meta {
|
||||
|
@ -185,11 +144,6 @@
|
|||
align-items: center;
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
color: var(--card-text-color);
|
||||
|
||||
.icon + .icon,
|
||||
.credit-amount + .icon {
|
||||
margin-left: $spacing-vertical * 1/3;
|
||||
}
|
||||
}
|
||||
|
||||
// .card-media__internal__links should always be inside a card
|
||||
|
@ -201,58 +155,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card--small {
|
||||
.card-media__internal-links {
|
||||
top: $spacing-vertical * 1/3;
|
||||
right: $spacing-vertical * 1/3;
|
||||
}
|
||||
}
|
||||
|
||||
// Channel info with buttons on the right side
|
||||
.card__channel-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-width 0 0;
|
||||
}
|
||||
|
||||
.card__channel-info--large {
|
||||
padding-top: 0;
|
||||
padding-bottom: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
.card__content {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
.card__content--extra-padding {
|
||||
margin-top: $spacing-vertical * 3/2;
|
||||
}
|
||||
|
||||
.card__subtext-title {
|
||||
color: var(--text-color);
|
||||
font-size: calc(var(--font-size-subtext-multiple) * 1.2em);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: $spacing-vertical * 3/2;
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtext {
|
||||
font-size: calc(var(--font-size-subtext-multiple) * 1em);
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
word-break: break-word;
|
||||
font-family: 'metropolis-medium';
|
||||
}
|
||||
|
||||
.card__subtext--small {
|
||||
font-size: calc(var(--font-size-subtext-multiple) * 0.8em);
|
||||
}
|
||||
|
||||
.card__subtext--large {
|
||||
font-size: calc(var(--font-size-subtext-multiple) * 0.9em);
|
||||
}
|
||||
|
||||
.card__actions {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
display: flex;
|
||||
|
@ -335,6 +241,7 @@
|
|||
}
|
||||
|
||||
.card-row__title {
|
||||
font-family: 'metropolis-semibold';
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
|
@ -462,43 +369,3 @@
|
|||
padding: $spacing-vertical * 1/3;
|
||||
margin: $spacing-vertical * 1/3 0;
|
||||
}
|
||||
|
||||
.card__media--autothumb {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.card__media {
|
||||
&.card__media--autothumb.purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
&.card__media--autothumb.red {
|
||||
background-color: #e53935;
|
||||
}
|
||||
&.card__media--autothumb.pink {
|
||||
background-color: #e91e63;
|
||||
}
|
||||
&.card__media--autothumb.indigo {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
&.card__media--autothumb.blue {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
&.card__media--autothumb.light-blue {
|
||||
background-color: #039be5;
|
||||
}
|
||||
&.card__media--autothumb.cyan {
|
||||
background-color: #00acc1;
|
||||
}
|
||||
&.card__media--autothumb.teal {
|
||||
background-color: #009688;
|
||||
}
|
||||
&.card__media--autothumb.green {
|
||||
background-color: #43a047;
|
||||
}
|
||||
&.card__media--autothumb.yellow {
|
||||
background-color: #ffeb3b;
|
||||
}
|
||||
&.card__media--autothumb.orange {
|
||||
background-color: #ffa726;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,3 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.channel-indicator__icon--invalid {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
|
|
@ -95,10 +95,6 @@
|
|||
&.content__empty--nsfw {
|
||||
background-color: var(--color-nsfw);
|
||||
}
|
||||
|
||||
.card__media-text {
|
||||
margin-top: calc(var(--video-aspect-ratio) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
@ -22,25 +22,32 @@
|
|||
|
||||
.file-tile {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
padding-top: $spacing-vertical;
|
||||
|
||||
.card__media {
|
||||
height: var(--file-tile-media-height);
|
||||
flex: 0 0 var(--file-tile-media-width);
|
||||
}
|
||||
}
|
||||
|
||||
.card__subtitle {
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.file-tile--small {
|
||||
font-size: 12px;
|
||||
|
||||
.card__media {
|
||||
height: var(--file-tile-media-height-small);
|
||||
flex: 0 0 var(--file-tile-media-width-small);
|
||||
}
|
||||
}
|
||||
|
||||
.file-tile__channel {
|
||||
padding-right: $spacing-width * 1/4;
|
||||
.file-tile--large {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-tile.file-tile--small {
|
||||
.file-tile__title {
|
||||
}
|
||||
|
||||
.file-tile--small {
|
||||
padding-top: $spacing-vertical * 2/3;
|
||||
|
||||
.card__media {
|
||||
|
@ -49,7 +56,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-tile.file-tile--large {
|
||||
.file-tile--large {
|
||||
.card__media {
|
||||
height: var(--file-tile-media-height-large);
|
||||
flex: 0 0 var(--file-tile-media-width-large);
|
||||
|
|
6
src/renderer/scss/component/_icon.scss
Normal file
6
src/renderer/scss/component/_icon.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Icons with icons directly following should have a margin-right for proper spacing
|
||||
// Same for prices on cards
|
||||
.icon + .icon,
|
||||
.credit-amount + .icon {
|
||||
margin-left: $spacing-vertical * 1/3;
|
||||
}
|
26
src/renderer/scss/component/_item-list.scss
Normal file
26
src/renderer/scss/component/_item-list.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
.item-list {
|
||||
background-color: var(--card-bg);
|
||||
margin-top: $spacing-vertical;
|
||||
}
|
||||
|
||||
.item-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-vertical * 1/3;
|
||||
|
||||
input,
|
||||
.item-list__item--cutoff {
|
||||
margin-right: $spacing-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list__item--selected {
|
||||
background-color: var(--table-item-odd);
|
||||
}
|
||||
|
||||
.item-list__item--cutoff {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
max-width: 350px;
|
||||
}
|
|
@ -39,7 +39,6 @@
|
|||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
margin: $spacing-vertical * 2/3 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
padding: 10px;
|
||||
padding-left: 30px;
|
||||
font-size: 13px;
|
||||
font-family: 'metropolis-medium';
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -54,7 +53,6 @@
|
|||
flex-direction: row;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
font-family: 'metropolis-medium';
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--search-item-border-color);
|
||||
|
@ -75,7 +73,6 @@
|
|||
.wunderbar__suggestion-label--action {
|
||||
margin-left: $spacing-vertical * 1/3;
|
||||
white-space: nowrap;
|
||||
font-family: 'metropolis-medium';
|
||||
font-size: 12px;
|
||||
line-height: 0.1; // to vertically align because the font size is smaller
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ table.table,
|
|||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
|
||||
tr td:first-of-type,
|
||||
tr th:first-of-type {
|
||||
|
@ -107,48 +106,3 @@ table.table--transactions {
|
|||
width: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
table.table--history {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:nth-child(even),
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--table-item-even);
|
||||
|
||||
&.history__selected {
|
||||
color: red;
|
||||
background-color: var(--table-item-odd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
cursor: default;
|
||||
padding: $spacing-vertical * 1/3 0;
|
||||
}
|
||||
|
||||
td:nth-of-type(1) {
|
||||
width: 7.5%;
|
||||
}
|
||||
td:nth-of-type(2) {
|
||||
width: 17.5%;
|
||||
}
|
||||
td:nth-of-type(3) {
|
||||
width: 40%;
|
||||
max-width: 30vw;
|
||||
padding-right: $spacing-vertical * 2/3;
|
||||
}
|
||||
td:nth-of-type(4) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
td:nth-of-type(3),
|
||||
td:nth-of-type(4) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
src/renderer/scss/component/_time.scss
Normal file
9
src/renderer/scss/component/_time.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
// All CSS for date & time ui
|
||||
|
||||
.time {
|
||||
color: var(--color-help);
|
||||
}
|
||||
|
||||
.time--ago {
|
||||
min-width: 160px;
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
--color-credit-free: var(--color-secondary);
|
||||
|
||||
/* Text */
|
||||
--text-color: var(--color-white);
|
||||
--text-color: var(--color-text-white);
|
||||
--text-help-color: var(--color-help);
|
||||
|
||||
/* Form */
|
||||
|
|
Loading…
Reference in a new issue