Merge pull request #111 from lbryio/wunderbar

Wunderbar
This commit is contained in:
Jeremy Kauffman 2017-05-02 10:31:12 -04:00 committed by GitHub
commit 99405578af
36 changed files with 962 additions and 924 deletions

View file

@ -8,6 +8,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
* The UI has been overhauled to use an omnibar and drop the sidebar.
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
* lbry.js now offers a subscription model for wallet balance similar to file info.
* Fixed file info subscribes not being unsubscribed in unmount.

View file

@ -12,10 +12,11 @@ import RewardPage from './page/reward.js';
import WalletPage from './page/wallet.js';
import ShowPage from './page/show.js';
import PublishPage from './page/publish.js';
import SearchPage from './page/search.js';
import DiscoverPage from './page/discover.js';
import DeveloperPage from './page/developer.js';
import lbryuri from './lbryuri.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Drawer from './component/drawer.js';
import Header from './component/header.js';
import {Modal, ExpandableModal} from './component/modal.js';
import {Link} from './component/link.js';
@ -38,6 +39,7 @@ var App = React.createClass({
data: 'Error data',
},
_fullScreenPages: ['watch'],
_storeHistoryOfNextRender: false,
_upgradeDownloadItem: null,
_isMounted: false,
@ -73,15 +75,13 @@ var App = React.createClass({
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
return {
viewingPage: viewingPage,
pageArgs: pageArgs === undefined ? null : pageArgs
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
};
},
getInitialState: function() {
var match, param, val, viewingPage, pageArgs,
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
viewingPage: 'discover',
appUrl: null,
errorInfo: null,
modal: null,
downloadProgress: null,
@ -89,6 +89,8 @@ var App = React.createClass({
});
},
componentWillMount: function() {
window.addEventListener("popstate", this.onHistoryPop);
document.addEventListener('unhandledError', (event) => {
this.alertError(event.detail);
});
@ -105,9 +107,10 @@ var App = React.createClass({
if (target.matches('a[href^="?"]')) {
event.preventDefault();
if (this._isMounted) {
history.pushState({}, document.title, target.getAttribute('href'));
this.registerHistoryPop();
this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
let appUrl = target.getAttribute('href');
this._storeHistoryOfNextRender = true;
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
document.body.scrollTop = 0;
}
}
target = target.parentNode;
@ -125,14 +128,6 @@ var App = React.createClass({
});
}
},
openDrawer: function() {
sessionStorage.setItem('drawerOpen', true);
this.setState({ drawerOpen: true });
},
closeDrawer: function() {
sessionStorage.setItem('drawerOpen', false);
this.setState({ drawerOpen: false });
},
closeModal: function() {
this.setState({
modal: null,
@ -143,12 +138,29 @@ var App = React.createClass({
},
componentWillUnmount: function() {
this._isMounted = false;
window.removeEventListener("popstate", this.onHistoryPop);
},
registerHistoryPop: function() {
window.addEventListener("popstate", () => {
this.setState(this.getViewingPageAndArgs(location.pathname));
onHistoryPop: function() {
this.setState(this.getViewingPageAndArgs(location.search));
},
onSearch: function(term) {
this._storeHistoryOfNextRender = true;
const isShow = term.startsWith('lbry://');
this.setState({
viewingPage: isShow ? "show" : "search",
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
pageArgs: term
});
},
onSubmit: function(uri) {
this._storeHistoryOfNextRender = true;
this.setState({
address: uri,
appUrl: "?show=" + encodeURIComponent(uri),
viewingPage: "show",
pageArgs: uri
})
},
handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
@ -201,12 +213,6 @@ var App = React.createClass({
modal: null,
});
},
onSearch: function(term) {
this.setState({
viewingPage: 'discover',
pageArgs: term
});
},
alertError: function(error) {
var errorInfoList = [];
for (let key of Object.keys(error)) {
@ -220,75 +226,57 @@ var App = React.createClass({
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
});
},
getHeaderLinks: function()
{
switch(this.state.viewingPage)
{
case 'wallet':
case 'send':
case 'receive':
case 'rewards':
return {
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?rewards': 'Rewards',
};
case 'downloaded':
case 'published':
return {
'?downloaded': 'Downloaded',
'?published': 'Published',
};
default:
return null;
}
},
getMainContent: function()
getContentAndAddress: function()
{
switch(this.state.viewingPage)
{
case 'search':
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
case 'settings':
return <SettingsPage />;
return ["Settings", "icon-gear", <SettingsPage />];
case 'help':
return <HelpPage />;
return ["Help", "icon-question", <HelpPage />];
case 'report':
return <ReportPage />;
return ['Report an Issue', 'icon-file', <ReportPage />];
case 'downloaded':
return <FileListDownloaded />;
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
case 'published':
return <FileListPublished />;
return ["Publishes", "icon-folder", <FileListPublished />];
case 'start':
return <StartPage />;
return ["Start", "icon-file", <StartPage />];
case 'rewards':
return <RewardsPage />;
return ["Rewards", "icon-bank", <RewardsPage />];
case 'wallet':
case 'send':
case 'receive':
return <WalletPage viewingPage={this.state.viewingPage} />;
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
case 'show':
return <ShowPage uri={this.state.pageArgs} />;
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
case 'publish':
return <PublishPage />;
return ["Publish", "icon-upload", <PublishPage />];
case 'developer':
return <DeveloperPage />;
return ["Developer", "icon-file", <DeveloperPage />];
case 'discover':
default:
return <DiscoverPage showWelcome={this.state.justRegistered} {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
return ["Home", "icon-home", <DiscoverPage />];
}
},
render: function() {
var mainContent = this.getMainContent(),
headerLinks = this.getHeaderLinks(),
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
lbry.setTitle(address);
if (this._storeHistoryOfNextRender) {
this._storeHistoryOfNextRender = false;
history.pushState({}, document.title, this.state.appUrl);
}
return (
this._fullScreenPages.includes(this.state.viewingPage) ?
mainContent :
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
<Header onOpenDrawer={this.openDrawer} initialQuery={searchQuery} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} />
<div id="window">
<Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
<div id="main-content">
{mainContent}
</div>
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"

View file

@ -1,6 +1,5 @@
import React from 'react';
import lbry from '../lbry.js';
import $clamp from 'clamp-js-main';
//component/icon.js
export let Icon = React.createClass({
@ -19,29 +18,15 @@ export let Icon = React.createClass({
export let TruncatedText = React.createClass({
propTypes: {
lines: React.PropTypes.number,
height: React.PropTypes.string,
auto: React.PropTypes.bool,
lines: React.PropTypes.number
},
getDefaultProps: function() {
return {
lines: null,
height: null,
auto: true,
}
},
componentDidMount: function() {
// Manually round up the line height, because clamp.js doesn't like fractional-pixel line heights.
// Need to work directly on the style object because setting the style prop doesn't update internal styles right away.
this.refs.span.style.lineHeight = Math.ceil(parseFloat(getComputedStyle(this.refs.span).lineHeight)) + 'px';
$clamp(this.refs.span, {
clamp: this.props.lines || this.props.height || 'auto',
});
},
render: function() {
return <span ref="span" className="truncated-text">{this.props.children}</span>;
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
}
});

View file

@ -1,67 +0,0 @@
import lbry from '../lbry.js';
import React from 'react';
import {Link} from './link.js';
var DrawerItem = React.createClass({
getDefaultProps: function() {
return {
subPages: [],
};
},
render: function() {
var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
this.props.subPages.indexOf(this.props.viewingPage) != -1);
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
}
});
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
height: '36px'
};
var Drawer = React.createClass({
_balanceSubscribeId: null,
handleLogoClicked: function(event) {
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
window.location.href = '?developer'
event.preventDefault();
}
},
getInitialState: function() {
return {
balance: 0,
};
},
componentDidMount: function() {
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
this.setState({
balance: balance
});
});
},
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
return (
<nav id="drawer">
<div id="drawer-handle">
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
<a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
</div>
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<DrawerItem href="?wallet" subPages={['send', 'receive', 'rewards']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav>
);
}
});
export default Drawer;

View file

@ -3,7 +3,7 @@ import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.js';
import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js';
import UriIndicator from '../component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/
@ -77,40 +77,32 @@ export let FileTileStream = React.createClass({
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
const primaryUrl = "?show=" + uri;
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card__inner file-tile__row"}>
<div className="span3 file-tile__thumbnail-container">
<a href={'?show=' + uri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + this.props.uri} /></a>
<a href={primaryUrl} className="card__link">
<div className={"card__inner file-tile__row"}>
<div className="card__media"
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
</div>
<div className="span9">
<div className="file-tile__content">
<div className="card__title-primary">
{ !this.props.hidePrice
? <FilePrice uri={this.props.uri} />
: null}
<div className="meta"><a href={'?show=' + this.props.uri}>{uri}</a></div>
<h3>
<a href={'?show=' + uri} title={title}>
<TruncatedText lines={1}>
{title}
</TruncatedText>
</a>
</h3>
<div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
</div>
<div className="card__actions">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
<div className="card__content">
<p className="file-tile__description">
<TruncatedText lines={2}>
<div className="card__content card__subtext">
<TruncatedText lines={3}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</p>
</div>
</div>
</div>
</a>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
@ -227,6 +219,7 @@ export let FileCardStream = React.createClass({
export let FileTile = React.createClass({
_isMounted: false,
_isResolvePending: false,
propTypes: {
uri: React.PropTypes.string.isRequired,
@ -238,11 +231,10 @@ export let FileTile = React.createClass({
claimInfo: null
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
resolve: function(uri) {
this._isResolvePending = true;
lbry.resolve({uri: uri}).then((resolutionInfo) => {
this._isResolvePending = false;
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
// In case of a failed lookup, metadata will be null, in which case the component will never display
@ -252,6 +244,16 @@ export let FileTile = React.createClass({
}
});
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.uri != this.props.uri) {
this.setState(this.getInitialState());
this.resolve(nextProps.uri);
}
},
componentDidMount: function() {
this._isMounted = true;
this.resolve(this.props.uri);
},
componentWillUnmount: function() {
this._isMounted = false;
},
@ -261,6 +263,12 @@ export let FileTile = React.createClass({
return <FileCardStream outpoint={null} metadata={{title: this.props.uri, description: "Loading..."}} contentType={null} hidePrice={true}
hasSignature={false} signatureIsValid={false} uri={this.props.uri} />
}
if (this.props.showEmpty)
{
return this._isResolvePending ?
<BusyMessage message="Loading magic decentralized data" /> :
<div className="empty">{lbryuri.normalize(this.props.uri)} is unclaimed. <Link label="Put something here" href="?publish" /></div>;
}
return null;
}

View file

@ -1,76 +1,198 @@
import React from 'react';
import lbryuri from '../lbryuri.js';
import {Link} from './link.js';
import {Icon} from './common.js';
import {Icon, CreditAmount} from './common.js';
var Header = React.createClass({
_balanceSubscribeId: null,
_isMounted: false,
propTypes: {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
title: "LBRY",
isScrolled: false
balance: 0
};
},
componentWillMount: function() {
new MutationObserver((mutations) => {
this.setState({ title: mutations[0].target.textContent });
}).observe(
document.querySelector('title'),
{ subtree: true, characterData: true, childList: true }
);
},
componentDidMount: function() {
document.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount: function() {
document.removeEventListener('scroll', this.handleScroll);
if (this.userTypingTimer)
{
clearTimeout(this.userTypingTimer);
this._isMounted = true;
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
if (this._isMounted) {
this.setState({balance: balance});
}
},
handleScroll: function() {
this.setState({
isScrolled: document.body.scrollTop > 0
});
},
onQueryChange: function(event) {
if (this.userTypingTimer)
{
clearTimeout(this.userTypingTimer);
componentWillUnmount: function() {
this._isMounted = false;
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
//@TODO: Switch to React.js timing
var searchTerm = event.target.value;
this.userTypingTimer = setTimeout(() => {
this.props.onSearch(searchTerm);
}, 800); // 800ms delay, tweak for faster/slower
},
render: function() {
return (
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }>
<div className="header-top-bar">
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
<h1>{ this.state.title }</h1>
<div className="header-search">
<Icon icon="icon-search" />
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
placeholder="Find movies, music, games, and more"/>
return <header id="header">
<div className="header__item">
<Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
</div>
<div className="header__item">
<Link href="?discover" button="alt button--flat" icon="icon-home" />
</div>
<div className="header__item header__item--wunderbar">
<WunderBar address={this.props.address} icon={this.props.wunderBarIcon}
onSearch={this.props.onSearch} onSubmit={this.props.onSubmit} viewingPage={this.props.viewingPage} />
</div>
<div className="header__item">
<Link href="?wallet" button="text" icon="icon-bank" label={lbry.formatCredits(this.state.balance, 1)} ></Link>
</div>
<div className="header__item">
<Link button="primary button--flat" href="?publish" icon="icon-upload" label="Publish" />
</div>
<div className="header__item">
<Link button="alt button--flat" href="?downloaded" icon="icon-folder" />
</div>
<div className="header__item">
<Link button="alt button--flat" href="?settings" icon="icon-gear" />
</div>
{
this.props.links ?
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
''
}
</header>
);
}
});
var SubHeader = React.createClass({
class WunderBar extends React.PureComponent {
static propTypes = {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
}
constructor(props) {
super(props);
this._userTypingTimer = null;
this._input = null;
this._stateBeforeSearch = null;
this._resetOnNextBlur = true;
this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onReceiveRef = this.onReceiveRef.bind(this);
this.state = {
address: this.props.address,
icon: this.props.icon
};
}
componentWillUnmount() {
if (this.userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
}
onChange(event) {
if (this._userTypingTimer)
{
clearTimeout(this._userTypingTimer);
}
this.setState({ address: event.target.value })
let searchTerm = event.target.value;
this._userTypingTimer = setTimeout(() => {
this._resetOnNextBlur = false;
this.props.onSearch(searchTerm);
}, 800); // 800ms delay, tweak for faster/slower
}
componentWillReceiveProps(nextProps) {
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
this.setState({ address: nextProps.address, icon: nextProps.icon });
}
}
onFocus() {
this._stateBeforeSearch = this.state;
let newState = {
icon: "icon-search",
isActive: true
}
this._focusPending = true;
//below is hacking, improved when we have proper routing
if (!this.state.address.startsWith('lbry://') && this.state.icon !== "icon-search") //onFocus, if they are not on an exact URL or a search page, clear the bar
{
newState.address = '';
}
this.setState(newState);
}
onBlur() {
let commonState = {isActive: false};
if (this._resetOnNextBlur) {
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
this._input.value = this.state.address;
} else {
this._resetOnNextBlur = true;
this._stateBeforeSearch = this.state;
this.setState(commonState);
}
}
componentDidUpdate() {
this._input.value = this.state.address;
if (this._input && this._focusPending) {
this._input.select();
this._focusPending = false;
}
}
onKeyPress(event) {
if (event.charCode == 13 && this._input.value) {
let uri = null,
method = "onSubmit";
this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer);
try {
uri = lbryuri.normalize(this._input.value);
this.setState({ value: uri });
} catch (error) { //then it's not a valid URL, so let's search
uri = this._input.value;
method = "onSearch";
}
this.props[method](uri);
this._input.blur();
}
}
onReceiveRef(ref) {
this._input = ref;
}
render() {
return (
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
<input className="wunderbar__input" type="search" placeholder="Type a LBRY address or search term"
ref={this.onReceiveRef}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.address}
placeholder="Find movies, music, games, and more" />
</div>
);
}
}
export let SubHeader = React.createClass({
render: function() {
var links = [],
let links = [],
viewingUrl = '?' + this.props.viewingPage;
for (let link of Object.keys(this.props.links)) {
@ -81,7 +203,7 @@ var SubHeader = React.createClass({
);
}
return (
<nav className="sub-header">
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
{links}
</nav>
);

View file

@ -41,7 +41,7 @@ export let Link = React.createClass({
content = (
<span {... 'button' in this.props ? {className: 'button__content'} : {}}>
{'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
{<span className="link-label">{this.props.label}</span>}
{this.props.label ? <span className="link-label">{this.props.label}</span> : null}
{'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
</span>
);

View file

@ -9,9 +9,6 @@ var LoadScreen = React.createClass({
details: React.PropTypes.string,
isWarning: React.PropTypes.bool,
},
handleCancelClick: function() {
history.back();
},
getDefaultProps: function() {
return {
isWarning: false,
@ -34,9 +31,6 @@ var LoadScreen = React.createClass({
<BusyMessage message={this.props.message} />
</h3>
{this.props.isWarning ? <Icon icon="icon-warning" /> : null} <span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span>
{window.history.length > 1
? <div><Link label="Cancel" onClick={this.handleCancelClick} className='load-screen__cancel-link button-text' /></div>
: null}
</div>
</div>
);

View file

@ -31,7 +31,6 @@ function savePendingPublish({name, channel_name}) {
return newPendingPublish;
}
/**
* If there is a pending publish with the given name or outpoint, remove it.
* A channel name may also be provided along with name.
@ -132,6 +131,21 @@ lbry.connect = function() {
return lbry._connectPromise;
}
//kill this but still better than document.title =, which this replaced
lbry.setTitle = function(title) {
document.title = title + " - LBRY";
}
//kill this with proper routing
lbry.back = function() {
if (window.history.length > 1) {
window.history.back();
} else {
window.location.href = "?discover";
}
}
lbry.isDaemonAcceptingConnections = function (callback) {
// Returns true/false whether the daemon is at a point it will start returning status
lbry.call('status', {}, () => callback(true), null, () => callback(false))
@ -633,7 +647,7 @@ lbry.resolve = function(params={}) {
if (!params.uri) {
throw "Resolve has hacked cache on top of it that requires a URI"
}
if (params.uri && claimCache[params.uri]) {
if (params.uri && claimCache[params.uri] !== undefined) {
resolve(claimCache[params.uri]);
} else {
lbry.call('resolve', params, function(data) {

View file

@ -7,7 +7,7 @@ const lbryio = {
_accessToken: getLocal('accessToken'),
_authenticationPromise: null,
_user : null,
enabled: true
enabled: false
};
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
@ -150,20 +150,6 @@ lbryio.authenticate = function() {
} else {
setCurrentUser()
}
// if (!lbryio._
//(data) => {
// resolve(data)
// localStorage.setItem('accessToken', ID);
// localStorage.setItem('appId', installation_id);
// this.setState({
// registrationCheckComplete: true,
// justRegistered: true,
// });
//});
// lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => {
// // TODO: deal with case where user exists already with the same app ID, but we have no access token.
// // Possibly merge in to the existing user with the same app ID.
// })
}).catch(reject);
});
}

View file

@ -1,79 +1,18 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import lbryuri from '../lbryuri.js';
import lighthouse from '../lighthouse.js';
import {FileTile, FileTileStream} from '../component/file-tile.js';
import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js';
var fetchResultsStyle = {
color: '#888',
textAlign: 'center',
fontSize: '1.2em'
};
var SearchActive = React.createClass({
render: function() {
return (
<div style={fetchResultsStyle}>
<BusyMessage message="Looking up the Dewey Decimals" />
</div>
);
}
});
var searchNoResultsStyle = {
textAlign: 'center'
}, searchNoResultsMessageStyle = {
fontStyle: 'italic',
marginRight: '5px'
};
var SearchNoResults = React.createClass({
render: function() {
return (
<section style={searchNoResultsStyle}>
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {this.props.query} yet.</span>
<Link label="Be the first" href="?publish" />
</section>
);
}
});
var SearchResults = React.createClass({
render: function() {
var rows = [],
seenNames = {}; //fix this when the search API returns claim IDs
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
const uri = lbryuri.build({
channelName: channel_name,
contentName: name,
claimId: channel_id || claim_id,
});
rows.push(
<FileTileStream key={name} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
);
}
return (
<div>{rows}</div>
);
}
});
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!');
var FeaturedCategory = React.createClass({
let FeaturedCategory = React.createClass({
render: function() {
return (<div className="card-row card-row--small">
{ this.props.category ?
<h3 className="card-row__header">{this.props.category}
{ this.props.category == "community" ?
{ this.props.category.match(/^community/i) ?
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
: '' }</h3>
: '' }
@ -82,7 +21,7 @@ var FeaturedCategory = React.createClass({
}
})
var FeaturedContent = React.createClass({
let DiscoverPage = React.createClass({
getInitialState: function() {
return {
featuredUris: {},
@ -105,7 +44,7 @@ var FeaturedContent = React.createClass({
});
},
render: function() {
return (
return <main>{
this.state.failed ?
<div className="empty">Failed to load landing content.</div> :
<div>
@ -117,89 +56,7 @@ var FeaturedContent = React.createClass({
})
}
</div>
);
}
});
var DiscoverPage = React.createClass({
userTypingTimer: null,
propTypes: {
showWelcome: React.PropTypes.bool.isRequired,
},
componentDidUpdate: function() {
if (this.props.query != this.state.query)
{
this.handleSearchChanged(this.props.query);
}
},
getDefaultProps: function() {
return {
showWelcome: false,
}
},
componentWillReceiveProps: function(nextProps, nextState) {
if (nextProps.query != nextState.query)
{
this.handleSearchChanged(nextProps.query);
}
},
handleSearchChanged: function(query) {
this.setState({
searching: true,
query: query,
});
lighthouse.search(query).then(this.searchCallback);
},
handleWelcomeDone: function() {
this.setState({
welcomeComplete: true,
});
},
componentWillMount: function() {
document.title = "Discover";
if (this.props.query) {
// Rendering with a query already typed
this.handleSearchChanged(this.props.query);
}
},
getInitialState: function() {
return {
welcomeComplete: false,
results: [],
query: this.props.query,
searching: ('query' in this.props) && (this.props.query.length > 0)
};
},
searchCallback: function(results) {
if (this.state.searching) //could have canceled while results were pending, in which case nothing to do
{
this.setState({
results: results,
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
}
},
render: function() {
return (
<main>
{ this.state.searching ? <SearchActive /> : null }
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null }
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null }
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
</main>
);
}</main>;
}
});

View file

@ -3,12 +3,22 @@ import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {FormField} from '../component/form.js';
import {SubHeader} from '../component/header.js';
import {FileTileStream} from '../component/file-tile.js';
import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import {BusyMessage, Thumbnail} from '../component/common.js';
export let FileListNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?downloaded': 'Downloaded',
'?published': 'Published',
}} />;
}
});
export let FileListDownloaded = React.createClass({
_isMounted: false,
@ -19,7 +29,6 @@ export let FileListDownloaded = React.createClass({
},
componentDidMount: function() {
this._isMounted = true;
document.title = "Downloaded Files";
lbry.claim_list_mine().then((myClaimInfos) => {
if (!this._isMounted) { return; }
@ -38,26 +47,21 @@ export let FileListDownloaded = React.createClass({
this._isMounted = false;
},
render: function() {
let content = "";
if (this.state.fileInfos === null) {
return (
<main className="page">
<BusyMessage message="Loading" />
</main>
);
content = <BusyMessage message="Loading" />;
} else if (!this.state.fileInfos.length) {
return (
<main className="page">
<span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
</main>
);
content = <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>;
} else {
content = <FileList fileInfos={this.state.fileInfos} hidePrices={true} />;
}
return (
<main className="page">
<FileList fileInfos={this.state.fileInfos} hidePrices={true} />
<main className="main--single-column">
<FileListNav viewingPage="downloaded" />
{content}
</main>
);
}
}
});
export let FileListPublished = React.createClass({
@ -79,12 +83,11 @@ export let FileListPublished = React.createClass({
else {
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
}
});
}, () => {});
},
componentDidMount: function () {
this._isMounted = true;
this._requestPublishReward();
document.title = "Published Files";
lbry.claim_list_mine().then((claimInfos) => {
if (!this._isMounted) { return; }
@ -103,28 +106,23 @@ export let FileListPublished = React.createClass({
this._isMounted = false;
},
render: function () {
let content = null;
if (this.state.fileInfos === null) {
return (
<main className="page">
<BusyMessage message="Loading" />
</main>
);
content = <BusyMessage message="Loading" />;
}
else if (!this.state.fileInfos.length) {
return (
<main className="page">
<span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
</main>
);
content = <span>You haven't published anything to LBRY yet. Try <Link href="?publish" label="publishing" />!</span>;
}
else {
content = <FileList fileInfos={this.state.fileInfos} />;
}
return (
<main className="page">
<FileList fileInfos={this.state.fileInfos} />
<main className="main--single-column">
<FileListNav viewingPage="published" />
{content}
</main>
);
}
}
});
export let FileList = React.createClass({

View file

@ -3,6 +3,7 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import {SettingsNav} from './settings.js';
import {version as uiVersion} from 'json!../../package.json';
var HelpPage = React.createClass({
@ -24,9 +25,6 @@ var HelpPage = React.createClass({
});
});
},
componentDidMount: function() {
document.title = "Help";
},
render: function() {
let ver, osName, platform, newVerLink;
if (this.state.versionInfo) {
@ -49,7 +47,8 @@ var HelpPage = React.createClass({
}
return (
<main className="page">
<main className="main--single-column">
<SettingsNav viewingPage="help" />
<section className="card">
<div className="card__title-primary">
<h3>Read the FAQ</h3>

View file

@ -148,7 +148,7 @@ var PublishPage = React.createClass({
});
},
handlePublishStartedConfirmed: function() {
window.location = "?published";
window.location.href = "?published";
},
handlePublishError: function(error) {
this.setState({
@ -348,9 +348,6 @@ var PublishPage = React.createClass({
componentWillMount: function() {
this._updateChannelList();
},
componentDidMount: function() {
document.title = "Publish";
},
componentDidUpdate: function() {
},
onFileChange: function() {
@ -387,7 +384,7 @@ var PublishPage = React.createClass({
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
return (
<main ref="page">
<main className="main--single-column">
<form onSubmit={this.handleSubmit}>
<section className="card">
<div className="card__title-primary">
@ -551,7 +548,7 @@ var PublishPage = React.createClass({
<div className="card-series-submit">
<Link button="primary" label={!this.state.submitting ? 'Publish' : 'Publishing...'} onClick={this.handleSubmit} disabled={this.state.submitting} />
<Link button="cancel" onClick={window.history.back} label="Cancel" />
<Link button="cancel" onClick={lbry.back} label="Cancel" />
<input type="submit" className="hidden" />
</div>
</form>

View file

@ -18,9 +18,6 @@ var ReportPage = React.createClass({
this._messageArea.value = '';
}
},
componentDidMount: function() {
document.title = "Report an Issue";
},
closeModal: function() {
this.setState({
modal: null,
@ -34,7 +31,7 @@ var ReportPage = React.createClass({
},
render: function() {
return (
<main className="page">
<main className="main--single-column">
<section className="card">
<h3>Report an Issue</h3>
<p>Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!</p>

View file

@ -4,6 +4,7 @@ import lbryio from '../lbryio.js';
import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js';
import {WalletNav} from './wallet.js';
import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({
@ -56,14 +57,15 @@ var RewardsPage = React.createClass({
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<main className="main--single-column">
<WalletNav viewingPage="rewards"/>
<div>
{!this.state.userRewards
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
})}
</form>
</div>
</main>
);
}

165
ui/js/page/search.js Normal file
View file

@ -0,0 +1,165 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import lbryuri from '../lbryuri.js';
import lighthouse from '../lighthouse.js';
import {FileTile, FileTileStream} from '../component/file-tile.js';
import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js';
var SearchNoResults = React.createClass({
render: function() {
return <section>
<span className="empty">
No one has checked anything in for {this.props.query} yet.
<Link label="Be the first" href="?publish" />
</span>
</section>;
}
});
var SearchResultList = React.createClass({
render: function() {
var rows = [],
seenNames = {}; //fix this when the search API returns claim IDs
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
const uri = lbryuri.build({
channelName: channel_name,
contentName: name,
claimId: channel_id || claim_id,
});
rows.push(
<FileTileStream key={uri} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
);
}
return (
<div>{rows}</div>
);
}
});
let SearchResults = React.createClass({
propTypes: {
query: React.PropTypes.string.isRequired
},
_isMounted: false,
search: function(term) {
lighthouse.search(term).then(this.searchCallback);
if (!this.state.searching) {
this.setState({ searching: true })
}
},
componentWillMount: function () {
this._isMounted = true;
this.search(this.props.query);
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.query != this.props.query) {
this.search(nextProps.query);
}
},
componentWillUnmount: function () {
this._isMounted = false;
},
getInitialState: function () {
return {
results: [],
searching: true
};
},
searchCallback: function (results) {
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
{
this.setState({
results: results,
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
}
},
render: function () {
return this.state.searching ?
<BusyMessage message="Looking up the Dewey Decimals" /> :
(this.state.results && this.state.results.length ?
<SearchResultList results={this.state.results} /> :
<SearchNoResults query={this.props.query} />);
}
});
let SearchPage = React.createClass({
_isMounted: false,
propTypes: {
query: React.PropTypes.string.isRequired
},
isValidUri: function(query) {
try {
lbryuri.parse(query);
return true;
} catch (e) {
return false;
}
},
componentWillMount: function() {
this._isMounted = true;
lighthouse.search(this.props.query).then(this.searchCallback);
},
componentWillUnmount: function() {
this._isMounted = false;
},
getInitialState: function() {
return {
results: [],
searching: true
};
},
searchCallback: function(results) {
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
{
this.setState({
results: results,
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
}
},
render: function() {
return (
<main className="main--single-column">
{ this.isValidUri(this.props.query) ?
<section className="section-spaced">
<h3 className="card-row__header">
Exact URL
<ToolTip label="?" body="This is the resolution of a LBRY URL and not controlled by LBRY Inc." className="tooltip--header"/>
</h3>
<FileTile uri={this.props.query} showEmpty={true} />
</section> : '' }
<section className="section-spaced">
<h3 className="card-row__header">
Search Results for {this.props.query}
<ToolTip label="?" body="These search results are provided by LBRY Inc." className="tooltip--header"/>
</h3>
<SearchResults query={this.props.query} />
</section>
</main>
);
}
});
export default SearchPage;

View file

@ -1,7 +1,17 @@
import React from 'react';
import {FormField, FormRow} from '../component/form.js';
import {SubHeader} from '../component/header.js';
import lbry from '../lbry.js';
export let SettingsNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?settings': 'Settings',
'?help' : 'Help'
}} />;
}
});
var SettingsPage = React.createClass({
_onSettingSaveSuccess: function() {
// This is bad.
@ -56,9 +66,6 @@ var SettingsPage = React.createClass({
showUnavailable: lbry.getClientSetting('showUnavailable'),
}
},
componentDidMount: function() {
document.title = "Settings";
},
componentWillMount: function() {
lbry.getDaemonSettings((settings) => {
this.setState({
@ -92,7 +99,8 @@ var SettingsPage = React.createClass({
</section>
*/
return (
<main>
<main className="main--single-column">
<SettingsNav viewingPage="settings" />
<section className="card">
<div className="card__content">
<h3>Download Directory</h3>

View file

@ -16,8 +16,11 @@ var FormatItem = React.createClass({
outpoint: React.PropTypes.string,
},
render: function() {
const {thumbnail, author, title, description, language, license} = this.props.metadata;
const mediaType = lbry.getMediaType(this.props.contentType);
const {author, language, license} = this.props.metadata;
if (!this.props.contentType && [author, language, license].filter((val) => {return !!val; }).length === 0) {
return null;
}
return (
<table className="table-standard">
@ -40,92 +43,107 @@ var FormatItem = React.createClass({
}
});
let ShowPage = React.createClass({
_uri: null,
let ChannelPage = React.createClass({
render: function() {
return <main className="main--single-column">
<section className="card">
<div className="card__inner">
<div className="card__title-identity"><h1>{this.props.title}</h1></div>
</div>
<div className="card__content">
<p>
This channel page is a stub.
</p>
</div>
</section>
</main>
}
});
let FilePage = React.createClass({
_isMounted: false,
propTypes: {
uri: React.PropTypes.string,
},
getInitialState: function() {
return {
metadata: null,
contentType: null,
hasSignature: false,
signatureIsValid: false,
cost: null,
costIncludesData: null,
uriLookupComplete: null,
isDownloaded: null,
};
},
componentWillUnmount: function() {
this._isMounted = false;
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.outpoint != this.props.outpoint || nextProps.uri != this.props.uri) {
this.loadCostAndFileState(nextProps.uri, nextProps.outpoint);
}
},
componentWillMount: function() {
this._uri = lbryuri.normalize(this.props.uri);
document.title = this._uri;
this._isMounted = true;
this.loadCostAndFileState(this.props.uri, this.props.outpoint);
},
lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
const outpoint = txid + ':' + nout;
lbry.file_list({outpoint}).then((fileInfo) => {
loadCostAndFileState: function(uri, outpoint) {
lbry.file_list({outpoint: outpoint}).then((fileInfo) => {
if (this._isMounted) {
this.setState({
isDownloaded: fileInfo.length > 0,
});
}
});
this.setState({
outpoint: outpoint,
metadata: metadata,
hasSignature: has_signature,
signatureIsValid: signature_is_valid,
contentType: contentType,
uriLookupComplete: true,
});
});
lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
lbry.getCostInfo(uri).then(({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
costIncludesData: includesData,
});
}
});
},
render: function() {
const metadata = this.state.metadata;
const title = metadata ? this.state.metadata.title : this._uri;
const metadata = this.props.metadata,
title = metadata ? this.props.metadata.title : this.props.uri,
uriIndicator = <UriIndicator uri={this.props.uri} hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />;
return (
<main className="constrained-page">
<main className="main--single-column">
<section className="show-page-media">
{ this.state.contentType && this.state.contentType.startsWith('video/') ?
<Video className="video-embedded" uri={this._uri} metadata={metadata} outpoint={this.state.outpoint} /> :
{ this.props.contentType && this.props.contentType.startsWith('video/') ?
<Video className="video-embedded" uri={this.props.uri} metadata={metadata} outpoint={this.props.outpoint} /> :
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
</section>
<section className="card">
<div className="card__inner">
<div className="card__title-identity">
{this.state.isDownloaded === false
? <span style={{float: "right"}}><FilePrice uri={this._uri} metadata={this.state.metadata} /></span>
? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span>
: null}
<h1>{title}</h1>
{ this.state.uriLookupComplete ?
<div>
<div className="card__subtitle">
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />
{ this.props.channelUri ?
<Link href={"?show=" + this.props.channelUri }>{uriIndicator}</Link> :
uriIndicator}
</div>
<div className="card__actions">
<FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
</div> : '' }
</div>
{ this.state.uriLookupComplete ?
<div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata.description}
</div>
</div>
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
</div>
{ metadata ?
<div className="card__content">
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData} />
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.state.costIncludesData} />
</div> : '' }
<div className="card__content">
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
@ -136,4 +154,131 @@ let ShowPage = React.createClass({
}
});
let ShowPage = React.createClass({
_uri: null,
_isMounted: false,
propTypes: {
uri: React.PropTypes.string,
},
getInitialState: function() {
return {
outpoint: null,
metadata: null,
contentType: null,
hasSignature: false,
claimType: null,
signatureIsValid: false,
cost: null,
costIncludesData: null,
uriLookupComplete: null,
isFailed: false,
};
},
componentWillUnmount: function() {
this._isMounted = false;
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.uri != this.props.uri) {
this.setState(this.getInitialState());
this.loadUri(nextProps.uri);
}
},
componentWillMount: function() {
this._isMounted = true;
this.loadUri(this.props.uri);
},
loadUri: function(uri) {
this._uri = lbryuri.normalize(uri);
lbry.resolve({uri: this._uri}).then((resolveData) => {
const isChannel = resolveData && resolveData.claims_in_channel;
if (!this._isMounted) {
return;
}
if (resolveData) {
let newState = { uriLookupComplete: true }
if (!isChannel) {
let {claim: {txid: txid, nout: nout, has_signature: has_signature, signature_is_valid: signature_is_valid, value: {stream: {metadata: metadata, source: {contentType: contentType}}}}} = resolveData;
Object.assign(newState, {
claimType: "file",
metadata: metadata,
outpoint: txid + ':' + nout,
hasSignature: has_signature,
signatureIsValid: signature_is_valid,
contentType: contentType
});
lbry.setTitle(metadata.title ? metadata.title : this._uri)
} else {
let {certificate: {txid: txid, nout: nout, has_signature: has_signature}} = resolveData;
Object.assign(newState, {
claimType: "channel",
outpoint: txid + ':' + nout,
txid: txid,
metadata: {
title:resolveData.certificate.name
}
});
}
this.setState(newState);
} else {
this.setState(Object.assign({}, this.getInitialState(), {
uriLookupComplete: true,
isFailed: true
}));
}
});
},
render: function() {
const metadata = this.state.metadata,
title = metadata ? this.state.metadata.title : this._uri;
let innerContent = "";
if (!this.state.uriLookupComplete || this.state.isFailed) {
innerContent = <section className="card">
<div className="card__inner">
<div className="card__title-identity"><h1>{title}</h1></div>
</div>
<div className="card__content">
{ this.state.uriLookupComplete ?
<p>This location is not yet in use. { ' ' }<Link href="?publish" label="Put something here" />.</p> :
<BusyMessage message="Loading magic decentralized data..." />
}
</div>
</section>;
} else if (this.state.claimType == "channel") {
innerContent = <ChannelPage title={this._uri} />
} else {
let channelUriObj = lbryuri.parse(this._uri)
delete channelUriObj.path;
delete channelUriObj.contentName;
const channelUri = this.state.signatureIsValid && this.state.hasSignature && channelUriObj.isChannel ? lbryuri.build(channelUriObj, false) : null;
innerContent = <FilePage
uri={this._uri}
channelUri={channelUri}
outpoint={this.state.outpoint}
metadata={metadata}
contentType={this.state.contentType}
hasSignature={this.state.hasSignature}
signatureIsValid={this.state.signatureIsValid}
/>;
}
return <main className="main--single-column">{innerContent}</main>;
}
});
export default ShowPage;

View file

@ -5,12 +5,9 @@ var StartPage = React.createClass({
componentWillMount: function() {
lbry.stop();
},
componentDidMount: function() {
document.title = "LBRY is Closed";
},
render: function() {
return (
<main className="page">
<main className="main--single-column">
<h3>LBRY is Closed</h3>
<Link href="lbry://lbry" label="Click here to start LBRY" />
</main>

View file

@ -2,6 +2,7 @@ import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import Modal from '../component/modal.js';
import {SubHeader} from '../component/header.js';
import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
@ -263,6 +264,16 @@ var TransactionList = React.createClass({
}
});
export let WalletNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?rewards': 'Rewards'
}} />;
}
});
var WalletPage = React.createClass({
_balanceSubscribeId: null,
@ -270,9 +281,6 @@ var WalletPage = React.createClass({
propTypes: {
viewingPage: React.PropTypes.string,
},
componentDidMount: function() {
document.title = "My Wallet";
},
/*
Below should be refactored so that balance is shared all of wallet page. Or even broader?
What is the proper React pattern for sharing a global state like balance?
@ -296,7 +304,8 @@ var WalletPage = React.createClass({
},
render: function() {
return (
<main className="page">
<main className="main--single-column">
<WalletNav viewingPage={this.props.viewingPage} />
<section className="card">
<div className="card__title-primary">
<h3>Balance</h3>

View file

@ -2,9 +2,9 @@
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet.
*/
export function getLocal(key) {
export function getLocal(key, fallback=undefined) {
const itemRaw = localStorage.getItem(key);
return itemRaw === null ? undefined : JSON.parse(itemRaw);
return itemRaw === null ? fallback : JSON.parse(itemRaw);
}
/**

View file

@ -21,7 +21,6 @@
"babel-cli": "^6.11.4",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"clamp-js-main": "^0.11.1",
"mediaelement": "^2.23.4",
"node-sass": "^3.8.0",
"rc-progress": "^2.0.6",

View file

@ -1,220 +0,0 @@
@import "global";
html
{
height: 100%;
font-size: $font-size;
}
body
{
font-family: 'Source Sans Pro', sans-serif;
line-height: $font-line-height;
}
$drawer-width: 220px;
#drawer
{
width: $drawer-width;
position: fixed;
min-height: 100vh;
left: 0;
top: 0;
background: $color-bg;
z-index: 3;
.drawer-item
{
display: block;
padding: $spacing-vertical / 2;
font-size: 1.2em;
height: $spacing-vertical * 1.5;
.icon
{
margin-right: 6px;
}
.link-label
{
line-height: $spacing-vertical * 1.5;
}
.badge
{
float: right;
margin-top: $spacing-vertical * 0.25 - 2;
background: $color-money;
}
}
.drawer-item-selected
{
background: $color-canvas;
color: $color-primary;
}
}
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount--indicator
{
font-weight: bold;
color: $color-money;
}
#drawer-handle
{
padding: $spacing-vertical / 2;
max-height: $height-header - $spacing-vertical;
text-align: center;
}
#window
{
position: relative; /*window has it's own z-index inside of it*/
z-index: 1;
}
#window.drawer-closed
{
#drawer { display: none }
}
#window.drawer-open
{
#main-content { margin-left: $drawer-width; }
.open-drawer-link { display: none }
#header { padding-left: $drawer-width + $spacing-vertical / 2; }
}
#header
{
background: $color-primary;
color: white;
&.header-no-subnav {
height: $height-header;
}
&.header-with-subnav {
height: $height-header * 2;
}
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 2;
box-sizing: border-box;
h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; }
&.header-scrolled
{
box-shadow: $default-box-shadow;
}
}
.header-top-bar
{
padding: $spacing-vertical / 2;
}
.header-search
{
margin-left: 60px;
$padding-adjust: 36px;
text-align: center;
.icon {
position: absolute;
top: $spacing-vertical * 1.5 / 2 + 2px; //hacked
margin-left: -$padding-adjust + 14px; //hacked
}
input[type="search"] {
position: relative;
left: -$padding-adjust;
background: rgba(255, 255, 255, 0.3);
color: white;
width: 400px;
height: $spacing-vertical * 1.5;
line-height: $spacing-vertical * 1.5;
padding-left: $padding-adjust + 3;
padding-right: 3px;
@include border-radius(2px);
@include placeholder-color(#e8e8e8);
&:focus {
box-shadow: $focus-box-shadow;
}
}
}
nav.sub-header
{
background: $color-primary;
text-transform: uppercase;
padding: $spacing-vertical / 2;
> a
{
$sub-header-selected-underline-height: 2px;
display: inline-block;
margin: 0 15px;
padding: 0 5px;
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
color: #e8e8e8;
&:first-child
{
margin-left: 0;
}
&:last-child
{
margin-right: 0;
}
&.sub-header-selected
{
border-bottom: $sub-header-selected-underline-height solid #fff;
color: #fff;
}
&:hover
{
color: #fff;
}
}
}
#main-content
{
background: $color-canvas;
&.no-sub-nav
{
min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $height-header; }
}
&.with-sub-nav
{
min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $height-header * 2; }
}
main
{
padding: $spacing-vertical;
&.constrained-page
{
max-width: $width-page-constrained;
margin-left: auto;
margin-right: auto;
}
}
}
$header-icon-size: 1.5em;
.open-drawer-link, .close-drawer-link
{
display: inline-block;
font-size: $header-icon-size;
padding: 2px 6px 0 6px;
float: left;
}
.close-lbry-link
{
font-size: $header-icon-size;
float: right;
padding: 0 6px 0 18px;
}
.full-screen
{
width: 100%;
height: 100%;
}

View file

@ -34,8 +34,8 @@ $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5;
$height-video-embedded: $width-page-constrained * 9 / 16;
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
$box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
$transition-standard: .225s ease;
@ -161,3 +161,34 @@ $blur-intensity: 8px;
height:1px;
overflow:hidden;
}
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
.icon
{
&:first-child {
padding-right: 5px;
}
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover
{
opacity: $hover-opacity;
transition: opacity $transition-standard;
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
}

View file

@ -1,87 +0,0 @@
@import "global";
$gutter_fluid: 4;
[class*="span"] {
min-height: 1px;
max-width: 100%;
}
.span12 { width: 100%; }
.span11 { width: 91.666%; }
.span10 { width: 83.333%; }
.span9 { width: 75%; }
.span8 { width: 66.666%; }
.span7 { width: 58.333%; }
.span6 { width: 50%; }
.span5 { width: 41.666%; }
.span4 { width: 33.333%; }
.span3 { width: 25%; }
.span2 { width: 16.666%; }
.span1 { width: 8.333%; }
.row-fluid {
width: 100%;
> [class*="span"] {
float: left;
width: 100%;
margin-left: 1% * $gutter_fluid;
&:first-child
{
margin-left: 0;
}
}
$column_width: (100% - $gutter_fluid * 11) / 12;
> .span12 { width: $column_width * 12 + $gutter_fluid * 11; }
> .span11 { width: $column_width * 11 + $gutter_fluid * 10; }
> .span10 { width: $column_width * 10 + $gutter_fluid * 9; }
> .span9 { width: $column_width * 9 + $gutter_fluid * 8; }
> .span8 { width: $column_width * 8 + $gutter_fluid * 7; }
> .span7 { width: $column_width * 7 + $gutter_fluid * 6; }
> .span6 { width: $column_width * 6 + $gutter_fluid * 5; }
> .span5 { width: $column_width * 5 + $gutter_fluid * 4; }
> .span4 { width: $column_width * 4 + $gutter_fluid * 3; }
> .span3 { width: $column_width * 3 + $gutter_fluid * 2; }
> .span2 { width: $column_width * 2 + $gutter_fluid * 1; }
> .span1 { width: $column_width; }
}
.tile-fluid {
width: 100%;
> [class*="span"] {
float: left;
}
}
.column-fluid {
@include display-flex();
flex-wrap: wrap;
> [class*="span"] {
@include display-flex();
@include flex(1 0 auto);
overflow: hidden;
justify-content: center;
}
}
.row-fluid, .tile-fluid {
@include clearfix();
}
@media (max-width: $mobile-width-threshold) {
.row-fluid, .tile-fluid, .column-fluid {
width: 100%;
}
.pull-left, .pull-right
{
float: none;
}
[class*="span"] {
float: none !important;
width: 100% !important;
margin-left: 0 !important;
display: block !important;
}
}

View file

@ -1,35 +1,51 @@
@import "global";
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
html
{
height: 100%;
font-size: $font-size;
}
body
{
font-family: 'Source Sans Pro', sans-serif;
line-height: $font-line-height;
}
.icon
#window
{
min-height: 100vh;
background: $color-canvas;
}
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount--indicator
{
font-weight: bold;
color: $color-money;
}
#main-content
{
padding: $spacing-vertical;
margin-top: $height-header;
display: flex;
flex-direction: column;
main {
margin-left: auto;
margin-right: auto;
max-width: 100%;
}
main.main--single-column
{
&:first-child {
padding-right: 5px;
width: $width-page-constrained;
}
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover
{
opacity: $hover-opacity;
transition: opacity $transition-standard;
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
}
.icon-fixed-width {
@ -80,7 +96,10 @@ p
}
.truncated-text {
display: inline-block;
//display: inline-block;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}
.busy-indicator
@ -126,10 +145,11 @@ p
.sort-section {
display: block;
margin-bottom: 5px;
margin-bottom: $spacing-vertical * 2/3;
text-align: right;
line-height: 1;
font-size: 0.85em;
color: $color-help;
}

View file

@ -25,12 +25,6 @@
transform: translate(0, 0);
}
.icon-mega
{
font-size: 200px;
line-height: 1;
}
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */
.icon-glass:before {

View file

@ -1,8 +1,6 @@
@import "_reset";
@import "_grid";
@import "_icons";
@import "_mediaelement";
@import "_canvas";
@import "_gui";
@import "component/_table";
@import "component/_button.scss";
@ -10,6 +8,7 @@
@import "component/_file-actions.scss";
@import "component/_file-tile.scss";
@import "component/_form-field.scss";
@import "component/_header.scss";
@import "component/_menu.scss";
@import "component/_tooltip.scss";
@import "component/_load-screen.scss";

View file

@ -34,6 +34,11 @@ $button-focus-shift: 12%;
{
padding-left: 5px;
}
.icon:only-child
{
padding-left: 0;
padding-right: 0;
}
}
.button-block
{
@ -49,17 +54,17 @@ $button-focus-shift: 12%;
$color-button-text: white;
color: darken($color-button-text, $button-focus-shift * 0.5);
background-color: $color-primary;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
&:focus {
color: $color-button-text;
//box-shadow: $focus-box-shadow;
//box-shadow: $box-shadow-focus;
background-color: mix(black, $color-primary, $button-focus-shift)
}
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
}
.button-text
@ -76,3 +81,7 @@ $button-focus-shift: 12%;
@include text-link(#aaa);
font-size: 0.8em;
}
.button--flat
{
box-shadow: none !important;
}

View file

@ -7,7 +7,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
margin-right: auto;
max-width: $width-page-constrained;
background: $color-bg;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
border-radius: 2px;
margin-bottom: $spacing-vertical * 2/3;
overflow: auto;
@ -86,7 +86,7 @@ $card-link-scaling: 1.1;
.card--link:hover {
position: relative;
z-index: 1;
box-shadow: $focus-box-shadow;
box-shadow: $box-shadow-focus;
transform: scale($card-link-scaling);
transform-origin: 50% 50%;
overflow-x: visible;
@ -139,8 +139,12 @@ $height-card-small: $spacing-vertical * 15;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
/*hacky way to give space for hover */
padding-left: 20px;
margin-left: -20px; /*hacky way to give space for hover */
margin-left: -20px;
padding-right: 20px;
margin-right: -20px;
}
.card-row__header {
margin-bottom: $spacing-vertical / 3;

View file

@ -1,37 +1,26 @@
@import "../global";
$height-file-tile: $spacing-vertical * 8;
$height-file-tile: $spacing-vertical * 6;
.file-tile__row {
overflow: hidden;
height: $height-file-tile;
.credit-amount {
float: right;
}
//Hack! Remove below!
//also a hack
.card__media {
height: $height-file-tile;
max-width: $height-file-tile;
width: $height-file-tile;
margin-right: $spacing-vertical / 2;
float: left;
}
//basically everything here is a hack now
.file-tile__content {
padding-top: $spacing-vertical * 1/3;
margin-left: $height-file-tile + $spacing-vertical / 2;
}
.card__title-primary {
margin-top: $spacing-vertical * 2/3;
margin-top: 0;
}
}
.file-tile__thumbnail {
max-width: 100%;
max-height: $height-file-tile;
vertical-align: middle;
display: block;
margin-left: auto;
margin-right: auto;
}
.file-tile__thumbnail-container
{
height: $height-file-tile;
@include absolute-center();
}
.file-tile__title {
font-weight: bold;
}
.file-tile__description {
color: #444;
margin-top: 12px;
font-size: 0.9em;
}

View file

@ -0,0 +1,94 @@
@import "../global";
$color-header: #666;
$color-header-active: darken($color-header, 20%);
#header
{
color: $color-header;
background: #fff;
display: flex;
position: fixed;
box-shadow: $box-shadow-layer;
top: 0;
left: 0;
width: 100%;
z-index: 2;
padding: $spacing-vertical / 2;
box-sizing: border-box;
}
.header__item {
flex: 0 0 content;
padding-left: $spacing-vertical / 4;
padding-right: $spacing-vertical / 4;
}
.header__item--wunderbar {
flex-grow: 1;
}
.wunderbar
{
position: relative;
.icon {
position: absolute;
left: 10px;
top: $spacing-vertical / 2 - 4px; //hacked
}
}
.wunderbar--active .icon-search { color: $color-primary; }
.wunderbar__input {
background: rgba(255, 255, 255, 0.7);
width: 100%;
color: $color-header;
height: $spacing-vertical * 1.5;
line-height: $spacing-vertical * 1.5;
padding-left: 38px;
padding-right: 5px;
border: 1px solid $color-text-dark;
@include border-radius(2px);
border: 1px solid #ccc;
&:focus {
color: $color-header-active;
box-shadow: $box-shadow-focus;
border-color: $color-primary;
}
}
nav.sub-header
{
text-transform: uppercase;
padding: 0 0 $spacing-vertical;
&.sub-header--constrained {
max-width: $width-page-constrained;
margin-left: auto;
margin-right: auto;
}
> a
{
$sub-header-selected-underline-height: 2px;
display: inline-block;
margin: 0 15px;
padding: 0 5px;
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
color: $color-header;
&:first-child
{
margin-left: 0;
}
&:last-child
{
margin-right: 0;
}
&.sub-header-selected
{
border-bottom: $sub-header-selected-underline-height solid $color-header-active;
color: $color-header-active;
}
&:hover
{
color: $color-header-active;
}
}
}

View file

@ -10,7 +10,7 @@ $border-radius-menu: 2px;
position: absolute;
white-space: nowrap;
background-color: white;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
border-radius: $border-radius-menu;
padding-top: ($spacing-vertical / 5) 0px;
z-index: 1;

View file

@ -29,7 +29,7 @@
overflow: auto;
border-radius: 4px;
padding: $spacing-vertical;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
max-width: 400px;
}

View file

@ -15,6 +15,7 @@
z-index: 1;
left: 50%;
margin-left: $tooltip-body-width * -1 / 2;
white-space: normal;
box-sizing: border-box;
padding: $spacing-vertical / 2;
@ -24,7 +25,7 @@
background-color: $color-bg;
font-size: $font-size * 7/8;
line-height: $font-line-height;
box-shadow: $default-box-shadow;
box-shadow: $box-shadow-layer;
}
.tooltip--header .tooltip__link {