Seed Support #56

Closed
ocnios wants to merge 173 commits from master into build
36 changed files with 962 additions and 924 deletions
Showing only changes of commit 99405578af - Show all commits

View file

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

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import $clamp from 'clamp-js-main';
//component/icon.js //component/icon.js
export let Icon = React.createClass({ export let Icon = React.createClass({
@ -19,29 +18,15 @@ export let Icon = React.createClass({
export let TruncatedText = React.createClass({ export let TruncatedText = React.createClass({
propTypes: { propTypes: {
lines: React.PropTypes.number, lines: React.PropTypes.number
height: React.PropTypes.string,
auto: React.PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
lines: null, 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() { 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 lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.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'; import UriIndicator from '../component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/ /*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 isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : uri; const title = isConfirmed ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
const primaryUrl = "?show=" + uri;
return ( return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}> <section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card__inner file-tile__row"}> <a href={primaryUrl} className="card__link">
<div className="span3 file-tile__thumbnail-container"> <div className={"card__inner file-tile__row"}>
<a href={'?show=' + uri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + this.props.uri} /></a> <div className="card__media"
</div> style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
<div className="span9">
<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> </div>
<div className="card__actions"> <div className="file-tile__content">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} /> <div className="card__title-primary">
</div> { !this.props.hidePrice
<div className="card__content"> ? <FilePrice uri={this.props.uri} />
<p className="file-tile__description"> : null}
<TruncatedText lines={2}> <div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
</div>
<div className="card__content card__subtext">
<TruncatedText lines={3}>
{isConfirmed {isConfirmed
? metadata.description ? metadata.description
: <span className="empty">This file is pending confirmation.</span>} : <span className="empty">This file is pending confirmation.</span>}
</TruncatedText> </TruncatedText>
</p> </div>
</div> </div>
</div> </div>
</div> </a>
{this.state.showNsfwHelp {this.state.showNsfwHelp
? <div className='card-overlay'> ? <div className='card-overlay'>
<p> <p>
@ -227,6 +219,7 @@ export let FileCardStream = React.createClass({
export let FileTile = React.createClass({ export let FileTile = React.createClass({
_isMounted: false, _isMounted: false,
_isResolvePending: false,
propTypes: { propTypes: {
uri: React.PropTypes.string.isRequired, uri: React.PropTypes.string.isRequired,
@ -238,13 +231,12 @@ export let FileTile = React.createClass({
claimInfo: null claimInfo: null
} }
}, },
resolve: function(uri) {
componentDidMount: function() { this._isResolvePending = true;
this._isMounted = true; lbry.resolve({uri: uri}).then((resolutionInfo) => {
this._isResolvePending = false;
lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value && if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) { 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 // In case of a failed lookup, metadata will be null, in which case the component will never display
this.setState({ this.setState({
claimInfo: resolutionInfo.claim, claimInfo: resolutionInfo.claim,
@ -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() { componentWillUnmount: function() {
this._isMounted = false; 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} return <FileCardStream outpoint={null} metadata={{title: this.props.uri, description: "Loading..."}} contentType={null} hidePrice={true}
hasSignature={false} signatureIsValid={false} uri={this.props.uri} /> 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; return null;
} }

View file

@ -1,76 +1,198 @@
import React from 'react'; import React from 'react';
import lbryuri from '../lbryuri.js';
import {Link} from './link.js'; import {Link} from './link.js';
import {Icon} from './common.js'; import {Icon, CreditAmount} from './common.js';
var Header = React.createClass({ var Header = React.createClass({
_balanceSubscribeId: null,
_isMounted: false,
propTypes: {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
},
getInitialState: function() { getInitialState: function() {
return { return {
title: "LBRY", balance: 0
isScrolled: false
}; };
}, },
componentWillMount: function() {
new MutationObserver((mutations) => {
this.setState({ title: mutations[0].target.textContent });
}).observe(
document.querySelector('title'),
{ subtree: true, characterData: true, childList: true }
);
},
componentDidMount: function() { componentDidMount: function() {
document.addEventListener('scroll', this.handleScroll); this._isMounted = true;
}, this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
componentWillUnmount: function() { if (this._isMounted) {
document.removeEventListener('scroll', this.handleScroll); this.setState({balance: balance});
if (this.userTypingTimer) }
{
clearTimeout(this.userTypingTimer);
}
},
handleScroll: function() {
this.setState({
isScrolled: document.body.scrollTop > 0
}); });
}, },
onQueryChange: function(event) { componentWillUnmount: function() {
this._isMounted = false;
if (this.userTypingTimer) if (this._balanceSubscribeId) {
{ lbry.balanceUnsubscribe(this._balanceSubscribeId)
clearTimeout(this.userTypingTimer);
} }
//@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() { render: function() {
return ( return <header id="header">
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }> <div className="header__item">
<div className="header-top-bar"> <Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" /> </div>
<h1>{ this.state.title }</h1> <div className="header__item">
<div className="header-search"> <Link href="?discover" button="alt button--flat" icon="icon-home" />
<Icon icon="icon-search" /> </div>
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery} <div className="header__item header__item--wunderbar">
placeholder="Find movies, music, games, and more"/> <WunderBar address={this.props.address} icon={this.props.wunderBarIcon}
</div> 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> </div>
{
this.props.links ?
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
''
}
</header> </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() { render: function() {
var links = [], let links = [],
viewingUrl = '?' + this.props.viewingPage; viewingUrl = '?' + this.props.viewingPage;
for (let link of Object.keys(this.props.links)) { for (let link of Object.keys(this.props.links)) {
@ -81,7 +203,7 @@ var SubHeader = React.createClass({
); );
} }
return ( return (
<nav className="sub-header"> <nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
{links} {links}
</nav> </nav>
); );

View file

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

View file

@ -9,9 +9,6 @@ var LoadScreen = React.createClass({
details: React.PropTypes.string, details: React.PropTypes.string,
isWarning: React.PropTypes.bool, isWarning: React.PropTypes.bool,
}, },
handleCancelClick: function() {
history.back();
},
getDefaultProps: function() { getDefaultProps: function() {
return { return {
isWarning: false, isWarning: false,
@ -34,9 +31,6 @@ var LoadScreen = React.createClass({
<BusyMessage message={this.props.message} /> <BusyMessage message={this.props.message} />
</h3> </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> {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>
</div> </div>
); );

View file

@ -31,7 +31,6 @@ function savePendingPublish({name, channel_name}) {
return newPendingPublish; return newPendingPublish;
} }
/** /**
* If there is a pending publish with the given name or outpoint, remove it. * If there is a pending publish with the given name or outpoint, remove it.
* A channel name may also be provided along with name. * A channel name may also be provided along with name.
@ -132,6 +131,21 @@ lbry.connect = function() {
return lbry._connectPromise; 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) { lbry.isDaemonAcceptingConnections = function (callback) {
// Returns true/false whether the daemon is at a point it will start returning status // Returns true/false whether the daemon is at a point it will start returning status
lbry.call('status', {}, () => callback(true), null, () => callback(false)) lbry.call('status', {}, () => callback(true), null, () => callback(false))
@ -633,7 +647,7 @@ lbry.resolve = function(params={}) {
if (!params.uri) { if (!params.uri) {
throw "Resolve has hacked cache on top of it that requires a 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]); resolve(claimCache[params.uri]);
} else { } else {
lbry.call('resolve', params, function(data) { lbry.call('resolve', params, function(data) {

View file

@ -7,7 +7,7 @@ const lbryio = {
_accessToken: getLocal('accessToken'), _accessToken: getLocal('accessToken'),
_authenticationPromise: null, _authenticationPromise: null,
_user : 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/'; 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 { } else {
setCurrentUser() 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); }).catch(reject);
}); });
} }

View file

@ -1,79 +1,18 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.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 {FileTile, FileTileStream} from '../component/file-tile.js';
import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.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 ' + 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!'); '"five" to put your content here!');
var FeaturedCategory = React.createClass({ let FeaturedCategory = React.createClass({
render: function() { render: function() {
return (<div className="card-row card-row--small"> return (<div className="card-row card-row--small">
{ this.props.category ? { this.props.category ?
<h3 className="card-row__header">{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"/> <ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
: '' }</h3> : '' }</h3>
: '' } : '' }
@ -82,7 +21,7 @@ var FeaturedCategory = React.createClass({
} }
}) })
var FeaturedContent = React.createClass({ let DiscoverPage = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
featuredUris: {}, featuredUris: {},
@ -105,101 +44,19 @@ var FeaturedContent = React.createClass({
}); });
}, },
render: function() { render: function() {
return ( return <main>{
this.state.failed ? this.state.failed ?
<div className="empty">Failed to load landing content.</div> : <div className="empty">Failed to load landing content.</div> :
<div> <div>
{ {
Object.keys(this.state.featuredUris).map((category) => { Object.keys(this.state.featuredUris).map((category) => {
return this.state.featuredUris[category].length ? return this.state.featuredUris[category].length ?
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> : <FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
''; '';
}) })
} }
</div> </div>
); }</main>;
}
});
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>
);
} }
}); });

View file

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

View file

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

View file

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

View file

@ -18,9 +18,6 @@ var ReportPage = React.createClass({
this._messageArea.value = ''; this._messageArea.value = '';
} }
}, },
componentDidMount: function() {
document.title = "Report an Issue";
},
closeModal: function() { closeModal: function() {
this.setState({ this.setState({
modal: null, modal: null,
@ -34,7 +31,7 @@ var ReportPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<main className="page"> <main className="main--single-column">
<section className="card"> <section className="card">
<h3>Report an Issue</h3> <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> <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 {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js'; import rewards from '../rewards.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {WalletNav} from './wallet.js';
import {RewardLink} from '../component/link.js'; import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({ const RewardTile = React.createClass({
@ -56,14 +57,15 @@ var RewardsPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<main> <main className="main--single-column">
<form onSubmit={this.handleSubmit}> <WalletNav viewingPage="rewards"/>
<div>
{!this.state.userRewards {!this.state.userRewards
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '') ? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => { : 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} />; return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
})} })}
</form> </div>
</main> </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 React from 'react';
import {FormField, FormRow} from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import {SubHeader} from '../component/header.js';
import lbry from '../lbry.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({ var SettingsPage = React.createClass({
_onSettingSaveSuccess: function() { _onSettingSaveSuccess: function() {
// This is bad. // This is bad.
@ -17,7 +27,7 @@ var SettingsPage = React.createClass({
setClientSetting: function(name, value) { setClientSetting: function(name, value) {
lbry.setClientSetting(name, value) lbry.setClientSetting(name, value)
this._onSettingSaveSuccess() this._onSettingSaveSuccess()
}, },
onRunOnStartChange: function (event) { onRunOnStartChange: function (event) {
this.setDaemonSetting('run_on_startup', event.target.checked); this.setDaemonSetting('run_on_startup', event.target.checked);
}, },
@ -56,9 +66,6 @@ var SettingsPage = React.createClass({
showUnavailable: lbry.getClientSetting('showUnavailable'), showUnavailable: lbry.getClientSetting('showUnavailable'),
} }
}, },
componentDidMount: function() {
document.title = "Settings";
},
componentWillMount: function() { componentWillMount: function() {
lbry.getDaemonSettings((settings) => { lbry.getDaemonSettings((settings) => {
this.setState({ this.setState({
@ -92,7 +99,8 @@ var SettingsPage = React.createClass({
</section> </section>
*/ */
return ( return (
<main> <main className="main--single-column">
<SettingsNav viewingPage="settings" />
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>Download Directory</h3> <h3>Download Directory</h3>

View file

@ -16,8 +16,11 @@ var FormatItem = React.createClass({
outpoint: React.PropTypes.string, outpoint: React.PropTypes.string,
}, },
render: function() { render: function() {
const {thumbnail, author, title, description, language, license} = this.props.metadata; const {author, language, license} = this.props.metadata;
const mediaType = lbry.getMediaType(this.props.contentType);
if (!this.props.contentType && [author, language, license].filter((val) => {return !!val; }).length === 0) {
return null;
}
return ( return (
<table className="table-standard"> <table className="table-standard">
@ -40,93 +43,108 @@ var FormatItem = React.createClass({
} }
}); });
let ShowPage = React.createClass({ let ChannelPage = React.createClass({
_uri: null, 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: { propTypes: {
uri: React.PropTypes.string, uri: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
metadata: null,
contentType: null,
hasSignature: false,
signatureIsValid: false,
cost: null, cost: null,
costIncludesData: null, costIncludesData: null,
uriLookupComplete: null,
isDownloaded: 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() { componentWillMount: function() {
this._uri = lbryuri.normalize(this.props.uri); this._isMounted = true;
document.title = this._uri; 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}}}}}) => { loadCostAndFileState: function(uri, outpoint) {
const outpoint = txid + ':' + nout; lbry.file_list({outpoint: outpoint}).then((fileInfo) => {
if (this._isMounted) {
lbry.file_list({outpoint}).then((fileInfo) => {
this.setState({ this.setState({
isDownloaded: fileInfo.length > 0, 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}) => {
this.setState({ if (this._isMounted) {
cost: cost, this.setState({
costIncludesData: includesData, cost: cost,
}); costIncludesData: includesData,
});
}
}); });
}, },
render: function() { render: function() {
const metadata = this.state.metadata; const metadata = this.props.metadata,
const title = metadata ? this.state.metadata.title : this._uri; title = metadata ? this.props.metadata.title : this.props.uri,
uriIndicator = <UriIndicator uri={this.props.uri} hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />;
return ( return (
<main className="constrained-page"> <main className="main--single-column">
<section className="show-page-media"> <section className="show-page-media">
{ this.state.contentType && this.state.contentType.startsWith('video/') ? { this.props.contentType && this.props.contentType.startsWith('video/') ?
<Video className="video-embedded" uri={this._uri} metadata={metadata} outpoint={this.state.outpoint} /> : <Video className="video-embedded" uri={this.props.uri} metadata={metadata} outpoint={this.props.outpoint} /> :
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) } (metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
</section> </section>
<section className="card"> <section className="card">
<div className="card__inner"> <div className="card__inner">
<div className="card__title-identity"> <div className="card__title-identity">
{this.state.isDownloaded === false {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} : null}
<h1>{title}</h1> <h1>{title}</h1>
{ this.state.uriLookupComplete ? <div className="card__subtitle">
<div> { this.props.channelUri ?
<div className="card__subtitle"> <Link href={"?show=" + this.props.channelUri }>{uriIndicator}</Link> :
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} /> uriIndicator}
</div> </div>
<div className="card__actions"> <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> : '' } </div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata.description}
</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> </div>
{ metadata ? { metadata ?
<div className="card__content"> <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> : '' }
<div className="card__content"> <div className="card__content">
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" /> <Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</div> </div>
@ -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; export default ShowPage;

View file

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

View file

@ -2,6 +2,7 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {SubHeader} from '../component/header.js';
import {FormField, FormRow} from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.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({ var WalletPage = React.createClass({
_balanceSubscribeId: null, _balanceSubscribeId: null,
@ -270,9 +281,6 @@ var WalletPage = React.createClass({
propTypes: { propTypes: {
viewingPage: React.PropTypes.string, 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? 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? What is the proper React pattern for sharing a global state like balance?
@ -296,7 +304,8 @@ var WalletPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<main className="page"> <main className="main--single-column">
<WalletNav viewingPage={this.props.viewingPage} />
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h3>Balance</h3> <h3>Balance</h3>

View file

@ -2,9 +2,9 @@
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet. * is not set yet.
*/ */
export function getLocal(key) { export function getLocal(key, fallback=undefined) {
const itemRaw = localStorage.getItem(key); 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-cli": "^6.11.4",
"babel-preset-es2015": "^6.13.2", "babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"clamp-js-main": "^0.11.1",
"mediaelement": "^2.23.4", "mediaelement": "^2.23.4",
"node-sass": "^3.8.0", "node-sass": "^3.8.0",
"rc-progress": "^2.0.6", "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-button: $spacing-vertical * 1.5;
$height-video-embedded: $width-page-constrained * 9 / 16; $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); $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);
$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-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; $transition-standard: .225s ease;
@ -160,4 +160,35 @@ $blur-intensity: 8px;
width:1px; width:1px;
height:1px; height:1px;
overflow:hidden; 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"; @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 { width: $width-page-constrained;
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;
} }
.icon-fixed-width { .icon-fixed-width {
@ -80,7 +96,10 @@ p
} }
.truncated-text { .truncated-text {
display: inline-block; //display: inline-block;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
} }
.busy-indicator .busy-indicator
@ -126,10 +145,11 @@ p
.sort-section { .sort-section {
display: block; display: block;
margin-bottom: 5px; margin-bottom: $spacing-vertical * 2/3;
text-align: right; text-align: right;
line-height: 1;
font-size: 0.85em; font-size: 0.85em;
color: $color-help; color: $color-help;
} }

View file

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

View file

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

View file

@ -34,6 +34,11 @@ $button-focus-shift: 12%;
{ {
padding-left: 5px; padding-left: 5px;
} }
.icon:only-child
{
padding-left: 0;
padding-right: 0;
}
} }
.button-block .button-block
{ {
@ -49,17 +54,17 @@ $button-focus-shift: 12%;
$color-button-text: white; $color-button-text: white;
color: darken($color-button-text, $button-focus-shift * 0.5); color: darken($color-button-text, $button-focus-shift * 0.5);
background-color: $color-primary; background-color: $color-primary;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
&:focus { &:focus {
color: $color-button-text; color: $color-button-text;
//box-shadow: $focus-box-shadow; //box-shadow: $box-shadow-focus;
background-color: mix(black, $color-primary, $button-focus-shift) background-color: mix(black, $color-primary, $button-focus-shift)
} }
} }
.button-alt .button-alt
{ {
background-color: $color-bg-alt; background-color: $color-bg-alt;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
} }
.button-text .button-text
@ -76,3 +81,7 @@ $button-focus-shift: 12%;
@include text-link(#aaa); @include text-link(#aaa);
font-size: 0.8em; 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; margin-right: auto;
max-width: $width-page-constrained; max-width: $width-page-constrained;
background: $color-bg; background: $color-bg;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
border-radius: 2px; border-radius: 2px;
margin-bottom: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 2/3;
overflow: auto; overflow: auto;
@ -86,7 +86,7 @@ $card-link-scaling: 1.1;
.card--link:hover { .card--link:hover {
position: relative; position: relative;
z-index: 1; z-index: 1;
box-shadow: $focus-box-shadow; box-shadow: $box-shadow-focus;
transform: scale($card-link-scaling); transform: scale($card-link-scaling);
transform-origin: 50% 50%; transform-origin: 50% 50%;
overflow-x: visible; overflow-x: visible;
@ -139,8 +139,12 @@ $height-card-small: $spacing-vertical * 15;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
white-space: nowrap; white-space: nowrap;
/*hacky way to give space for hover */
padding-left: 20px; 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 { .card-row__header {
margin-bottom: $spacing-vertical / 3; margin-bottom: $spacing-vertical / 3;

View file

@ -1,37 +1,26 @@
@import "../global"; @import "../global";
$height-file-tile: $spacing-vertical * 8; $height-file-tile: $spacing-vertical * 6;
.file-tile__row { .file-tile__row {
overflow: hidden;
height: $height-file-tile; height: $height-file-tile;
.credit-amount { .credit-amount {
float: right; float: right;
} }
//Hack! Remove below! //also a hack
.card__title-primary { .card__media {
margin-top: $spacing-vertical * 2/3; 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: 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; position: absolute;
white-space: nowrap; white-space: nowrap;
background-color: white; background-color: white;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
border-radius: $border-radius-menu; border-radius: $border-radius-menu;
padding-top: ($spacing-vertical / 5) 0px; padding-top: ($spacing-vertical / 5) 0px;
z-index: 1; z-index: 1;

View file

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

View file

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