commit
181d267bd2
33 changed files with 1294 additions and 1030 deletions
33
README.md
33
README.md
|
@ -1,23 +1,28 @@
|
|||
# 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
|
||||
|
||||
These steps will get you to change-reload-see:
|
||||
|
||||
- Install [LBRY](https://github.com/lbryio/lbry/releases)
|
||||
- Install node and npm ([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`
|
||||
- Run `lbrynet-daemon --ui=/full/path/to/dist/` to start LBRY
|
||||
- `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.
|
||||
- `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
|
||||
- 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))
|
||||
- Checkout this project via git
|
||||
- Run `./watch.sh` (this will `npm install` dependencies)
|
||||
- Run LBRY
|
||||
|
||||
## Common Issues
|
||||
1. Error: Couldn't find preset "es2015" relative to directory "js"
|
||||
While `watch.sh` is running, any change made to the `js` or `scss` folders will automatically be compiled into the `dist` folder.
|
||||
|
||||
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
|
||||
|
|
11
js/app.js
11
js/app.js
|
@ -4,7 +4,6 @@ import SettingsPage from './page/settings.js';
|
|||
import HelpPage from './page/help.js';
|
||||
import WatchPage from './page/watch.js';
|
||||
import ReportPage from './page/report.js';
|
||||
import MyFilesPage from './page/my_files.js';
|
||||
import StartPage from './page/start.js';
|
||||
import ClaimCodePage from './page/claim_code.js';
|
||||
import ReferralPage from './page/referral.js';
|
||||
|
@ -14,6 +13,7 @@ import PublishPage from './page/publish.js';
|
|||
import DiscoverPage from './page/discover.js';
|
||||
import SplashScreen from './component/splash.js';
|
||||
import DeveloperPage from './page/developer.js';
|
||||
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
|
||||
import Drawer from './component/drawer.js';
|
||||
import Header from './component/header.js';
|
||||
import Modal from './component/modal.js';
|
||||
|
@ -164,9 +164,9 @@ var App = React.createClass({
|
|||
case 'report':
|
||||
return <ReportPage />;
|
||||
case 'downloaded':
|
||||
return <MyFilesPage show="downloaded" />;
|
||||
return <FileListDownloaded />;
|
||||
case 'published':
|
||||
return <MyFilesPage show="published" />;
|
||||
return <FileListPublished />;
|
||||
case 'start':
|
||||
return <StartPage />;
|
||||
case 'claim':
|
||||
|
@ -190,7 +190,8 @@ var App = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
var mainContent = this.getMainContent(),
|
||||
headerLinks = this.getHeaderLinks();
|
||||
headerLinks = this.getHeaderLinks(),
|
||||
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
|
||||
|
||||
return (
|
||||
this.state.viewingPage == 'watch' ?
|
||||
|
@ -198,7 +199,7 @@ var App = React.createClass({
|
|||
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
|
||||
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
|
||||
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
|
||||
<Header onOpenDrawer={this.openDrawer} 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}
|
||||
</div>
|
||||
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import $clamp from 'clamp';
|
||||
import $clamp from 'clamp-js';
|
||||
|
||||
//component/icon.js
|
||||
export let Icon = React.createClass({
|
||||
propTypes: {
|
||||
style: React.PropTypes.object,
|
||||
fixed: React.PropTypes.bool,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
className: React.PropTypes.string,
|
||||
fixed: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
var className = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + this.props.icon + ' ' +
|
||||
(this.props.className || ''));
|
||||
return <span className={className} style={this.props.style}></span>
|
||||
const {fixed, className, ...other} = this.props;
|
||||
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
|
||||
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 = {
|
||||
color: '#216C2A',
|
||||
fontWeight: 'bold',
|
||||
|
@ -123,7 +99,7 @@ export let Thumbnail = React.createClass({
|
|||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
src: React.PropTypes.string,
|
||||
},
|
||||
handleError: function() {
|
||||
if (this.state.imageUrl != this._defaultImageUri) {
|
||||
|
@ -151,6 +127,6 @@ export let Thumbnail = React.createClass({
|
|||
this._isMounted = false;
|
||||
},
|
||||
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} />
|
||||
},
|
||||
});
|
||||
|
|
287
js/component/file-actions.js
Normal file
287
js/component/file-actions.js
Normal 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
198
js/component/file-tile.js
Normal 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} />;
|
||||
}
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import {Icon} from './common.js';
|
||||
|
||||
var requiredFieldWarningStyle = {
|
||||
color: '#cc0000',
|
||||
|
|
|
@ -52,7 +52,7 @@ var Header = React.createClass({
|
|||
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
|
||||
<h1>{ this.state.title }</h1>
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -70,7 +70,7 @@ var SubHeader = React.createClass({
|
|||
render: function() {
|
||||
var links = [],
|
||||
viewingUrl = '?' + this.props.viewingPage;
|
||||
|
||||
|
||||
for (let link of Object.keys(this.props.links)) {
|
||||
links.push(
|
||||
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>
|
||||
|
|
|
@ -1,221 +1,55 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import Modal from './modal.js';
|
||||
import {Icon, ToolTip} from './common.js';
|
||||
|
||||
import {Icon} from './common.js';
|
||||
|
||||
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) {
|
||||
this.props.onClick();
|
||||
this.props.onClick(e);
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var href = this.props.href ? this.props.href : 'javascript:;',
|
||||
icon = this.props.icon ? <Icon icon={this.props.icon} fixed={true} /> : '',
|
||||
className = (this.props.className ? this.props.className : '') +
|
||||
(this.props.button ? ' button-block button-' + this.props.button : '') +
|
||||
(this.props.hidden ? ' hidden' : '') +
|
||||
(this.props.disabled ? ' disabled' : '');
|
||||
if (this.props.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* The way the class name is generated here is a mess -- refactor */
|
||||
|
||||
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 (
|
||||
<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 : '' }
|
||||
<span className="link-label">{this.props.label}</span>
|
||||
{this.props.badge ? <span className="badge">{this.props.badge}</span> : '' }
|
||||
<a className={className} href={this.props.href || 'javascript:;'} title={this.props.title}
|
||||
onClick={this.handleClick} {... 'style' in this.props ? {style: this.props.style} : {}}>
|
||||
{content}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,22 +1,7 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import {BusyMessage, Icon} from './common.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',
|
||||
};
|
||||
import {Link} from '../component/link.js'
|
||||
|
||||
var LoadScreen = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -24,6 +9,9 @@ var LoadScreen = React.createClass({
|
|||
details: React.PropTypes.string,
|
||||
isWarning: React.PropTypes.bool,
|
||||
},
|
||||
handleCancelClick: function() {
|
||||
history.back();
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
isWarning: false,
|
||||
|
@ -37,15 +25,18 @@ var LoadScreen = React.createClass({
|
|||
}
|
||||
},
|
||||
render: function() {
|
||||
var imgSrc = lbry.imagePath('lbry-white-485x160.png');
|
||||
const imgSrc = lbry.imagePath('lbry-white-485x160.png');
|
||||
return (
|
||||
<div className="load-screen" style={loadScreenStyle}>
|
||||
<div className="load-screen">
|
||||
<img src={imgSrc} alt="LBRY"/>
|
||||
<div style={loadScreenMessageStyle}>
|
||||
<div className="load-screen__message">
|
||||
<h3>
|
||||
<BusyMessage message={this.props.message} />
|
||||
</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>
|
||||
);
|
||||
|
|
|
@ -1,53 +1,8 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Icon} from './common.js';
|
||||
import {Link} from '../component/link.js';
|
||||
|
||||
// Generic menu styles
|
||||
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({
|
||||
export let DropDownMenuItem = React.createClass({
|
||||
propTypes: {
|
||||
href: 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);
|
||||
|
||||
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}>
|
||||
{this.props.iconPosition == 'left' ? icon : null}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -14,24 +14,36 @@ var SplashScreen = React.createClass({
|
|||
}
|
||||
},
|
||||
updateStatus: function(was_lagging=false) {
|
||||
lbry.getDaemonStatus((status) => {
|
||||
if (status.code == 'started') {
|
||||
this.props.onLoadDone();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
details: status.message + (status.is_lagging ? '' : '...'),
|
||||
isLagging: status.is_lagging,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateStatus(status.is_lagging);
|
||||
}, 500);
|
||||
lbry.getDaemonStatus(this._updateStatusCallback);
|
||||
},
|
||||
_updateStatusCallback: function(status) {
|
||||
if (status.code == 'started') {
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
// to give us a better sense of when we are actually started
|
||||
this.setState({
|
||||
details: 'Waiting for name resolution',
|
||||
isLagging: false
|
||||
});
|
||||
|
||||
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() {
|
||||
this.updateStatus();
|
||||
lbry.connect((connected) => {
|
||||
this.updateStatus();
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />;
|
||||
|
|
36
js/component/tooltip.js
Normal file
36
js/component/tooltip.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
159
js/lbry.js
159
js/lbry.js
|
@ -5,11 +5,13 @@ var lbry = {
|
|||
rootPath: '.',
|
||||
daemonConnectionString: 'http://localhost:5279/lbryapi',
|
||||
webUiUri: 'http://localhost:5279',
|
||||
peerListTimeout: 6000,
|
||||
colors: {
|
||||
primary: '#155B4A'
|
||||
},
|
||||
defaultClientSettings: {
|
||||
showNsfw: false,
|
||||
showUnavailable: true,
|
||||
debug: false,
|
||||
useCustomLighthouseServers: false,
|
||||
customLighthouseServers: [],
|
||||
|
@ -90,17 +92,18 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall
|
|||
//core
|
||||
lbry.connect = function(callback)
|
||||
{
|
||||
// Check every half second to see if the daemon's running.
|
||||
// Returns true to callback once connected, or false if it takes too long and we give up.
|
||||
function checkDaemonRunning(tryNum=0) {
|
||||
lbry.daemonRunningStatus(function (runningStatus) {
|
||||
// Check every half second to see if the daemon is accepting connections
|
||||
// Once this returns True, can call getDaemonStatus to see where
|
||||
// we are in the startup process
|
||||
function checkDaemonStarted(tryNum=0) {
|
||||
lbry.isDaemonAcceptingConnections(function (runningStatus) {
|
||||
if (runningStatus) {
|
||||
lbry.isConnected = true;
|
||||
callback(true);
|
||||
} else {
|
||||
if (tryNum <= 600) { // Move # of tries into constant or config option
|
||||
setTimeout(function () {
|
||||
checkDaemonRunning(tryNum + 1);
|
||||
checkDaemonStarted(tryNum + 1);
|
||||
}, 500);
|
||||
} else {
|
||||
callback(false);
|
||||
|
@ -108,16 +111,12 @@ lbry.connect = function(callback)
|
|||
}
|
||||
});
|
||||
}
|
||||
checkDaemonRunning();
|
||||
checkDaemonStarted();
|
||||
}
|
||||
|
||||
lbry.daemonRunningStatus = function (callback) {
|
||||
// Returns true/false whether the daemon is running (i.e. fully conncected to the network),
|
||||
// or null if the AJAX connection to the daemon fails.
|
||||
|
||||
lbry.call('is_running', {}, callback, null, function () {
|
||||
callback(null);
|
||||
});
|
||||
lbry.isDaemonAcceptingConnections = function (callback) {
|
||||
// Returns true/false whether the daemon is at a point it will start returning status
|
||||
lbry.call('status', {}, () => callback(true), null, () => callback(false))
|
||||
};
|
||||
|
||||
lbry.getDaemonStatus = function (callback) {
|
||||
|
@ -132,7 +131,7 @@ lbry.getNewAddress = function(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);
|
||||
}
|
||||
|
||||
|
@ -177,25 +176,38 @@ lbry.getClaimInfo = 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.call('get_est_cost', { name: name }, callback);
|
||||
lbry.getKeyFee = function(name, callback, errorCallback) {
|
||||
lbry.call('stream_cost_estimate', { name: name }, callback, errorCallback);
|
||||
}
|
||||
|
||||
lbry.getTotalCost = function(name, size, callback) {
|
||||
lbry.call('get_est_cost', {
|
||||
lbry.getTotalCost = function(name, size, callback, errorCallback) {
|
||||
lbry.call('stream_cost_estimate', {
|
||||
name: name,
|
||||
size: size,
|
||||
}, callback);
|
||||
}, callback, errorCallback);
|
||||
}
|
||||
|
||||
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
|
||||
* 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
|
||||
* from Lighthouse is included.
|
||||
*/
|
||||
function getCostWithData(name, size, callback) {
|
||||
function getCostWithData(name, size, callback, errorCallback) {
|
||||
lbry.getTotalCost(name, size, (cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: true,
|
||||
});
|
||||
});
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
function getCostNoData(name, callback) {
|
||||
function getCostNoData(name, callback, errorCallback) {
|
||||
lbry.getKeyFee(name, (cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: false,
|
||||
});
|
||||
});
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
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.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', {
|
||||
name: name,
|
||||
delete_target_file: deleteTargetFile,
|
||||
}, callback);
|
||||
}
|
||||
|
||||
lbry.revealFile = function(path, callback) {
|
||||
lbry.call('reveal', { path: path }, callback);
|
||||
lbry.openFile = function(sdHash, 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) {
|
||||
// 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.
|
||||
lbry.getFilesInfo(function(filesInfo) {
|
||||
for (var fileInfo of filesInfo) {
|
||||
lbry.getFilesInfo(function(fileInfos) {
|
||||
for (var fileInfo of fileInfos) {
|
||||
if (fileInfo.lbry_uri == name) {
|
||||
callback(fileInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (tryNum <= 200) {
|
||||
setTimeout(function() { lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1) }, 250);
|
||||
} else if (timeoutCallback) {
|
||||
if (timeoutCallback && tryNum > 200) {
|
||||
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.
|
||||
|
||||
// Use ES6 named arguments instead of directly passing param dict?
|
||||
lbry.call('publish', params, publishedCallback, (errorInfo) => {
|
||||
errorCallback({
|
||||
name: fault.fault,
|
||||
message: fault.faultString,
|
||||
});
|
||||
});
|
||||
lbry.call('publish', params, publishedCallback, errorCallback);
|
||||
if (fileListedCallback) {
|
||||
lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
|
||||
fileListedCallback(fileInfo);
|
||||
|
@ -453,5 +467,64 @@ lbry.stop = function(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;
|
||||
|
|
|
@ -133,7 +133,7 @@ var ClaimCodePage = React.createClass({
|
|||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
|
||||
onConfirmed={this.handleFinished}>
|
||||
Your invite code has been redeemed.
|
||||
Your invite code has been redeemed. { ' ' }
|
||||
{this.state.referralCredits > 0
|
||||
? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits}
|
||||
will be added to your balance shortly.`
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {Link, ToolTipLink, DownloadLink, WatchLink} from '../component/link.js';
|
||||
import {Thumbnail, CreditAmount, TruncatedText, BusyMessage} from '../component/common.js';
|
||||
import {FileTile} from '../component/file-tile.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {BusyMessage} from '../component/common.js';
|
||||
|
||||
var fetchResultsStyle = {
|
||||
color: '#888',
|
||||
|
@ -40,14 +42,15 @@ var SearchNoResults = React.createClass({
|
|||
|
||||
var SearchResults = React.createClass({
|
||||
render: function() {
|
||||
var rows = [];
|
||||
this.props.results.forEach(function(result) {
|
||||
console.log(result);
|
||||
var mediaType = lbry.getMediaType(result.value.content_type);
|
||||
rows.push(
|
||||
<SearchResultRow key={result.name} name={result.name} title={result.value.title} imgUrl={result.value.thumbnail}
|
||||
description={result.value.description} nsfw={result.value.nsfw} mediaType={mediaType} />
|
||||
);
|
||||
var rows = [],
|
||||
seenNames = {}; //fix this when the search API returns claim IDs
|
||||
this.props.results.forEach(function({name, value}) {
|
||||
if (!seenNames[name]) {
|
||||
seenNames[name] = name;
|
||||
rows.push(
|
||||
<FileTile key={name} name={name} sdHash={value.sources.lbry_sd_hash} />
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<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 = {
|
||||
fontSize: '12px',
|
||||
color: '#aaa',
|
||||
|
@ -237,25 +66,30 @@ var featuredContentLegendStyle = {
|
|||
|
||||
var FeaturedContent = React.createClass({
|
||||
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 (
|
||||
<div className="row-fluid">
|
||||
<div className="span6">
|
||||
<h3>Featured Content</h3>
|
||||
<FeaturedContentItem name="bellflower" />
|
||||
<FeaturedContentItem name="itsadisaster" />
|
||||
<FeaturedContentItem name="dopeman" />
|
||||
<FeaturedContentItem name="smlawncare" />
|
||||
<FeaturedContentItem name="cinemasix" />
|
||||
<FileTile name="bellflower" />
|
||||
<FileTile name="itsadisaster" />
|
||||
<FileTile name="dopeman" />
|
||||
<FileTile name="smlawncare" />
|
||||
<FileTile name="cinemasix" />
|
||||
|
||||
</div>
|
||||
<div className="span6">
|
||||
<h3>Community Content <ToolTipLink style={featuredContentLegendStyle} label="What's this?"
|
||||
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>
|
||||
<FeaturedContentItem name="one" />
|
||||
<FeaturedContentItem name="two" />
|
||||
<FeaturedContentItem name="three" />
|
||||
<FeaturedContentItem name="four" />
|
||||
<FeaturedContentItem name="five" />
|
||||
<h3>
|
||||
Community Content
|
||||
<ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/>
|
||||
</h3>
|
||||
<FileTile name="one" />
|
||||
<FileTile name="two" />
|
||||
<FileTile name="three" />
|
||||
<FileTile name="four" />
|
||||
<FileTile name="five" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
201
js/page/file-list.js
Normal file
201
js/page/file-list.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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;
|
|
@ -91,8 +91,7 @@ var PublishPage = React.createClass({
|
|||
if (this.refs.file.getValue() !== '') {
|
||||
publishArgs.file_path = this._tempFilePath;
|
||||
}
|
||||
|
||||
console.log(publishArgs);
|
||||
|
||||
lbry.publish(publishArgs, (message) => {
|
||||
this.handlePublishStarted();
|
||||
}, null, (error) => {
|
||||
|
@ -198,15 +197,14 @@ var PublishPage = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount);
|
||||
|
||||
var newState = {
|
||||
const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount);
|
||||
const newState = {
|
||||
name: name,
|
||||
nameResolved: true,
|
||||
topClaimValue: parseFloat(claimInfo.amount),
|
||||
myClaimExists: !!myClaimInfo,
|
||||
myClaimValue: parseFloat(myClaimInfo.amount),
|
||||
myClaimMetadata: myClaimInfo.value,
|
||||
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null,
|
||||
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
|
||||
topClaimIsMine: topClaimIsMine,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={failureReason}
|
||||
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
|
||||
onConfirmed={this.closeModal}>
|
||||
{this.state.failureReason}
|
||||
</Modal>
|
||||
|
|
|
@ -51,7 +51,8 @@ var SettingsPage = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
settings: null,
|
||||
showNsfw: lbry.getClientSetting('showNsfw')
|
||||
showNsfw: lbry.getClientSetting('showNsfw'),
|
||||
showUnavailable: lbry.getClientSetting('showUnavailable'),
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
|
@ -69,6 +70,9 @@ var SettingsPage = React.createClass({
|
|||
onShowNsfwChange: function(event) {
|
||||
lbry.setClientSetting('showNsfw', event.target.checked);
|
||||
},
|
||||
onShowUnavailableChange: function(event) {
|
||||
lbry.setClientSetting('showUnavailable', event.target.checked);
|
||||
},
|
||||
render: function() {
|
||||
if (!this.state.daemonSettings) {
|
||||
return null;
|
||||
|
@ -114,7 +118,7 @@ var SettingsPage = React.createClass({
|
|||
<h3>Content</h3>
|
||||
<div className="form-row">
|
||||
<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>
|
||||
<div className="help">
|
||||
NSFW content may include nudity, intense sexuality, profanity, or other adult content.
|
||||
|
@ -122,6 +126,17 @@ var SettingsPage = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<h3>Share Diagnostic Data</h3>
|
||||
<label style={settingsCheckBoxOptionStyles}>
|
||||
|
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
|||
import lbry from '../lbry.js';
|
||||
import lighthouse from '../lighthouse.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 = {
|
||||
maxWidth: '100%',
|
||||
|
@ -21,7 +22,6 @@ var FormatItem = React.createClass({
|
|||
costIncludesData: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
|
||||
var claimInfo = this.props.claimInfo;
|
||||
var thumbnail = claimInfo.thumbnail;
|
||||
var title = claimInfo.title;
|
||||
|
@ -62,10 +62,7 @@ var FormatItem = React.createClass({
|
|||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section>
|
||||
{mediaType == 'video' ? <WatchLink streamName={this.props.name} button="primary" /> : null}
|
||||
<DownloadLink streamName={this.props.name} button="alt" />
|
||||
</section>
|
||||
<FileActions streamName={this.props.name} sdHash={claimInfo.sources.lbry_sd_hash} metadata={claimInfo} />
|
||||
<section>
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
</section>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"babel-cli": "^6.11.4",
|
||||
"babel-preset-es2015": "^6.13.2",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"clamp": "^1.0.1",
|
||||
"clamp-js": "^0.7.0",
|
||||
"mediaelement": "^2.23.4",
|
||||
"node-sass": "^3.8.0",
|
||||
"react": "^15.4.0",
|
||||
|
@ -34,9 +34,10 @@
|
|||
"babel-core": "^6.18.2",
|
||||
"babel-loader": "^6.2.8",
|
||||
"babel-plugin-react-require": "^3.0.0",
|
||||
"babel-polyfill": "^6.20.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"babel-polyfill": "^6.20.0",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"eslint": "^3.10.2",
|
||||
"eslint-config-airbnb": "^13.0.0",
|
||||
"eslint-loader": "^1.6.1",
|
||||
|
|
|
@ -8,7 +8,7 @@ html
|
|||
body
|
||||
{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: 1.3333;
|
||||
line-height: $font-line-height;
|
||||
}
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
@ -56,7 +56,7 @@ $drawer-width: 240px;
|
|||
#drawer-handle
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
max-height: $header-height - $spacing-vertical;
|
||||
max-height: $height-header - $spacing-vertical;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,10 @@ $drawer-width: 240px;
|
|||
background: $color-primary;
|
||||
color: white;
|
||||
&.header-no-subnav {
|
||||
height: $header-height;
|
||||
height: $height-header;
|
||||
}
|
||||
&.header-with-subnav {
|
||||
height: $header-height * 2;
|
||||
height: $height-header * 2;
|
||||
}
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -87,7 +87,7 @@ $drawer-width: 240px;
|
|||
width: 100%;
|
||||
z-index: 2;
|
||||
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
|
||||
{
|
||||
box-shadow: $default-box-shadow;
|
||||
|
@ -120,7 +120,7 @@ nav.sub-header
|
|||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
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;
|
||||
&:first-child
|
||||
{
|
||||
|
@ -147,13 +147,13 @@ nav.sub-header
|
|||
background: $color-canvas;
|
||||
&.no-sub-nav
|
||||
{
|
||||
min-height: calc(100vh - 60px); //should be -$header-height, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $header-height; }
|
||||
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 -$header-height, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $header-height * 2; }
|
||||
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
|
||||
{
|
||||
|
@ -206,9 +206,6 @@ $header-icon-size: 1.5em;
|
|||
box-shadow: $default-box-shadow;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.card-compact {
|
||||
padding: 22px;
|
||||
}
|
||||
.card-obscured
|
||||
{
|
||||
position: relative;
|
||||
|
|
|
@ -2,22 +2,31 @@
|
|||
|
||||
$spacing-vertical: 24px;
|
||||
|
||||
$padding-button: 12px;
|
||||
$padding-text-link: 4px;
|
||||
|
||||
$color-primary: #155B4A;
|
||||
$color-light-alt: hsl(hue($color-primary), 15, 85);
|
||||
$color-text-dark: #000;
|
||||
$color-help: rgba(0,0,0,.6);
|
||||
$color-notice: #921010;
|
||||
$color-warning: #ffffff;
|
||||
$color-load-screen-text: #c3c3c3;
|
||||
$color-canvas: #f5f5f5;
|
||||
$color-bg: #ffffff;
|
||||
$color-bg-alt: #D9D9D9;
|
||||
$color-money: #216C2A;
|
||||
$color-meta-light: #505050;
|
||||
|
||||
$font-size: 16px;
|
||||
$font-line-height: 1.3333;
|
||||
|
||||
$mobile-width-threshold: 801px;
|
||||
$max-content-width: 1000px;
|
||||
$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);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@import "global";
|
||||
|
||||
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
|
||||
color: $color;
|
||||
|
||||
.icon
|
||||
{
|
||||
&:first-child {
|
||||
|
@ -27,6 +27,9 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
color: $color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-fixed-width {
|
||||
|
@ -138,18 +141,20 @@ input[type="text"], input[type="search"]
|
|||
}
|
||||
|
||||
.button-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
+ .button-container
|
||||
{
|
||||
margin-left: 12px;
|
||||
margin-left: $padding-button;
|
||||
}
|
||||
}
|
||||
|
||||
.button-block
|
||||
.button-block, .faux-button-block
|
||||
{
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: $spacing-vertical * 1.5;
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
height: $height-button;
|
||||
line-height: $height-button;
|
||||
text-decoration: none;
|
||||
border: 0 none;
|
||||
text-align: center;
|
||||
|
@ -168,31 +173,39 @@ input[type="text"], input[type="search"]
|
|||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
.button-block
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-button;
|
||||
}
|
||||
|
||||
.button-primary
|
||||
{
|
||||
color: white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: $default-box-shadow;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.button-alt
|
||||
{
|
||||
background-color: rgba(0,0,0,.15);
|
||||
background-color: $color-bg-alt;
|
||||
box-shadow: $default-box-shadow;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.button-cancel
|
||||
{
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.button-text
|
||||
{
|
||||
@include text-link();
|
||||
display: inline-block;
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-text-link;
|
||||
}
|
||||
}
|
||||
.button-text-help
|
||||
{
|
||||
@include text-link(#5b8c80);
|
||||
@include text-link(#aaa);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
@ -338,21 +351,10 @@ input[type="text"], input[type="search"]
|
|||
margin: 0px 6px;
|
||||
}
|
||||
|
||||
.error-modal__error-list {
|
||||
border: 1px solid #eee;
|
||||
padding: 8px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.error-modal-overlay {
|
||||
background: rgba(#000, .88);
|
||||
}
|
||||
|
||||
.error-modal {
|
||||
max-width: none;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.error-modal__content {
|
||||
display: flex;
|
||||
padding: 0px 8px 10px 10px;
|
||||
|
@ -367,3 +369,15 @@ input[type="text"], input[type="search"]
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
@import "_icons";
|
||||
@import "_mediaelement";
|
||||
@import "_canvas";
|
||||
@import "_table";
|
||||
@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";
|
32
scss/component/_file-actions.scss
Normal file
32
scss/component/_file-actions.scss
Normal 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;
|
||||
}
|
31
scss/component/_file-tile.scss
Normal file
31
scss/component/_file-tile.scss
Normal 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;
|
||||
}
|
31
scss/component/_load-screen.scss
Normal file
31
scss/component/_load-screen.scss
Normal 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
22
scss/component/_menu.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@import "../global";
|
||||
|
||||
table.table-standard {
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
35
scss/component/_tooltip.scss
Normal file
35
scss/component/_tooltip.scss
Normal 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;
|
||||
}
|
|
@ -25,12 +25,12 @@ module.exports = {
|
|||
loaders: [
|
||||
{ test: /\.css$/, loader: "style!css" },
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
// Enable caching for improved performance during development
|
||||
// It uses default OS directory by default. If you need
|
||||
// something more custom, pass a path to it.
|
||||
// I.e., babel?cacheDirectory=<path>
|
||||
loader: 'babel?cacheDirectory'
|
||||
test: /\.jsx?$/,
|
||||
loader: 'babel',
|
||||
query: {
|
||||
cacheDirectory: true,
|
||||
presets:[ 'es2015', 'react', 'stage-2' ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue