Merge pull request #143 from lbryio/development

Release
This commit is contained in:
alexliebowitz 2017-02-01 14:11:30 -05:00 committed by GitHub
commit 181d267bd2
33 changed files with 1294 additions and 1030 deletions

View file

@ -1,23 +1,28 @@
# LBRY Web User Interface # LBRY Web User Interface
This is the frontend for LBRY's in-browser application, that is automatically installed when a user installs [LBRY](https://github.com/lbryio/lbry). This is the web-based frontend for the LBRY network. It is automatically installed when a user installs [LBRY](https://github.com/lbryio/lbry).
## Development Setup ## Development Setup
These steps will get you to change-reload-see:
- Install [LBRY](https://github.com/lbryio/lbry/releases) - Install [LBRY](https://github.com/lbryio/lbry/releases)
- Install node and npm ([this gist may be useful](https://gist.github.com/isaacs/579814)) - Install node and npm (linux users: [use this](https://github.com/nodesource/distributions). if that doesn't work, [this gist may be useful](https://gist.github.com/isaacs/579814))
- Run `./watch.sh` (this will `npm install` dependencies). Changes made in `sass` and `js` will be auto compiled to `dist` - Checkout this project via git
- Run `lbrynet-daemon --ui=/full/path/to/dist/` to start LBRY - Run `./watch.sh` (this will `npm install` dependencies)
- `lbry.call('configure_ui', {path: '/path/to/ui'})` can be used in JS console on web ui to switch ui path. This is also needed to trigger a reload after making changes to the UI. - Run LBRY
- `lbrynet-daemon --branch=branchname` can be used to test remote branches
- Occasionally refreshing the cache may be necessary for changes to show up in browser
## Common Issues While `watch.sh` is running, any change made to the `js` or `scss` folders will automatically be compiled into the `dist` folder.
1. Error: Couldn't find preset "es2015" relative to directory "js"
Fix with: While changes will automatically compile, they will not automatically be loaded by the app. Every time a file changes, you must run:
`lbrynet-cli configure_ui path=/path/to/repo/dist`
Then reload the page. This call can also be made directly via the browser Javascript console:
`lbry.call('configure_ui', {path: '/path/to/ui'})`
To reset your UI to the version packaged with the application, run:
`lbrynet-cli configure_ui branch=master`
This command also works to test non-released branches of `lbry-web-ui`
npm install babel-preset-es2015 --save
npm install babel-preset-react --save

View file

@ -4,7 +4,6 @@ import SettingsPage from './page/settings.js';
import HelpPage from './page/help.js'; import HelpPage from './page/help.js';
import WatchPage from './page/watch.js'; import WatchPage from './page/watch.js';
import ReportPage from './page/report.js'; import ReportPage from './page/report.js';
import MyFilesPage from './page/my_files.js';
import StartPage from './page/start.js'; import StartPage from './page/start.js';
import ClaimCodePage from './page/claim_code.js'; import ClaimCodePage from './page/claim_code.js';
import ReferralPage from './page/referral.js'; import ReferralPage from './page/referral.js';
@ -14,6 +13,7 @@ import PublishPage from './page/publish.js';
import DiscoverPage from './page/discover.js'; import DiscoverPage from './page/discover.js';
import SplashScreen from './component/splash.js'; import SplashScreen from './component/splash.js';
import DeveloperPage from './page/developer.js'; import DeveloperPage from './page/developer.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Drawer from './component/drawer.js'; import Drawer from './component/drawer.js';
import Header from './component/header.js'; import Header from './component/header.js';
import Modal from './component/modal.js'; import Modal from './component/modal.js';
@ -164,9 +164,9 @@ var App = React.createClass({
case 'report': case 'report':
return <ReportPage />; return <ReportPage />;
case 'downloaded': case 'downloaded':
return <MyFilesPage show="downloaded" />; return <FileListDownloaded />;
case 'published': case 'published':
return <MyFilesPage show="published" />; return <FileListPublished />;
case 'start': case 'start':
return <StartPage />; return <StartPage />;
case 'claim': case 'claim':
@ -190,7 +190,8 @@ var App = React.createClass({
}, },
render: function() { render: function() {
var mainContent = this.getMainContent(), var mainContent = this.getMainContent(),
headerLinks = this.getHeaderLinks(); headerLinks = this.getHeaderLinks(),
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
return ( return (
this.state.viewingPage == 'watch' ? this.state.viewingPage == 'watch' ?
@ -198,7 +199,7 @@ var App = React.createClass({
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }> <div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} /> <Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }> <div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
<Header onOpenDrawer={this.openDrawer} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} /> <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,18 +1,19 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import $clamp from 'clamp'; import $clamp from 'clamp-js';
//component/icon.js //component/icon.js
export let Icon = React.createClass({ export let Icon = React.createClass({
propTypes: { propTypes: {
style: React.PropTypes.object, icon: React.PropTypes.string.isRequired,
fixed: React.PropTypes.bool,
className: React.PropTypes.string, className: React.PropTypes.string,
fixed: React.PropTypes.bool,
}, },
render: function() { render: function() {
var className = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + this.props.icon + ' ' + const {fixed, className, ...other} = this.props;
(this.props.className || '')); const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
return <span className={className} style={this.props.style}></span> this.props.icon + ' ' + (this.props.className || ''));
return <span className={spanClassName} {... other}></span>
} }
}); });
@ -49,31 +50,6 @@ export let BusyMessage = React.createClass({
} }
}); });
var toolTipStyle = {
position: 'absolute',
zIndex: '1',
top: '100%',
left: '-120px',
width: '260px',
padding: '15px',
border: '1px solid #aaa',
backgroundColor: '#fff',
fontSize: '14px',
};
export let ToolTip = React.createClass({
propTypes: {
open: React.PropTypes.bool.isRequired,
onMouseOut: React.PropTypes.func
},
render: function() {
return (
<div className={this.props.open ? '' : 'hidden'} style={toolTipStyle} onMouseOut={this.props.onMouseOut}>
{this.props.children}
</div>
);
}
});
var creditAmountStyle = { var creditAmountStyle = {
color: '#216C2A', color: '#216C2A',
fontWeight: 'bold', fontWeight: 'bold',
@ -123,7 +99,7 @@ export let Thumbnail = React.createClass({
_isMounted: false, _isMounted: false,
propTypes: { propTypes: {
src: React.PropTypes.string.isRequired, src: React.PropTypes.string,
}, },
handleError: function() { handleError: function() {
if (this.state.imageUrl != this._defaultImageUri) { if (this.state.imageUrl != this._defaultImageUri) {
@ -151,6 +127,6 @@ export let Thumbnail = React.createClass({
this._isMounted = false; this._isMounted = false;
}, },
render: function() { render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} /> return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
}, },
}); });

View file

@ -0,0 +1,287 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import {Icon} from '../component/common.js';
import Modal from './modal.js';
import FormField from './form.js';
import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js';
let WatchLink = React.createClass({
propTypes: {
streamName: React.PropTypes.string,
},
handleClick: function() {
this.setState({
loading: true,
})
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
loading: false,
});
} else {
window.location = '?watch=' + this.props.streamName;
}
});
});
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<div className="button-container">
<Link button="primary" disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} />
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
</div>
);
}
});
let FileActionsRow = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
propTypes: {
streamName: React.PropTypes.string,
sdHash: React.PropTypes.string.isRequired,
metadata: React.PropTypes.object
},
getInitialState: function() {
return {
fileInfo: null,
modal: null,
menuOpen: false,
deleteChecked: false,
attemptingDownload: false,
attemptingRemove: false
}
},
onFileInfoUpdate: function(fileInfo) {
if (this._isMounted) {
this.setState({
fileInfo: fileInfo ? fileInfo : false,
attemptingDownload: fileInfo ? false : this.state.attemptingDownload
});
}
},
tryDownload: function() {
this.setState({
attemptingDownload: true,
attemptingRemove: false
});
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
attemptingDownload: false,
});
} else {
lbry.getStream(this.props.streamName, (streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
});
}
});
});
},
closeModal: function() {
this.setState({
modal: null,
})
},
onDownloadClick: function() {
if (!this.state.fileInfo && !this.state.attemptingDownload) {
this.tryDownload();
}
},
onOpenClick: function() {
if (this.state.fileInfo && this.state.fileInfo.completed) {
lbry.openFile(this.props.sdHash);
}
},
handleDeleteCheckboxClicked: function(event) {
this.setState({
deleteChecked: event.target.checked,
});
},
handleRevealClicked: function() {
if (this.state.fileInfo && this.state.fileInfo.download_path) {
lbry.revealFile(this.props.sdHash);
}
},
handleRemoveClicked: function() {
this.setState({
modal: 'confirmRemove',
});
},
handleRemoveConfirmed: function() {
if (this.props.streamName) {
lbry.removeFile(this.props.sdHash, this.props.streamName, this.state.deleteChecked);
} else {
alert('this file cannot be deleted because lbry is a retarded piece of shit');
}
this.setState({
modal: null,
fileInfo: false,
attemptingDownload: false
});
},
openMenu: function() {
this.setState({
menuOpen: !this.state.menuOpen,
});
},
componentDidMount: function() {
this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.sdHash, this.onFileInfoUpdate);
},
componentWillUnmount: function() {
this._isMounted = false;
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.sdHash, this._fileInfoSubscribeId);
}
},
render: function() {
if (this.state.fileInfo === null)
{
return null;
}
const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
showMenu = !!this.state.fileInfo;
let linkBlock;
if (this.state.fileInfo === false && !this.state.attemptingDownload) {
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.onDownloadClick} />;
} else if (this.state.attemptingDownload || !this.state.fileInfo.completed) {
const
progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
linkBlock = (
<div className="faux-button-block file-actions__download-status-bar">
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
{labelWithIcon}
</div>
);
} else {
linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={this.onOpenClick} />;
}
return (
<div>
{(this.props.metadata.content_type && this.props.metadata.content_type.startsWith('video/')) ? <WatchLink streamName={this.props.streamName} /> : null}
{this.state.fileInfo !== null || this.state.fileInfo.isMine ?
<div className="button-container">{linkBlock}</div>
: null}
{ showMenu ?
<DropDownMenu>
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
</DropDownMenu> : '' }
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
onConfirmed={this.closeModal}>
LBRY was unable to download the stream <strong>lbry://{this.props.streamName}</strong>.
</Modal>
<Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
onAborted={this.closeModal}>
<p>Are you sure you'd like to remove <cite>{this.props.metadata.title}</cite> from LBRY?</p>
<label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
</Modal>
</div>
);
}
});
export let FileActions = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
propTypes: {
streamName: React.PropTypes.string,
sdHash: React.PropTypes.string.isRequired,
metadata: React.PropTypes.object
},
getInitialState: function() {
return {
available: true,
forceShowActions: false,
fileInfo: null,
}
},
onShowFileActionsRowClicked: function() {
this.setState({
forceShowActions: true,
});
},
onFileInfoUpdate: function(fileInfo) {
this.setState({
fileInfo: fileInfo,
});
},
componentDidMount: function() {
this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.sdHash, this.onFileInfoUpdate);
lbry.getPeersForBlobHash(this.props.sdHash, (peers) => {
if (!this._isMounted) {
return;
}
this.setState({
available: peers.length > 0,
});
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
const fileInfo = this.state.fileInfo;
if (fileInfo === null) {
return null;
}
return (<section className="file-actions">
{
fileInfo || this.state.available || this.state.forceShowActions
? <FileActionsRow sdHash={this.props.sdHash} metadata={this.props.metadata} streamName={this.props.streamName} />
: <div>
<div className="button-container empty">This file is not currently available.</div>
<div className="button-container">
<ToolTip label="Why?"
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment." />
</div>
<div className="button-container">
<Link label="Try Anyway" onClick={this.onShowFileActionsRowClicked} />
</div>
</div>
}
</section>);
}
});

198
js/component/file-tile.js Normal file
View file

@ -0,0 +1,198 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.js';
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js';
let FilePrice = React.createClass({
_isMounted: false,
propTypes: {
name: React.PropTypes.string
},
getInitialState: function() {
return {
cost: null,
costIncludesData: null,
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
costIncludesData: includesData,
});
}
}, () => {
// If we get an error looking up cost information, do nothing
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.cost === null)
{
return null;
}
return (
<span className="file-tile__cost">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
);
}
});
/*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
propTypes: {
metadata: React.PropTypes.object,
sdHash: React.PropTypes.string,
hideOnRemove: React.PropTypes.bool,
hidePrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool
},
getInitialState: function() {
return {
showNsfwHelp: false,
isHidden: false,
available: null,
}
},
getDefaultProps: function() {
return {
obscureNsfw: !lbry.getClientSetting('showNsfw'),
hidePrice: false
}
},
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
lbry.fileInfoSubscribe(this.props.sdHash, this.onFileInfoUpdate);
}
},
componentWillUnmount: function() {
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.sdHash, this._fileInfoSubscribeId);
}
},
onFileInfoUpdate: function(fileInfo) {
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
this.setState({
isHidden: true
});
}
},
handleMouseOver: function() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
},
handleMouseOut: function() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: false,
});
}
},
render: function() {
if (this.state.isHidden) {
return null;
}
const metadata = this.props.metadata;
const isConfirmed = typeof metadata == 'object';
const title = isConfirmed ? metadata.title : ('lbry://' + this.props.name);
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card-content file-tile__row"}>
<div className="span3">
<a href={'/?show=' + this.props.name}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={`Photo for ${title}`} /></a>
</div>
<div className="span9">
{ !this.props.hidePrice
? <FilePrice name={this.props.name} />
: null}
<div className="meta"><a href={'/?show=' + this.props.name}>{'lbry://' + this.props.name}</a></div>
<h3 className="file-tile__title">
<a href={'/?show=' + this.props.name}>
<TruncatedText lines={1}>
{title}
</TruncatedText>
</a>
</h3>
<FileActions streamName={this.props.name} sdHash={this.props.sdHash} metadata={metadata} />
<p className="file-tile__description">
<TruncatedText lines={3}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</p>
</div>
</div>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
</p>
</div>
: null}
</section>
);
}
});
export let FileTile = React.createClass({
_isMounted: false,
propTypes: {
name: React.PropTypes.string.isRequired,
available: React.PropTypes.bool,
},
getInitialState: function() {
return {
sdHash: null,
metadata: null
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.resolveName(this.props.name, (metadata) => {
if (this._isMounted) {
this.setState({
sdHash: metadata.sources.lbry_sd_hash,
metadata: metadata,
});
}
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (!this.state.metadata || !this.state.sdHash) {
return null;
}
return <FileTileStream sdHash={this.state.sdHash} metadata={this.state.metadata} {... this.props} />;
}
});

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js';
var requiredFieldWarningStyle = { var requiredFieldWarningStyle = {
color: '#cc0000', color: '#cc0000',

View file

@ -52,7 +52,7 @@ var Header = React.createClass({
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" /> <Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
<h1>{ this.state.title }</h1> <h1>{ this.state.title }</h1>
<div className="header-search"> <div className="header-search">
<input type="search" onChange={this.onQueryChange} <input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
placeholder="Find movies, music, games, and more"/> placeholder="Find movies, music, games, and more"/>
</div> </div>
</div> </div>
@ -70,7 +70,7 @@ var SubHeader = React.createClass({
render: function() { render: function() {
var links = [], var 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)) {
links.push( links.push(
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }> <a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>

View file

@ -1,221 +1,55 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import {Icon} from './common.js';
import Modal from './modal.js';
import {Icon, ToolTip} from './common.js';
export let Link = React.createClass({ export let Link = React.createClass({
handleClick: function() { propTypes: {
label: React.PropTypes.string,
icon: React.PropTypes.string,
button: React.PropTypes.string,
badge: React.PropTypes.string,
hidden: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
hidden: false,
disabled: false,
};
},
handleClick: function(e) {
if (this.props.onClick) { if (this.props.onClick) {
this.props.onClick(); this.props.onClick(e);
} }
}, },
render: function() { render: function() {
var href = this.props.href ? this.props.href : 'javascript:;', if (this.props.hidden) {
icon = this.props.icon ? <Icon icon={this.props.icon} fixed={true} /> : '', return null;
className = (this.props.className ? this.props.className : '') + }
(this.props.button ? ' button-block button-' + this.props.button : '') +
(this.props.hidden ? ' hidden' : '') + /* The way the class name is generated here is a mess -- refactor */
(this.props.disabled ? ' disabled' : '');
const className = (this.props.className || '') +
(!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
(this.props.button ? 'button-block button-' + this.props.button : '') +
(this.props.disabled ? ' disabled' : '');
let content;
if (this.props.children) { // Custom content
content = this.props.children;
} else {
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>}
{'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
</span>
);
}
return ( return (
<a className={className ? className : 'button-text'} href={href} style={this.props.style ? this.props.style : {}} <a className={className} href={this.props.href || 'javascript:;'} title={this.props.title}
title={this.props.title} onClick={this.handleClick}> onClick={this.handleClick} {... 'style' in this.props ? {style: this.props.style} : {}}>
{this.props.icon ? icon : '' } {content}
<span className="link-label">{this.props.label}</span>
{this.props.badge ? <span className="badge">{this.props.badge}</span> : '' }
</a> </a>
); );
} }
}); });
var linkContainerStyle = {
position: 'relative',
};
export let ToolTipLink = React.createClass({
getInitialState: function() {
return {
showTooltip: false,
};
},
handleClick: function() {
if (this.props.tooltip) {
this.setState({
showTooltip: !this.state.showTooltip,
});
}
if (this.props.onClick) {
this.props.onClick();
}
},
handleTooltipMouseOut: function() {
this.setState({
showTooltip: false,
});
},
render: function() {
var href = this.props.href ? this.props.href : 'javascript:;',
icon = this.props.icon ? <Icon icon={this.props.icon} /> : '',
className = this.props.className +
(this.props.button ? ' button-block button-' + this.props.button : '') +
(this.props.hidden ? ' hidden' : '') +
(this.props.disabled ? ' disabled' : '');
return (
<span style={linkContainerStyle}>
<a className={className ? className : 'button-text'} href={href} style={this.props.style ? this.props.style : {}}
title={this.props.title} onClick={this.handleClick}>
{this.props.icon ? icon : '' }
{this.props.label}
</a>
{(!this.props.tooltip ? null :
<ToolTip open={this.state.showTooltip} onMouseOut={this.handleTooltipMouseOut}>
{this.props.tooltip}
</ToolTip>
)}
</span>
);
}
});
export let DownloadLink = React.createClass({
propTypes: {
type: React.PropTypes.string,
streamName: React.PropTypes.string,
label: React.PropTypes.string,
downloadingLabel: React.PropTypes.string,
button: React.PropTypes.string,
style: React.PropTypes.object,
hidden: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
icon: 'icon-download',
label: 'Download',
downloadingLabel: 'Downloading...',
}
},
getInitialState: function() {
return {
downloading: false,
filePath: null,
modal: null,
}
},
closeModal: function() {
this.setState({
modal: null,
})
},
handleClick: function() {
this.setState({
downloading: true
});
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
downloading: false
});
} else {
lbry.getStream(this.props.streamName, (streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
downloading: false,
});
} else {
this.setState({
modal: 'downloadStarted',
filePath: streamInfo.path,
});
}
});
}
});
});
},
render: function() {
var label = (!this.state.downloading ? this.props.label : this.props.downloadingLabel);
return (
<span className="button-container">
<Link button={this.props.button} hidden={this.props.hidden} style={this.props.style}
disabled={this.state.downloading} label={label} icon={this.props.icon} onClick={this.handleClick} />
<Modal className="download-started-modal" isOpen={this.state.modal == 'downloadStarted'}
contentLabel="Download started" onConfirmed={this.closeModal}>
<p>Downloading to:</p>
<div className="download-started-modal__file-path">{this.state.filePath}</div>
</Modal>
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
onConfirmed={this.closeModal}>
LBRY was unable to download the stream <strong>lbry://{this.props.streamName}</strong>.
</Modal>
</span>
);
}
});
export let WatchLink = React.createClass({
propTypes: {
type: React.PropTypes.string,
streamName: React.PropTypes.string,
label: React.PropTypes.string,
button: React.PropTypes.string,
style: React.PropTypes.object,
hidden: React.PropTypes.bool,
},
handleClick: function() {
this.setState({
loading: true,
})
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
loading: false,
});
} else {
window.location = '?watch=' + this.props.streamName;
}
});
});
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
modal: null,
});
},
getDefaultProps: function() {
return {
icon: 'icon-play',
label: 'Watch',
}
},
render: function() {
return (
<span className="button-container">
<Link button={this.props.button} hidden={this.props.hidden} style={this.props.style}
disabled={this.state.loading} label={this.props.label} icon={this.props.icon}
onClick={this.handleClick} />
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
</span>
);
}
});

View file

@ -1,22 +1,7 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {BusyMessage, Icon} from './common.js'; import {BusyMessage, Icon} from './common.js';
import {Link} from '../component/link.js'
var loadScreenStyle = {
color: 'white',
backgroundImage: 'url(' + lbry.imagePath('lbry-bg.png') + ')',
backgroundSize: 'cover',
minHeight: '100vh',
minWidth: '100vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}, loadScreenMessageStyle = {
marginTop: '24px',
width: '325px',
textAlign: 'center',
};
var LoadScreen = React.createClass({ var LoadScreen = React.createClass({
propTypes: { propTypes: {
@ -24,6 +9,9 @@ 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,
@ -37,15 +25,18 @@ var LoadScreen = React.createClass({
} }
}, },
render: function() { render: function() {
var imgSrc = lbry.imagePath('lbry-white-485x160.png'); const imgSrc = lbry.imagePath('lbry-white-485x160.png');
return ( return (
<div className="load-screen" style={loadScreenStyle}> <div className="load-screen">
<img src={imgSrc} alt="LBRY"/> <img src={imgSrc} alt="LBRY"/>
<div style={loadScreenMessageStyle}> <div className="load-screen__message">
<h3> <h3>
<BusyMessage message={this.props.message} /> <BusyMessage message={this.props.message} />
</h3> </h3>
<Icon icon='icon-warning' style={this.props.isWarning ? {} : { display: 'none' }}/> <span style={ this.state.isWarning ? {} : {'color': '#c3c3c3'} }>{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>
{this.props.isWarning
? <div><Link label="Cancel" onClick={this.handleCancelClick} className='load-screen__cancel-link button-text' /></div>
: null}
</div> </div>
</div> </div>
); );

View file

@ -1,53 +1,8 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import {Icon} from './common.js'; import {Icon} from './common.js';
import {Link} from '../component/link.js';
// Generic menu styles export let DropDownMenuItem = React.createClass({
export let menuStyle = {
whiteSpace: 'nowrap'
};
export let Menu = React.createClass({
handleWindowClick: function(e) {
if (this.props.toggleButton && ReactDOM.findDOMNode(this.props.toggleButton).contains(e.target)) {
// Toggle button was clicked
this.setState({
open: !this.state.open
});
} else if (this.state.open && !this.refs.div.contains(e.target)) {
// Menu is open and user clicked outside of it
this.setState({
open: false
});
}
},
propTypes: {
openButton: React.PropTypes.element,
},
getInitialState: function() {
return {
open: false,
};
},
componentDidMount: function() {
window.addEventListener('click', this.handleWindowClick, false);
},
componentWillUnmount: function() {
window.removeEventListener('click', this.handleWindowClick, false);
},
render: function() {
return (
<div ref='div' style={menuStyle} className={this.state.open ? '' : 'hidden'}>
{this.props.children}
</div>
);
}
});
export let menuItemStyle = {
display: 'block',
};
export let MenuItem = React.createClass({
propTypes: { propTypes: {
href: React.PropTypes.string, href: React.PropTypes.string,
label: React.PropTypes.string, label: React.PropTypes.string,
@ -63,7 +18,7 @@ export let MenuItem = React.createClass({
var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null); var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null);
return ( return (
<a style={menuItemStyle} className="button-text no-underline" onClick={this.props.onClick} <a className="menu__menu-item" onClick={this.props.onClick}
href={this.props.href || 'javascript:'} label={this.props.label}> href={this.props.href || 'javascript:'} label={this.props.label}>
{this.props.iconPosition == 'left' ? icon : null} {this.props.iconPosition == 'left' ? icon : null}
{this.props.label} {this.props.label}
@ -72,3 +27,54 @@ export let MenuItem = React.createClass({
); );
} }
}); });
export let DropDownMenu = React.createClass({
_isWindowClickBound: false,
_menuDiv: null,
getInitialState: function() {
return {
menuOpen: false,
};
},
componentWillUnmount: function() {
if (this._isWindowClickBound) {
window.removeEventListener('click', this.handleWindowClick, false);
}
},
onMenuIconClick: function(e) {
this.setState({
menuOpen: !this.state.menuOpen,
});
if (!this.state.menuOpen && !this._isWindowClickBound) {
this._isWindowClickBound = true;
window.addEventListener('click', this.handleWindowClick, false);
e.stopPropagation();
}
return false;
},
handleWindowClick: function(e) {
if (this.state.menuOpen &&
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
this.setState({
menuOpen: false
});
}
},
render: function() {
if (!this.state.menuOpen && this._isWindowClickBound) {
this._isWindowClickBound = false;
window.removeEventListener('click', this.handleWindowClick, false);
}
return (
<div className="button-container">
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={this.onMenuIconClick} />
{this.state.menuOpen
? <div ref={(div) => this._menuDiv = div} className="menu">
{this.props.children}
</div>
: null}
</div>
);
}
});

View file

@ -14,24 +14,36 @@ var SplashScreen = React.createClass({
} }
}, },
updateStatus: function(was_lagging=false) { updateStatus: function(was_lagging=false) {
lbry.getDaemonStatus((status) => { lbry.getDaemonStatus(this._updateStatusCallback);
if (status.code == 'started') { },
this.props.onLoadDone(); _updateStatusCallback: function(status) {
return; if (status.code == 'started') {
} // Wait until we are able to resolve a name before declaring
// that we are done.
this.setState({ // TODO: This is a hack, and the logic should live in the daemon
details: status.message + (status.is_lagging ? '' : '...'), // to give us a better sense of when we are actually started
isLagging: status.is_lagging, this.setState({
}); details: 'Waiting for name resolution',
isLagging: false
setTimeout(() => {
this.updateStatus(status.is_lagging);
}, 500);
}); });
lbry.resolveName('one', () => {
this.props.onLoadDone();
});
return;
}
this.setState({
details: status.message + (status.is_lagging ? '' : '...'),
isLagging: status.is_lagging,
});
setTimeout(() => {
this.updateStatus(status.is_lagging);
}, 500);
}, },
componentDidMount: function() { componentDidMount: function() {
this.updateStatus(); lbry.connect((connected) => {
this.updateStatus();
});
}, },
render: function() { render: function() {
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />; return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />;

36
js/component/tooltip.js Normal file
View file

@ -0,0 +1,36 @@
import React from 'react';
export let ToolTip = React.createClass({
propTypes: {
body: React.PropTypes.string.isRequired,
label: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
showTooltip: false,
};
},
handleClick: function() {
this.setState({
showTooltip: !this.state.showTooltip,
});
},
handleTooltipMouseOut: function() {
this.setState({
showTooltip: false,
});
},
render: function() {
return (
<span className={'tooltip ' + (this.props.className || '')}>
<a className="tooltip__link" onClick={this.handleClick}>
{this.props.label}
</a>
<div className={'tooltip__body ' + (this.state.showTooltip ? '' : ' hidden')}
onMouseOut={this.handleTooltipMouseOut}>
{this.props.body}
</div>
</span>
);
}
});

View file

@ -5,11 +5,13 @@ var lbry = {
rootPath: '.', rootPath: '.',
daemonConnectionString: 'http://localhost:5279/lbryapi', daemonConnectionString: 'http://localhost:5279/lbryapi',
webUiUri: 'http://localhost:5279', webUiUri: 'http://localhost:5279',
peerListTimeout: 6000,
colors: { colors: {
primary: '#155B4A' primary: '#155B4A'
}, },
defaultClientSettings: { defaultClientSettings: {
showNsfw: false, showNsfw: false,
showUnavailable: true,
debug: false, debug: false,
useCustomLighthouseServers: false, useCustomLighthouseServers: false,
customLighthouseServers: [], customLighthouseServers: [],
@ -90,17 +92,18 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall
//core //core
lbry.connect = function(callback) lbry.connect = function(callback)
{ {
// Check every half second to see if the daemon's running. // Check every half second to see if the daemon is accepting connections
// Returns true to callback once connected, or false if it takes too long and we give up. // Once this returns True, can call getDaemonStatus to see where
function checkDaemonRunning(tryNum=0) { // we are in the startup process
lbry.daemonRunningStatus(function (runningStatus) { function checkDaemonStarted(tryNum=0) {
lbry.isDaemonAcceptingConnections(function (runningStatus) {
if (runningStatus) { if (runningStatus) {
lbry.isConnected = true; lbry.isConnected = true;
callback(true); callback(true);
} else { } else {
if (tryNum <= 600) { // Move # of tries into constant or config option if (tryNum <= 600) { // Move # of tries into constant or config option
setTimeout(function () { setTimeout(function () {
checkDaemonRunning(tryNum + 1); checkDaemonStarted(tryNum + 1);
}, 500); }, 500);
} else { } else {
callback(false); callback(false);
@ -108,16 +111,12 @@ lbry.connect = function(callback)
} }
}); });
} }
checkDaemonRunning(); checkDaemonStarted();
} }
lbry.daemonRunningStatus = function (callback) { lbry.isDaemonAcceptingConnections = function (callback) {
// Returns true/false whether the daemon is running (i.e. fully conncected to the network), // Returns true/false whether the daemon is at a point it will start returning status
// or null if the AJAX connection to the daemon fails. lbry.call('status', {}, () => callback(true), null, () => callback(false))
lbry.call('is_running', {}, callback, null, function () {
callback(null);
});
}; };
lbry.getDaemonStatus = function (callback) { lbry.getDaemonStatus = function (callback) {
@ -132,7 +131,7 @@ lbry.getNewAddress = function(callback) {
lbry.call('get_new_address', {}, callback); lbry.call('get_new_address', {}, callback);
} }
lbry.checkAddressIsMine = function(address, callback) { lbry.checkAddressIsMine = function(address, callback) {
lbry.call('address_is_mine', {address: address}, callback); lbry.call('address_is_mine', {address: address}, callback);
} }
@ -177,25 +176,38 @@ lbry.getClaimInfo = function(name, callback) {
} }
lbry.getMyClaim = function(name, callback) { lbry.getMyClaim = function(name, callback) {
lbry.call('get_my_claim', { name: name }, callback); lbry.call('claim_list_mine', {}, (claims) => {
callback(claims.find((claim) => claim.name == name) || null);
});
} }
lbry.getKeyFee = function(name, callback) { lbry.getKeyFee = function(name, callback, errorCallback) {
lbry.call('get_est_cost', { name: name }, callback); lbry.call('stream_cost_estimate', { name: name }, callback, errorCallback);
} }
lbry.getTotalCost = function(name, size, callback) { lbry.getTotalCost = function(name, size, callback, errorCallback) {
lbry.call('get_est_cost', { lbry.call('stream_cost_estimate', {
name: name, name: name,
size: size, size: size,
}, callback); }, callback, errorCallback);
} }
lbry.getPeersForBlobHash = function(blobHash, callback) { lbry.getPeersForBlobHash = function(blobHash, callback) {
lbry.call('get_peers_for_hash', { blob_hash: blobHash }, callback) let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
callback([]);
}, lbry.peerListTimeout);
lbry.call('peer_list', { blob_hash: blobHash }, function(peers) {
if (!timedOut) {
clearTimeout(timeout);
callback(peers);
}
});
} }
lbry.getCostInfoForName = function(name, callback) { lbry.getCostInfoForName = function(name, callback, errorCallback) {
/** /**
* Takes a LBRY name; will first try and calculate a total cost using * Takes a LBRY name; will first try and calculate a total cost using
* Lighthouse. If Lighthouse can't be reached, it just retrives the * Lighthouse. If Lighthouse can't be reached, it just retrives the
@ -206,30 +218,30 @@ lbry.getCostInfoForName = function(name, callback) {
* - includes_data: Boolean; indicates whether or not the data fee info * - includes_data: Boolean; indicates whether or not the data fee info
* from Lighthouse is included. * from Lighthouse is included.
*/ */
function getCostWithData(name, size, callback) { function getCostWithData(name, size, callback, errorCallback) {
lbry.getTotalCost(name, size, (cost) => { lbry.getTotalCost(name, size, (cost) => {
callback({ callback({
cost: cost, cost: cost,
includesData: true, includesData: true,
}); });
}); }, errorCallback);
} }
function getCostNoData(name, callback) { function getCostNoData(name, callback, errorCallback) {
lbry.getKeyFee(name, (cost) => { lbry.getKeyFee(name, (cost) => {
callback({ callback({
cost: cost, cost: cost,
includesData: false, includesData: false,
}); });
}); }, errorCallback);
} }
lighthouse.getSizeForName(name, (size) => { lighthouse.getSizeForName(name, (size) => {
getCostWithData(name, size, callback); getCostWithData(name, size, callback, errorCallback);
}, () => { }, () => {
getCostNoData(name, callback); getCostNoData(name, callback, errorCallback);
}, () => { }, () => {
getCostNoData(name, callback); getCostNoData(name, callback, errorCallback);
}); });
} }
@ -265,32 +277,39 @@ lbry.stopFile = function(name, callback) {
lbry.call('stop_lbry_file', { name: name }, callback); lbry.call('stop_lbry_file', { name: name }, callback);
} }
lbry.deleteFile = function(name, deleteTargetFile=true, callback) { lbry.removeFile = function(sdHash, name, deleteTargetFile=true, callback) { // Name param is temporary until the API can delete by unique ID (SD hash, claim ID etc.)
this._removedFiles.push(sdHash);
this._updateSubscribedFileInfo(sdHash);
lbry.call('delete_lbry_file', { lbry.call('delete_lbry_file', {
name: name, name: name,
delete_target_file: deleteTargetFile, delete_target_file: deleteTargetFile,
}, callback); }, callback);
} }
lbry.revealFile = function(path, callback) { lbry.openFile = function(sdHash, callback) {
lbry.call('reveal', { path: path }, callback); lbry.call('open', {sd_hash: sdHash}, callback);
}
lbry.revealFile = function(sdHash, callback) {
lbry.call('reveal', {sd_hash: sdHash}, callback);
} }
lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) { lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
// Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo(). // Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo().
// If timeoutCallback is provided, it will be called if the file fails to appear. // If timeoutCallback is provided, it will be called if the file fails to appear.
lbry.getFilesInfo(function(filesInfo) { lbry.getFilesInfo(function(fileInfos) {
for (var fileInfo of filesInfo) { for (var fileInfo of fileInfos) {
if (fileInfo.lbry_uri == name) { if (fileInfo.lbry_uri == name) {
callback(fileInfo); callback(fileInfo);
return; return;
} }
} }
if (tryNum <= 200) { if (timeoutCallback && tryNum > 200) {
setTimeout(function() { lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1) }, 250);
} else if (timeoutCallback) {
timeoutCallback(); timeoutCallback();
} else {
setTimeout(function() { lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1) }, 250);
} }
}); });
} }
@ -301,12 +320,7 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
// lbry.getFilesInfo() during the publish process. // lbry.getFilesInfo() during the publish process.
// Use ES6 named arguments instead of directly passing param dict? // Use ES6 named arguments instead of directly passing param dict?
lbry.call('publish', params, publishedCallback, (errorInfo) => { lbry.call('publish', params, publishedCallback, errorCallback);
errorCallback({
name: fault.fault,
message: fault.faultString,
});
});
if (fileListedCallback) { if (fileListedCallback) {
lbry.getFileInfoWhenListed(params.name, function(fileInfo) { lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
fileListedCallback(fileInfo); fileListedCallback(fileInfo);
@ -453,5 +467,64 @@ lbry.stop = function(callback) {
lbry.call('stop', {}, callback); lbry.call('stop', {}, callback);
}; };
lbry.fileInfo = {};
lbry._fileInfoSubscribeIdCounter = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000;
lbry._removedFiles = [];
lbry._claimIdOwnershipCache = {}; // should be claimId!!! But not
lbry._updateClaimOwnershipCache = function(claimId) {
lbry.getMyClaims((claimInfos) => {
lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) {
return match || claimInfo.claim_id == claimId;
});
});
};
lbry._updateSubscribedFileInfo = function(sdHash) {
const callSubscribedCallbacks = (sdHash, fileInfo) => {
for (let [subscribeId, callback] of Object.entries(this._fileInfoSubscribeCallbacks[sdHash])) {
callback(fileInfo);
}
}
if (lbry._removedFiles.includes(sdHash)) {
callSubscribedCallbacks(sdHash, false);
} else {
lbry.getFileInfoBySdHash(sdHash, (fileInfo) => {
if (fileInfo) {
if (this._claimIdOwnershipCache[fileInfo.claim_id] === undefined) {
this._updateClaimOwnershipCache(fileInfo.claim_id);
}
fileInfo.isMine = !!this._claimIdOwnershipCache[fileInfo.claim_id];
}
callSubscribedCallbacks(sdHash, fileInfo);
});
}
if (Object.keys(this._fileInfoSubscribeCallbacks[sdHash]).length) {
setTimeout(() => {
this._updateSubscribedFileInfo(sdHash);
}, lbry._fileInfoSubscribeInterval);
}
}
lbry.fileInfoSubscribe = function(sdHash, callback) {
if (!lbry._fileInfoSubscribeCallbacks[sdHash])
{
lbry._fileInfoSubscribeCallbacks[sdHash] = {};
}
const subscribeId = ++lbry._fileInfoSubscribeIdCounter;
lbry._fileInfoSubscribeCallbacks[sdHash][subscribeId] = callback;
lbry._updateSubscribedFileInfo(sdHash);
return subscribeId;
}
lbry.fileInfoUnsubscribe = function(name, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[name][subscribeId];
}
export default lbry; export default lbry;

View file

@ -133,7 +133,7 @@ var ClaimCodePage = React.createClass({
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed" <Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
onConfirmed={this.handleFinished}> onConfirmed={this.handleFinished}>
Your invite code has been redeemed. Your invite code has been redeemed. { ' ' }
{this.state.referralCredits > 0 {this.state.referralCredits > 0
? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits} ? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits}
will be added to your balance shortly.` will be added to your balance shortly.`

View file

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import {Link, ToolTipLink, DownloadLink, WatchLink} from '../component/link.js'; import {FileTile} from '../component/file-tile.js';
import {Thumbnail, CreditAmount, TruncatedText, BusyMessage} from '../component/common.js'; import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js';
var fetchResultsStyle = { var fetchResultsStyle = {
color: '#888', color: '#888',
@ -40,14 +42,15 @@ var SearchNoResults = React.createClass({
var SearchResults = React.createClass({ var SearchResults = React.createClass({
render: function() { render: function() {
var rows = []; var rows = [],
this.props.results.forEach(function(result) { seenNames = {}; //fix this when the search API returns claim IDs
console.log(result); this.props.results.forEach(function({name, value}) {
var mediaType = lbry.getMediaType(result.value.content_type); if (!seenNames[name]) {
rows.push( seenNames[name] = name;
<SearchResultRow key={result.name} name={result.name} title={result.value.title} imgUrl={result.value.thumbnail} rows.push(
description={result.value.description} nsfw={result.value.nsfw} mediaType={mediaType} /> <FileTile key={name} name={name} sdHash={value.sources.lbry_sd_hash} />
); );
}
}); });
return ( return (
<div>{rows}</div> <div>{rows}</div>
@ -55,180 +58,6 @@ var SearchResults = React.createClass({
} }
}); });
var
searchRowStyle = {
height: (24 * 7) + 'px',
overflowY: 'hidden'
},
searchRowCompactStyle = {
height: '180px',
},
searchRowImgStyle = {
maxWidth: '100%',
maxHeight: (24 * 7) + 'px',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto'
},
searchRowTitleStyle = {
fontWeight: 'bold'
},
searchRowTitleCompactStyle = {
fontSize: '1.25em',
lineHeight: '1.15',
},
searchRowCostStyle = {
float: 'right',
},
searchRowDescriptionStyle = {
color : '#444',
marginTop: '12px',
fontSize: '0.9em'
};
var SearchResultRow = React.createClass({
getInitialState: function() {
return {
downloading: false,
isHovered: false,
cost: null,
costIncludesData: null,
}
},
handleMouseOver: function() {
this.setState({
isHovered: true,
});
},
handleMouseOut: function() {
this.setState({
isHovered: false,
});
},
componentWillMount: function() {
if ('cost' in this.props) {
this.setState({
cost: this.props.cost,
costIncludesData: this.props.costIncludesData,
});
} else {
lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => {
this.setState({
cost: cost,
costIncludesData: includesData,
});
});
}
},
render: function() {
var obscureNsfw = !lbry.getClientSetting('showNsfw') && this.props.nsfw;
if (!this.props.compact) {
var style = searchRowStyle;
var titleStyle = searchRowTitleStyle;
} else {
var style = Object.assign({}, searchRowStyle, searchRowCompactStyle);
var titleStyle = Object.assign({}, searchRowTitleStyle, searchRowTitleCompactStyle);
}
return (
<section className={ 'card ' + (obscureNsfw ? 'card-obscured ' : '') + (this.props.compact ? 'card-compact' : '')} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className="row-fluid card-content" style={style}>
<div className="span3">
<a href={'/?show=' + this.props.name}><Thumbnail src={this.props.imgUrl} alt={'Photo for ' + (this.props.title || this.props.name)} style={searchRowImgStyle} /></a>
</div>
<div className="span9">
{this.state.cost !== null
? <span style={searchRowCostStyle}>
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
: null}
<div className="meta"><a href={'/?show=' + this.props.name}>lbry://{this.props.name}</a></div>
<h3 style={titleStyle}>
<a href={'/?show=' + this.props.name}>
<TruncatedText lines={3}>
{this.props.title}
</TruncatedText>
</a>
</h3>
<div>
{this.props.mediaType == 'video' ? <WatchLink streamName={this.props.name} button="primary" /> : null}
<DownloadLink streamName={this.props.name} button="text" />
</div>
<p style={searchRowDescriptionStyle}>
<TruncatedText lines={3}>
{this.props.description}
</TruncatedText>
</p>
</div>
</div>
{
!obscureNsfw || !this.state.isHovered ? null :
<div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link href="?settings" label="Settings" />.
</p>
</div>
}
</section>
);
}
});
var featuredContentItemContainerStyle = {
position: 'relative',
};
var FeaturedContentItem = React.createClass({
resolveSearch: false,
propTypes: {
name: React.PropTypes.string,
},
getInitialState: function() {
return {
metadata: null,
title: null,
cost: null,
overlayShowing: false,
};
},
componentWillUnmount: function() {
this.resolveSearch = false;
},
componentDidMount: function() {
this._isMounted = true;
lbry.resolveName(this.props.name, (metadata) => {
if (!this._isMounted) {
return;
}
this.setState({
metadata: metadata,
title: metadata && metadata.title ? metadata.title : ('lbry://' + this.props.name),
});
});
},
render: function() {
if (this.state.metadata === null) {
// Still waiting for metadata, skip render
return null;
}
return (<div style={featuredContentItemContainerStyle}>
<SearchResultRow name={this.props.name} title={this.state.title} imgUrl={this.state.metadata.thumbnail}
description={this.state.metadata.description} mediaType={lbry.getMediaType(this.state.metadata.content_type)}
nsfw={this.state.metadata.nsfw} compact />
</div>);
}
});
var featuredContentLegendStyle = { var featuredContentLegendStyle = {
fontSize: '12px', fontSize: '12px',
color: '#aaa', color: '#aaa',
@ -237,25 +66,30 @@ var featuredContentLegendStyle = {
var FeaturedContent = React.createClass({ var FeaturedContent = React.createClass({
render: function() { render: function() {
const toolTipText = ('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 ' +
'"five" to put your content here!');
return ( return (
<div className="row-fluid"> <div className="row-fluid">
<div className="span6"> <div className="span6">
<h3>Featured Content</h3> <h3>Featured Content</h3>
<FeaturedContentItem name="bellflower" /> <FileTile name="bellflower" />
<FeaturedContentItem name="itsadisaster" /> <FileTile name="itsadisaster" />
<FeaturedContentItem name="dopeman" /> <FileTile name="dopeman" />
<FeaturedContentItem name="smlawncare" /> <FileTile name="smlawncare" />
<FeaturedContentItem name="cinemasix" /> <FileTile name="cinemasix" />
</div> </div>
<div className="span6"> <div className="span6">
<h3>Community Content <ToolTipLink style={featuredContentLegendStyle} label="What's this?" <h3>
tooltip='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 "five" to put your content here!' /></h3> Community Content
<FeaturedContentItem name="one" /> <ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/>
<FeaturedContentItem name="two" /> </h3>
<FeaturedContentItem name="three" /> <FileTile name="one" />
<FeaturedContentItem name="four" /> <FileTile name="two" />
<FeaturedContentItem name="five" /> <FileTile name="three" />
<FileTile name="four" />
<FileTile name="five" />
</div> </div>
</div> </div>
); );

201
js/page/file-list.js Normal file
View file

@ -0,0 +1,201 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import FormField from '../component/form.js';
import {FileTileStream} from '../component/file-tile.js';
import {BusyMessage, Thumbnail} from '../component/common.js';
export let FileListDownloaded = React.createClass({
_isMounted: false,
getInitialState: function() {
return {
fileInfos: null,
};
},
componentDidMount: function() {
this._isMounted = true;
document.title = "Downloaded Files";
let publishedFilesSdHashes = [];
lbry.getMyClaims((claimInfos) => {
if (!this._isMounted) { return; }
for (let claimInfo of claimInfos) {
let metadata = JSON.parse(claimInfo.value);
publishedFilesSdHashes.push(metadata.sources.lbry_sd_hash);
}
lbry.getFilesInfo((fileInfos) => {
if (!this._isMounted) { return; }
this.setState({
fileInfos: fileInfos.filter(({sd_hash}) => {
return publishedFilesSdHashes.indexOf(sd_hash) == -1;
})
});
});
});
},
render: function() {
if (this.state.fileInfos === null) {
return (
<main className="page">
<BusyMessage message="Loading" />
</main>
);
} else if (!this.state.fileInfos.length) {
return (
<main className="page">
<span>You haven't downloaded anything from LBRY yet. Go <Link href="/" label="search for your first download" />!</span>
</main>
);
} else {
return (
<main className="page">
<FileList fileInfos={this.state.fileInfos} hidePrices={true} />
</main>
);
}
}
});
export let FileListPublished = React.createClass({
_isMounted: false,
getInitialState: function () {
return {
fileInfos: null,
};
},
componentDidMount: function () {
this._isMounted = true;
document.title = "Published Files";
lbry.getMyClaims((claimInfos) => {
/**
* Build newFileInfos as a sparse array and drop elements in at the same position they
* occur in claimInfos, so the order is preserved even if the API calls inside this loop
* return out of order.
*/
let newFileInfos = Array(claimInfos.length),
claimInfoProcessedCount = 0;
for (let [i, claimInfo] of claimInfos.entries()) {
let metadata = JSON.parse(claimInfo.value);
lbry.getFileInfoBySdHash(metadata.sources.lbry_sd_hash, (fileInfo) => {
claimInfoProcessedCount++;
if (fileInfo !== false) {
newFileInfos[i] = fileInfo;
}
if (claimInfoProcessedCount >= claimInfos.length) {
/**
* newfileInfos may have gaps from claims that don't have associated files in
* lbrynet, so filter out any missing elements
*/
this.setState({
fileInfos: newFileInfos.filter(function () {
return true
}),
});
}
});
}
});
},
render: function () {
if (this.state.fileInfos === null) {
return (
<main className="page">
<BusyMessage message="Loading" />
</main>
);
}
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>
);
}
else {
return (
<main className="page">
<FileList fileInfos={this.state.fileInfos} />
</main>
);
}
}
});
export let FileList = React.createClass({
_sortFunctions: {
date: function(fileInfos) {
return fileInfos.reverse();
},
title: function(fileInfos) {
return fileInfos.sort(function(a, b) {
return ((a.metadata ? a.metadata.title.toLowerCase() : a.name) >
(b.metadata ? b.metadata.title.toLowerCase() : b.name));
});
},
filename: function(fileInfos) {
return fileInfos.sort(function(a, b) {
return (a.file_name.toLowerCase() >
b.file_name.toLowerCase());
});
},
},
propTypes: {
fileInfos: React.PropTypes.array.isRequired,
hidePrices: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
hidePrices: false,
};
},
getInitialState: function() {
return {
sortBy: 'date',
};
},
handleSortChanged: function(event) {
this.setState({
sortBy: event.target.value,
});
},
render: function() {
var content = [],
seenUris = {};
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
for (let fileInfo of fileInfosSorted) {
let {lbry_uri, sd_hash, metadata} = fileInfo;
if (!metadata || seenUris[lbry_uri]) {
continue;
}
seenUris[lbry_uri] = true;
content.push(<FileTileStream key={lbry_uri} name={lbry_uri} hideOnRemove={true} sdHash={sd_hash}
hidePrice={this.props.hidePrices} metadata={metadata} />);
}
return (
<section>
<span className='sort-section'>
Sort by { ' ' }
<FormField type="select" onChange={this.handleSortChanged}>
<option value="date">Date</option>
<option value="title">Title</option>
<option value="filename">File name</option>
</FormField>
</span>
{content}
</section>
);
}
});

View file

@ -1,380 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link, WatchLink} from '../component/link.js';
import {Menu, MenuItem} from '../component/menu.js';
import FormField from '../component/form.js';
import Modal from '../component/modal.js';
import {BusyMessage, Thumbnail} from '../component/common.js';
var moreMenuStyle = {
position: 'absolute',
display: 'block',
top: '26px',
right: '13px',
};
var MyFilesRowMoreMenu = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
path: React.PropTypes.string.isRequired,
completed: React.PropTypes.bool.isRequired,
lbryUri: React.PropTypes.string.isRequired,
},
handleRevealClicked: function() {
lbry.revealFile(this.props.path);
},
handleRemoveClicked: function() {
lbry.deleteFile(this.props.lbryUri, false);
},
handleDeleteClicked: function() {
this.setState({
modal: 'confirmDelete',
});
},
handleDeleteConfirmed: function() {
lbry.deleteFile(this.props.lbryUri);
this.setState({
modal: null,
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
getInitialState: function() {
return {
modal: null,
};
},
render: function() {
return (
<div style={moreMenuStyle}>
<Menu {...this.props}>
<section className="card">
<MenuItem onClick={this.handleRevealClicked} label="Reveal file" /> {/* @TODO: Switch to OS specific wording */}
<MenuItem onClick={this.handleRemoveClicked} label="Remove from LBRY" />
<MenuItem onClick={this.handleDeleteClicked} label="Remove and delete file" />
</section>
</Menu>
<Modal isOpen={this.state.modal == 'confirmDelete'} contentLabel="Confirm delete" type="confirm" confirmButtonLabel="Delete File"
onConfirmed={this.handleDeleteConfirmed} onAborted={this.closeModal}>
Are you sure you'd like to delete <cite>{this.props.title}</cite>? This will {this.props.completed ? ' stop the download and ' : ''}
permanently remove the file from your system.
</Modal>
</div>
);
}
});
var moreButtonColumnStyle = {
height: '120px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
moreButtonContainerStyle = {
display: 'block',
position: 'relative',
},
moreButtonStyle = {
fontSize: '1.3em',
},
progressBarStyle = {
height: '15px',
width: '230px',
backgroundColor: '#444',
border: '2px solid #eee',
display: 'inline-block',
},
artStyle = {
maxHeight: '100px',
maxWidth: '100%',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
};
var MyFilesRow = React.createClass({
onPauseResumeClicked: function() {
if (this.props.stopped) {
lbry.startFile(this.props.lbryUri);
} else {
lbry.stopFile(this.props.lbryUri);
}
},
render: function() {
//@TODO: Convert progress bar to reusable component
var progressBarWidth = 230;
if (this.props.completed) {
var pauseLink = null;
var curProgressBarStyle = {display: 'none'};
} else {
var pauseLink = <Link icon={this.props.stopped ? 'icon-play' : 'icon-pause'}
label={this.props.stopped ? 'Resume download' : 'Pause download'}
onClick={() => { this.onPauseResumeClicked() }} />;
var curProgressBarStyle = Object.assign({}, progressBarStyle);
curProgressBarStyle.width = Math.floor(this.props.ratioLoaded * progressBarWidth) + 'px';
curProgressBarStyle.borderRightWidth = progressBarWidth - Math.ceil(this.props.ratioLoaded * progressBarWidth) + 2;
}
if (this.props.showWatchButton) {
var watchButton = <WatchLink streamName={this.props.lbryUri} />
} else {
var watchButton = null;
}
return (
<section className="card">
<div className="row-fluid">
<div className="span3">
<Thumbnail src={this.props.imgUrl} alt={'Photo for ' + this.props.title} style={artStyle} />
</div>
<div className="span8">
<h3>{this.props.pending ? this.props.title : <a href={'/?show=' + this.props.lbryUri}>{this.props.title}</a>}</h3>
{this.props.pending ? <em>This file is pending confirmation</em>
: (
<div>
<div className={this.props.completed ? 'hidden' : ''} style={curProgressBarStyle}></div>
{ ' ' }
{this.props.completed
? (this.props.isMine
? 'Published'
: 'Download complete')
: (parseInt(this.props.ratioLoaded * 100) + '%')}
<div>{ pauseLink }</div>
<div>{ watchButton }</div>
</div>
)
}
</div>
<div className="span1" style={moreButtonColumnStyle}>
{this.props.pending ? null :
<div style={moreButtonContainerStyle}>
<Link style={moreButtonStyle} ref="moreButton" icon="icon-ellipsis-h" title="More Options" />
<MyFilesRowMoreMenu toggleButton={this.refs.moreButton} title={this.props.title}
completed={this.props.completed} lbryUri={this.props.lbryUri}
fileName={this.props.fileName} path={this.props.path}/>
</div>
}
</div>
</div>
</section>
);
}
});
var MyFilesPage = React.createClass({
_fileTimeout: null,
_fileInfoCheckRate: 300,
_fileInfoCheckNum: 0,
_sortFunctions: {
date: function(filesInfo) {
return filesInfo.reverse();
},
title: function(filesInfo) {
return filesInfo.sort(function(a, b) {
return ((a.metadata ? a.metadata.title.toLowerCase() : a.name) >
(b.metadata ? b.metadata.title.toLowerCase() : b.name));
});
},
filename: function(filesInfo) {
return filesInfo.sort(function(a, b) {
return (a.file_name.toLowerCase() >
b.file_name.toLowerCase());
});
},
},
getInitialState: function() {
return {
filesInfo: null,
publishedFilesSdHashes: null,
filesAvailable: {},
sortBy: 'date',
};
},
getDefaultProps: function() {
return {
show: null,
};
},
componentDidMount: function() {
document.title = "My Files";
},
componentWillMount: function() {
if (this.props.show == 'downloaded') {
this.getPublishedFilesSdHashes(() => {
this.updateFilesInfo();
});
} else {
this.updateFilesInfo();
}
},
getPublishedFilesSdHashes: function(callback) {
// Determines which files were published by the user and saves their SD hashes in
// this.state.publishedFilesSdHashes. Used on the Downloads page to filter out claims published
// by the user.
var publishedFilesSdHashes = [];
lbry.getMyClaims((claimsInfo) => {
for (let claimInfo of claimsInfo) {
let metadata = JSON.parse(claimInfo.value);
publishedFilesSdHashes.push(metadata.sources.lbry_sd_hash);
}
this.setState({
publishedFilesSdHashes: publishedFilesSdHashes,
});
callback();
});
},
componentWillUnmount: function() {
if (this._fileTimeout)
{
clearTimeout(this._fileTimeout);
}
},
handleSortChanged: function(event) {
this.setState({
sortBy: event.target.value,
});
},
updateFilesInfo: function() {
this._fileInfoCheckNum += 1;
if (this.props.show == 'published') {
// We're in the Published tab, so populate this.state.filesInfo with data from the user's claims
lbry.getMyClaims((claimsInfo) => {
/**
* Build newFilesInfo as a sparse array and drop elements in at the same position they
* occur in claimsInfo, so the order is preserved even if the API calls inside this loop
* return out of order.
*/
let newFilesInfo = Array(claimsInfo.length);
let claimInfoProcessedCount = 0;
for (let [i, claimInfo] of claimsInfo.entries()) {
let metadata = JSON.parse(claimInfo.value);
lbry.getFileInfoBySdHash(metadata.sources.lbry_sd_hash, (fileInfo) => {
claimInfoProcessedCount++;
if (fileInfo !== false) {
newFilesInfo[i] = fileInfo;
}
if (claimInfoProcessedCount >= claimsInfo.length) {
/**
* newFilesInfo may have gaps from claims that don't have associated files in
* lbrynet, so filter out any missing elements
*/
this.setState({
filesInfo: newFilesInfo.filter(function() { return true }),
});
this._fileTimeout = setTimeout(() => { this.updateFilesInfo() }, 1000);
}
});
}
});
} else {
// We're in the Downloaded tab, so populate this.state.filesInfo with files the user has in
// lbrynet, with published files filtered out.
lbry.getFilesInfo((filesInfo) => {
this.setState({
filesInfo: filesInfo.filter(({sd_hash}) => {
return this.state.publishedFilesSdHashes.indexOf(sd_hash) == -1;
}),
});
let newFilesAvailable;
if (!(this._fileInfoCheckNum % this._fileInfoCheckRate)) {
// Time to update file availability status
newFilesAvailable = {};
let filePeersCheckCount = 0;
for (let {sd_hash} of filesInfo) {
lbry.getPeersForBlobHash(sd_hash, (peers) => {
filePeersCheckCount++;
newFilesAvailable[sd_hash] = peers.length >= 0;
if (filePeersCheckCount >= filesInfo.length) {
this.setState({
filesAvailable: newFilesAvailable,
});
}
});
}
}
this._fileTimeout = setTimeout(() => { this.updateFilesInfo() }, 1000);
})
}
},
render: function() {
if (this.state.filesInfo === null || (this.props.show == 'downloaded' && this.state.publishedFileSdHashes === null)) {
return (
<main className="page">
<BusyMessage message="Loading" />
</main>
);
} else if (!this.state.filesInfo.length) {
return (
<main className="page">
{this.props.show == 'downloaded'
? <span>You haven't downloaded anything from LBRY yet. Go <Link href="/" label="search for your first download" />!</span>
: <span>You haven't published anything to LBRY yet.</span>}
</main>
);
} else {
var content = [],
seenUris = {};
const filesInfoSorted = this._sortFunctions[this.state.sortBy](this.state.filesInfo);
for (let fileInfo of filesInfoSorted) {
let {completed, written_bytes, total_bytes, lbry_uri, file_name, download_path,
stopped, metadata, sd_hash} = fileInfo;
if (!metadata || seenUris[lbry_uri]) {
continue;
}
seenUris[lbry_uri] = true;
let {title, thumbnail} = metadata;
if (!fileInfo.pending && typeof metadata == 'object') {
var {title, thumbnail} = metadata;
var pending = false;
} else {
var title = null;
var thumbnail = null;
var pending = true;
}
var ratioLoaded = written_bytes / total_bytes;
var mediaType = lbry.getMediaType(metadata.content_type, file_name);
var showWatchButton = (mediaType == 'video');
content.push(<MyFilesRow key={lbry_uri} lbryUri={lbry_uri} title={title || ('lbry://' + lbry_uri)} completed={completed} stopped={stopped}
ratioLoaded={ratioLoaded} imgUrl={thumbnail} path={download_path}
showWatchButton={showWatchButton} pending={pending}
available={this.state.filesAvailable[sd_hash]} isMine={this.props.show == 'published'} />);
}
}
return (
<main className="page">
<span className='sort-section'>
Sort by { ' ' }
<FormField type="select" onChange={this.handleSortChanged}>
<option value="date">Date</option>
<option value="title">Title</option>
<option value="filename">File name</option>
</FormField>
</span>
{content}
</main>
);
}
});
export default MyFilesPage;

View file

@ -91,8 +91,7 @@ var PublishPage = React.createClass({
if (this.refs.file.getValue() !== '') { if (this.refs.file.getValue() !== '') {
publishArgs.file_path = this._tempFilePath; publishArgs.file_path = this._tempFilePath;
} }
console.log(publishArgs);
lbry.publish(publishArgs, (message) => { lbry.publish(publishArgs, (message) => {
this.handlePublishStarted(); this.handlePublishStarted();
}, null, (error) => { }, null, (error) => {
@ -198,15 +197,14 @@ var PublishPage = React.createClass({
return; return;
} }
var topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount); const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount);
const newState = {
var newState = {
name: name, name: name,
nameResolved: true, nameResolved: true,
topClaimValue: parseFloat(claimInfo.amount), topClaimValue: parseFloat(claimInfo.amount),
myClaimExists: !!myClaimInfo, myClaimExists: !!myClaimInfo,
myClaimValue: parseFloat(myClaimInfo.amount), myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null,
myClaimMetadata: myClaimInfo.value, myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
topClaimIsMine: topClaimIsMine, topClaimIsMine: topClaimIsMine,
}; };

View file

@ -114,7 +114,7 @@ var ReferralPage = React.createClass({
? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!` ? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!`
: 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'} : 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={failureReason} <Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>
{this.state.failureReason} {this.state.failureReason}
</Modal> </Modal>

View file

@ -51,7 +51,8 @@ var SettingsPage = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
settings: null, settings: null,
showNsfw: lbry.getClientSetting('showNsfw') showNsfw: lbry.getClientSetting('showNsfw'),
showUnavailable: lbry.getClientSetting('showUnavailable'),
} }
}, },
componentDidMount: function() { componentDidMount: function() {
@ -69,6 +70,9 @@ var SettingsPage = React.createClass({
onShowNsfwChange: function(event) { onShowNsfwChange: function(event) {
lbry.setClientSetting('showNsfw', event.target.checked); lbry.setClientSetting('showNsfw', event.target.checked);
}, },
onShowUnavailableChange: function(event) {
lbry.setClientSetting('showUnavailable', event.target.checked);
},
render: function() { render: function() {
if (!this.state.daemonSettings) { if (!this.state.daemonSettings) {
return null; return null;
@ -114,7 +118,7 @@ var SettingsPage = React.createClass({
<h3>Content</h3> <h3>Content</h3>
<div className="form-row"> <div className="form-row">
<label style={settingsCheckBoxOptionStyles}> <label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW Content <input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content
</label> </label>
<div className="help"> <div className="help">
NSFW content may include nudity, intense sexuality, profanity, or other adult content. NSFW content may include nudity, intense sexuality, profanity, or other adult content.
@ -122,6 +126,17 @@ var SettingsPage = React.createClass({
</div> </div>
</div> </div>
</section> </section>
<section className="card">
<h3>Search</h3>
<div className="form-row">
<div className="help">
Would you like search results to include items that are not currently available for download?
</div>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} /> Show unavailable content in search results
</label>
</div>
</section>
<section className="card"> <section className="card">
<h3>Share Diagnostic Data</h3> <h3>Share Diagnostic Data</h3>
<label style={settingsCheckBoxOptionStyles}> <label style={settingsCheckBoxOptionStyles}>

View file

@ -2,7 +2,8 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import {CreditAmount, Thumbnail} from '../component/common.js'; import {CreditAmount, Thumbnail} from '../component/common.js';
import {Link, DownloadLink, WatchLink} from '../component/link.js'; import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js';
var formatItemImgStyle = { var formatItemImgStyle = {
maxWidth: '100%', maxWidth: '100%',
@ -21,7 +22,6 @@ var FormatItem = React.createClass({
costIncludesData: React.PropTypes.bool, costIncludesData: React.PropTypes.bool,
}, },
render: function() { render: function() {
var claimInfo = this.props.claimInfo; var claimInfo = this.props.claimInfo;
var thumbnail = claimInfo.thumbnail; var thumbnail = claimInfo.thumbnail;
var title = claimInfo.title; var title = claimInfo.title;
@ -62,10 +62,7 @@ var FormatItem = React.createClass({
</tbody> </tbody>
</table> </table>
</section> </section>
<section> <FileActions streamName={this.props.name} sdHash={claimInfo.sources.lbry_sd_hash} metadata={claimInfo} />
{mediaType == 'video' ? <WatchLink streamName={this.props.name} button="primary" /> : null}
<DownloadLink streamName={this.props.name} button="alt" />
</section>
<section> <section>
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" /> <Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</section> </section>

View file

@ -22,7 +22,7 @@
"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": "^1.0.1", "clamp-js": "^0.7.0",
"mediaelement": "^2.23.4", "mediaelement": "^2.23.4",
"node-sass": "^3.8.0", "node-sass": "^3.8.0",
"react": "^15.4.0", "react": "^15.4.0",
@ -34,9 +34,10 @@
"babel-core": "^6.18.2", "babel-core": "^6.18.2",
"babel-loader": "^6.2.8", "babel-loader": "^6.2.8",
"babel-plugin-react-require": "^3.0.0", "babel-plugin-react-require": "^3.0.0",
"babel-polyfill": "^6.20.0",
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0", "babel-preset-react": "^6.16.0",
"babel-polyfill": "^6.20.0", "babel-preset-stage-2": "^6.18.0",
"eslint": "^3.10.2", "eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0", "eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1", "eslint-loader": "^1.6.1",

View file

@ -8,7 +8,7 @@ html
body body
{ {
font-family: 'Source Sans Pro', sans-serif; font-family: 'Source Sans Pro', sans-serif;
line-height: 1.3333; line-height: $font-line-height;
} }
$drawer-width: 240px; $drawer-width: 240px;
@ -56,7 +56,7 @@ $drawer-width: 240px;
#drawer-handle #drawer-handle
{ {
padding: $spacing-vertical / 2; padding: $spacing-vertical / 2;
max-height: $header-height - $spacing-vertical; max-height: $height-header - $spacing-vertical;
text-align: center; text-align: center;
} }
@ -76,10 +76,10 @@ $drawer-width: 240px;
background: $color-primary; background: $color-primary;
color: white; color: white;
&.header-no-subnav { &.header-no-subnav {
height: $header-height; height: $height-header;
} }
&.header-with-subnav { &.header-with-subnav {
height: $header-height * 2; height: $height-header * 2;
} }
position: fixed; position: fixed;
top: 0; top: 0;
@ -87,7 +87,7 @@ $drawer-width: 240px;
width: 100%; width: 100%;
z-index: 2; z-index: 2;
box-sizing: border-box; box-sizing: border-box;
h1 { font-size: 1.8em; line-height: $header-height - $spacing-vertical; display: inline-block; float: left; } h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; }
&.header-scrolled &.header-scrolled
{ {
box-shadow: $default-box-shadow; box-shadow: $default-box-shadow;
@ -120,7 +120,7 @@ nav.sub-header
display: inline-block; display: inline-block;
margin: 0 15px; margin: 0 15px;
padding: 0 5px; padding: 0 5px;
line-height: $header-height - $spacing-vertical - $sub-header-selected-underline-height; line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
color: #e8e8e8; color: #e8e8e8;
&:first-child &:first-child
{ {
@ -147,13 +147,13 @@ nav.sub-header
background: $color-canvas; background: $color-canvas;
&.no-sub-nav &.no-sub-nav
{ {
min-height: calc(100vh - 60px); //should be -$header-height, but I'm dumb I guess? It wouldn't work min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $header-height; } main { margin-top: $height-header; }
} }
&.with-sub-nav &.with-sub-nav
{ {
min-height: calc(100vh - 120px); //should be -$header-height, but I'm dumb I guess? It wouldn't work min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $header-height * 2; } main { margin-top: $height-header * 2; }
} }
main main
{ {
@ -206,9 +206,6 @@ $header-icon-size: 1.5em;
box-shadow: $default-box-shadow; box-shadow: $default-box-shadow;
border-radius: 2px; border-radius: 2px;
} }
.card-compact {
padding: 22px;
}
.card-obscured .card-obscured
{ {
position: relative; position: relative;

View file

@ -2,22 +2,31 @@
$spacing-vertical: 24px; $spacing-vertical: 24px;
$padding-button: 12px;
$padding-text-link: 4px;
$color-primary: #155B4A; $color-primary: #155B4A;
$color-light-alt: hsl(hue($color-primary), 15, 85); $color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000; $color-text-dark: #000;
$color-help: rgba(0,0,0,.6); $color-help: rgba(0,0,0,.6);
$color-notice: #921010;
$color-warning: #ffffff;
$color-load-screen-text: #c3c3c3;
$color-canvas: #f5f5f5; $color-canvas: #f5f5f5;
$color-bg: #ffffff; $color-bg: #ffffff;
$color-bg-alt: #D9D9D9;
$color-money: #216C2A; $color-money: #216C2A;
$color-meta-light: #505050; $color-meta-light: #505050;
$font-size: 16px; $font-size: 16px;
$font-line-height: 1.3333;
$mobile-width-threshold: 801px; $mobile-width-threshold: 801px;
$max-content-width: 1000px; $max-content-width: 1000px;
$max-text-width: 660px; $max-text-width: 660px;
$header-height: $spacing-vertical * 2.5; $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5;
$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); $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);

View file

@ -1,7 +1,7 @@
@import "global"; @import "global";
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) { @mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
color: $color;
.icon .icon
{ {
&:first-child { &:first-child {
@ -27,6 +27,9 @@
text-decoration: none; text-decoration: none;
} }
} }
color: $color;
cursor: pointer;
} }
.icon-fixed-width { .icon-fixed-width {
@ -138,18 +141,20 @@ input[type="text"], input[type="search"]
} }
.button-container { .button-container {
position: relative;
display: inline-block;
+ .button-container + .button-container
{ {
margin-left: 12px; margin-left: $padding-button;
} }
} }
.button-block .button-block, .faux-button-block
{ {
cursor: pointer;
display: inline-block; display: inline-block;
height: $spacing-vertical * 1.5; height: $height-button;
line-height: $spacing-vertical * 1.5; line-height: $height-button;
text-decoration: none; text-decoration: none;
border: 0 none; border: 0 none;
text-align: center; text-align: center;
@ -168,31 +173,39 @@ input[type="text"], input[type="search"]
padding-left: 5px; padding-left: 5px;
} }
} }
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary .button-primary
{ {
color: white; color: white;
background-color: $color-primary; background-color: $color-primary;
box-shadow: $default-box-shadow; box-shadow: $default-box-shadow;
padding: 0 12px;
} }
.button-alt .button-alt
{ {
background-color: rgba(0,0,0,.15); background-color: $color-bg-alt;
box-shadow: $default-box-shadow; box-shadow: $default-box-shadow;
padding: 0 12px;
}
.button-cancel
{
padding: 0 12px;
} }
.button-text .button-text
{ {
@include text-link(); @include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
} }
.button-text-help .button-text-help
{ {
@include text-link(#5b8c80); @include text-link(#aaa);
font-size: 0.8em; font-size: 0.8em;
} }
@ -338,21 +351,10 @@ input[type="text"], input[type="search"]
margin: 0px 6px; margin: 0px 6px;
} }
.error-modal__error-list {
border: 1px solid #eee;
padding: 8px;
list-style: none;
}
.error-modal-overlay { .error-modal-overlay {
background: rgba(#000, .88); background: rgba(#000, .88);
} }
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__content { .error-modal__content {
display: flex; display: flex;
padding: 0px 8px 10px 10px; padding: 0px 8px 10px 10px;
@ -367,3 +369,15 @@ input[type="text"], input[type="search"]
word-break: break-all; word-break: break-all;
} }
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -3,6 +3,11 @@
@import "_icons"; @import "_icons";
@import "_mediaelement"; @import "_mediaelement";
@import "_canvas"; @import "_canvas";
@import "_table";
@import "_gui"; @import "_gui";
@import "component/_table";
@import "component/_file-actions.scss";
@import "component/_file-tile.scss";
@import "component/_menu.scss";
@import "component/_tooltip.scss";
@import "component/_load-screen.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";

View file

@ -0,0 +1,32 @@
@import "../global";
$color-download: #444;
.file-actions
{
line-height: $height-button;
min-height: $height-button;
}
.file-actions__download-status-bar, .file-actions__download-status-bar-overlay {
.button__content {
margin: 0 $padding-text-link;
}
}
.file-actions__download-status-bar
{
position: relative;
color: $color-download;
}
.file-actions__download-status-bar-overlay
{
background: $color-download;
color: white;
position: absolute;
white-space: nowrap;
overflow: hidden;
z-index: 1;
top: 0px;
left: 0px;
}

View file

@ -0,0 +1,31 @@
@import "../global";
.file-tile__row {
height: $spacing-vertical * 7;
}
.file-tile__row--unavailable {
opacity: 0.5;
}
.file-tile__thumbnail {
max-width: 100%;
max-height: $spacing-vertical * 7;
display: block;
margin-left: auto;
margin-right: auto;
}
.file-tile__title {
font-weight: bold;
}
.file-tile__cost {
float: right;
}
.file-tile__description {
color: #444;
margin-top: 12px;
font-size: 0.9em;
}

View file

@ -0,0 +1,31 @@
@import "../global";
.load-screen {
color: white;
background-image: url("/img/lbry-bg.png");
background-size: cover;
min-height: 100vh;
min-width: 100vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.load-screen__message {
margin-top: 24px;
width: 325px;
text-align: center;
}
.load-screen__details {
color: $color-load-screen-text;
}
.load-screen__details--warning {
color: $color-warning;
}
.load-screen__cancel-link {
color: white;
}

22
scss/component/_menu.scss Normal file
View file

@ -0,0 +1,22 @@
@import "../global";
$border-radius-menu: 2px;
.menu {
position: absolute;
white-space: nowrap;
background-color: white;
box-shadow: $default-box-shadow;
border-radius: $border-radius-menu;
padding-top: $spacing-vertical / 2;
padding-bottom: $spacing-vertical / 2;
z-index: 1;
}
.menu__menu-item {
display: block;
padding: $spacing-vertical / 4 $spacing-vertical / 2;
&:hover {
background: $color-bg-alt;
}
}

View file

@ -1,3 +1,5 @@
@import "../global";
table.table-standard { table.table-standard {
word-wrap: break-word; word-wrap: break-word;
max-width: 100%; max-width: 100%;

View file

@ -0,0 +1,35 @@
@import "../global";
.tooltip {
position: relative;
}
.tooltip__link {
@include text-link();
}
.tooltip__body {
$tooltip-body-width: 300px;
position: absolute;
z-index: 1;
left: 50%;
margin-left: $tooltip-body-width * -1 / 2;
box-sizing: border-box;
padding: $spacing-vertical / 2;
width: $tooltip-body-width;
border: 1px solid #aaa;
color: $color-text-dark;
background-color: $color-bg;
font-size: $font-size * 7/8;
line-height: $font-line-height;
box-shadow: $default-box-shadow;
}
.tooltip--header .tooltip__link {
@include text-link(#aaa);
font-size: $font-size * 3/4;
margin-left: $padding-button;
vertical-align: middle;
}

View file

@ -25,12 +25,12 @@ module.exports = {
loaders: [ loaders: [
{ test: /\.css$/, loader: "style!css" }, { test: /\.css$/, loader: "style!css" },
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
// Enable caching for improved performance during development loader: 'babel',
// It uses default OS directory by default. If you need query: {
// something more custom, pass a path to it. cacheDirectory: true,
// I.e., babel?cacheDirectory=<path> presets:[ 'es2015', 'react', 'stage-2' ]
loader: 'babel?cacheDirectory' }
} }
] ]
} }