Merge pull request #116 from lbryio/unified-tiles

Unified File Tiles (WIP)
This commit is contained in:
Jeremy Kauffman 2017-01-17 17:10:40 -05:00 committed by GitHub
commit 2192efc49f
23 changed files with 957 additions and 852 deletions

View file

@ -4,7 +4,6 @@ import SettingsPage from './page/settings.js';
import HelpPage from './page/help.js'; import HelpPage from './page/help.js';
import WatchPage from './page/watch.js'; import WatchPage from './page/watch.js';
import ReportPage from './page/report.js'; import ReportPage from './page/report.js';
import MyFilesPage from './page/my_files.js';
import StartPage from './page/start.js'; import StartPage from './page/start.js';
import ClaimCodePage from './page/claim_code.js'; import ClaimCodePage from './page/claim_code.js';
import ReferralPage from './page/referral.js'; import ReferralPage from './page/referral.js';
@ -14,6 +13,7 @@ import PublishPage from './page/publish.js';
import DiscoverPage from './page/discover.js'; import DiscoverPage from './page/discover.js';
import SplashScreen from './component/splash.js'; import SplashScreen from './component/splash.js';
import DeveloperPage from './page/developer.js'; import DeveloperPage from './page/developer.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Drawer from './component/drawer.js'; import Drawer from './component/drawer.js';
import Header from './component/header.js'; import Header from './component/header.js';
import Modal from './component/modal.js'; import Modal from './component/modal.js';
@ -164,9 +164,9 @@ var App = React.createClass({
case 'report': case 'report':
return <ReportPage />; return <ReportPage />;
case 'downloaded': case 'downloaded':
return <MyFilesPage show="downloaded" />; return <FileListDownloaded />;
case 'published': case 'published':
return <MyFilesPage show="published" />; return <FileListPublished />;
case 'start': case 'start':
return <StartPage />; return <StartPage />;
case 'claim': case 'claim':
@ -190,7 +190,8 @@ var App = React.createClass({
}, },
render: function() { render: function() {
var mainContent = this.getMainContent(), var mainContent = this.getMainContent(),
headerLinks = this.getHeaderLinks(); headerLinks = this.getHeaderLinks(),
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
return ( return (
this.state.viewingPage == 'watch' ? this.state.viewingPage == 'watch' ?
@ -198,7 +199,7 @@ var App = React.createClass({
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }> <div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} /> <Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }> <div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
<Header onOpenDrawer={this.openDrawer} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} /> <Header onOpenDrawer={this.openDrawer} initialQuery={searchQuery} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} />
{mainContent} {mainContent}
</div> </div>
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available" <Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"

View file

@ -1,18 +1,19 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import $clamp from 'clamp'; import $clamp from 'clamp-js';
//component/icon.js //component/icon.js
export let Icon = React.createClass({ export let Icon = React.createClass({
propTypes: { propTypes: {
style: React.PropTypes.object, icon: React.PropTypes.string.isRequired,
fixed: React.PropTypes.bool,
className: React.PropTypes.string, className: React.PropTypes.string,
fixed: React.PropTypes.bool,
}, },
render: function() { render: function() {
var className = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + this.props.icon + ' ' + const {fixed, className, ...other} = this.props;
(this.props.className || '')); const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
return <span className={className} style={this.props.style}></span> this.props.icon + ' ' + (this.props.className || ''));
return <span className={spanClassName} {... other}></span>
} }
}); });
@ -123,7 +124,7 @@ export let Thumbnail = React.createClass({
_isMounted: false, _isMounted: false,
propTypes: { propTypes: {
src: React.PropTypes.string.isRequired, src: React.PropTypes.string,
}, },
handleError: function() { handleError: function() {
if (this.state.imageUrl != this._defaultImageUri) { if (this.state.imageUrl != this._defaultImageUri) {
@ -151,6 +152,6 @@ export let Thumbnail = React.createClass({
this._isMounted = false; this._isMounted = false;
}, },
render: function() { render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} /> return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
}, },
}); });

View file

@ -0,0 +1,221 @@
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 {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>
);
}
});
export let FileActions = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
propTypes: {
streamName: React.PropTypes.string,
sdHash: React.PropTypes.string.isRequired,
metadata: React.PropTypes.object,
path: React.PropTypes.string,
hidden: React.PropTypes.bool,
deleteChecked: React.PropTypes.bool,
onRemove: React.PropTypes.function,
},
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 <section className="file-actions--stub"></section>;
}
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" />{label}</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 button="text" label="Open" icon="icon-folder-open" onClick={this.onOpenClick} />;
}
return (
<section className="file-actions">
{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>
</section>
);
}
});

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

@ -0,0 +1,192 @@
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,
});
}
});
},
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
}
},
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 || {},
obscureNsfw = this.props.obscureNsfw && metadata.nsfw,
title = metadata.title ? metadata.title : ('lbry://' + this.props.name);
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 || this.props.name)} /></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}>
{metadata.description}
</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 href="?settings" label="Settings" />.
</p>
</div>
: null}
</section>
);
}
});
export let FileTile = React.createClass({
_isMounted: false,
propTypes: {
name: React.PropTypes.string.isRequired
},
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 name={this.props.name} sdHash={this.state.sdHash} metadata={this.state.metadata} />;
}
});

View file

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

View file

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

View file

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

View file

@ -132,7 +132,7 @@ lbry.getNewAddress = function(callback) {
lbry.call('get_new_address', {}, callback); lbry.call('get_new_address', {}, callback);
} }
lbry.checkAddressIsMine = function(address, callback) { lbry.checkAddressIsMine = function(address, callback) {
lbry.call('address_is_mine', {address: address}, callback); lbry.call('address_is_mine', {address: address}, callback);
} }
@ -265,22 +265,29 @@ lbry.stopFile = function(name, callback) {
lbry.call('stop_lbry_file', { name: name }, callback); lbry.call('stop_lbry_file', { name: name }, callback);
} }
lbry.deleteFile = function(name, deleteTargetFile=true, callback) { lbry.removeFile = function(sdHash, name, deleteTargetFile=true, callback) { // Name param is temporary until the API can delete by unique ID (SD hash, claim ID etc.)
this._removedFiles.push(sdHash);
this._updateSubscribedFileInfo(sdHash);
lbry.call('delete_lbry_file', { lbry.call('delete_lbry_file', {
name: name, name: name,
delete_target_file: deleteTargetFile, delete_target_file: deleteTargetFile,
}, callback); }, callback);
} }
lbry.revealFile = function(path, callback) { lbry.openFile = function(sdHash, callback) {
lbry.call('reveal', { path: path }, callback); lbry.call('open', {sd_hash: sdHash}, callback);
}
lbry.revealFile = function(sdHash, callback) {
lbry.call('reveal', {sd_hash: sdHash}, callback);
} }
lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) { lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
// Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo(). // Calls callback with file info when it appears in the list of files returned by lbry.getFilesInfo().
// If timeoutCallback is provided, it will be called if the file fails to appear. // If timeoutCallback is provided, it will be called if the file fails to appear.
lbry.getFilesInfo(function(filesInfo) { lbry.getFilesInfo(function(fileInfos) {
for (var fileInfo of filesInfo) { for (var fileInfo of fileInfos) {
if (fileInfo.lbry_uri == name) { if (fileInfo.lbry_uri == name) {
callback(fileInfo); callback(fileInfo);
return; return;
@ -453,5 +460,64 @@ lbry.stop = function(callback) {
lbry.call('stop', {}, callback); lbry.call('stop', {}, callback);
}; };
lbry.fileInfo = {};
lbry._fileInfoSubscribeIdCounter = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000;
lbry._removedFiles = [];
lbry._claimIdOwnershipCache = {}; // should be claimId!!! But not
lbry._updateClaimOwnershipCache = function(claimId) {
lbry.getMyClaims((claimInfos) => {
lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) {
return match || claimInfo.claim_id == claimId;
});
});
};
lbry._updateSubscribedFileInfo = function(sdHash) {
const callSubscribedCallbacks = (sdHash, fileInfo) => {
for (let [subscribeId, callback] of Object.entries(this._fileInfoSubscribeCallbacks[sdHash])) {
callback(fileInfo);
}
}
if (lbry._removedFiles.includes(sdHash)) {
callSubscribedCallbacks(sdHash, false);
} else {
lbry.getFileInfoBySdHash(sdHash, (fileInfo) => {
if (fileInfo) {
if (this._claimIdOwnershipCache[fileInfo.claim_id] === undefined) {
this._updateClaimOwnershipCache(fileInfo.claim_id);
}
fileInfo.isMine = !!this._claimIdOwnershipCache[fileInfo.claim_id];
}
callSubscribedCallbacks(sdHash, fileInfo);
});
}
if (Object.keys(this._fileInfoSubscribeCallbacks[sdHash]).length) {
setTimeout(() => {
this._updateSubscribedFileInfo(sdHash);
}, lbry._fileInfoSubscribeInterval);
}
}
lbry.fileInfoSubscribe = function(sdHash, callback) {
if (!lbry._fileInfoSubscribeCallbacks[sdHash])
{
lbry._fileInfoSubscribeCallbacks[sdHash] = {};
}
const subscribeId = ++lbry._fileInfoSubscribeIdCounter;
lbry._fileInfoSubscribeCallbacks[sdHash][subscribeId] = callback;
lbry._updateSubscribedFileInfo(sdHash);
return subscribeId;
}
lbry.fileInfoUnsubscribe = function(name, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[name][subscribeId];
}
export default lbry; export default lbry;

View file

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

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

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

View file

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

View file

@ -91,8 +91,7 @@ var PublishPage = React.createClass({
if (this.refs.file.getValue() !== '') { if (this.refs.file.getValue() !== '') {
publishArgs.file_path = this._tempFilePath; publishArgs.file_path = this._tempFilePath;
} }
console.log(publishArgs);
lbry.publish(publishArgs, (message) => { lbry.publish(publishArgs, (message) => {
this.handlePublishStarted(); this.handlePublishStarted();
}, null, (error) => { }, null, (error) => {

View file

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

View file

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

View file

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

View file

@ -2,12 +2,15 @@
$spacing-vertical: 24px; $spacing-vertical: 24px;
$padding-button: 12px;
$color-primary: #155B4A; $color-primary: #155B4A;
$color-light-alt: hsl(hue($color-primary), 15, 85); $color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000; $color-text-dark: #000;
$color-help: rgba(0,0,0,.6); $color-help: rgba(0,0,0,.6);
$color-canvas: #f5f5f5; $color-canvas: #f5f5f5;
$color-bg: #ffffff; $color-bg: #ffffff;
$color-bg-alt: #D9D9D9;
$color-money: #216C2A; $color-money: #216C2A;
$color-meta-light: #505050; $color-meta-light: #505050;
@ -17,7 +20,8 @@ $mobile-width-threshold: 801px;
$max-content-width: 1000px; $max-content-width: 1000px;
$max-text-width: 660px; $max-text-width: 660px;
$header-height: $spacing-vertical * 2.5; $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5;
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); $default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);

View file

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

View file

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

View file

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

View file

@ -0,0 +1,27 @@
@import "../global";
.file-tile__row {
height: $spacing-vertical * 7;
}
.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;
}

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

@ -0,0 +1,21 @@
@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;
}
.menu__menu-item {
display: block;
padding: $spacing-vertical / 4 $spacing-vertical / 2;
&:hover {
background: $color-bg-alt;
}
}

View file

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

View file

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