diff --git a/README.md b/README.md index 0c7907cc7..d3f1ded33 100644 --- a/README.md +++ b/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 diff --git a/js/app.js b/js/app.js index c87017c28..68c03a648 100644 --- a/js/app.js +++ b/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 ; case 'downloaded': - return ; + return ; case 'published': - return ; + return ; case 'start': return ; 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({
-
+
{mainContent}
+ const {fixed, className, ...other} = this.props; + const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + + this.props.icon + ' ' + (this.props.className || '')); + return } }); @@ -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 ( -
- {this.props.children} -
- ); - } -}); - 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 + return }, }); diff --git a/js/component/file-actions.js b/js/component/file-actions.js new file mode 100644 index 000000000..cc1dba546 --- /dev/null +++ b/js/component/file-actions.js @@ -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 ( +
+ + + You don't have enough LBRY credits to pay for this stream. + +
+ ); + } +}); + +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 = ; + } 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 = {label}; + + linkBlock = ( +
+
{labelWithIcon}
+ {labelWithIcon} +
+ ); + } else { + linkBlock = ; + } + + return ( +
+ {(this.props.metadata.content_type && this.props.metadata.content_type.startsWith('video/')) ? : null} + {this.state.fileInfo !== null || this.state.fileInfo.isMine ? +
{linkBlock}
+ : null} + { showMenu ? + + + + : '' } + + You don't have enough LBRY credits to pay for this stream. + + + LBRY was unable to download the stream lbry://{this.props.streamName}. + + +

Are you sure you'd like to remove {this.props.metadata.title} from LBRY?

+ + +
+
+ ); + } +}); + +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 (
+ { + fileInfo || this.state.available || this.state.forceShowActions + ? + :
+
This file is not currently available.
+
+ +
+
+ +
+
+ } +
); + } +}); diff --git a/js/component/file-tile.js b/js/component/file-tile.js new file mode 100644 index 000000000..2e9e247cd --- /dev/null +++ b/js/component/file-tile.js @@ -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 ( + + + + ); + } +}); + +/*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 ( +
+
+
+ +
+
+ { !this.props.hidePrice + ? + : null} + +

+ + + {title} + + +

+ +

+ + {isConfirmed + ? metadata.description + : This file is pending confirmation.} + +

+
+
+ {this.state.showNsfwHelp + ?
+

+ This content is Not Safe For Work. + To view adult content, please change your . +

+
+ : null} +
+ ); + } +}); + +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 ; + } +}); \ No newline at end of file diff --git a/js/component/form.js b/js/component/form.js index 27a7c977a..ae4135a29 100644 --- a/js/component/form.js +++ b/js/component/form.js @@ -1,4 +1,5 @@ import React from 'react'; +import {Icon} from './common.js'; var requiredFieldWarningStyle = { color: '#cc0000', diff --git a/js/component/header.js b/js/component/header.js index 83fd3748b..fb64e9ece 100644 --- a/js/component/header.js +++ b/js/component/header.js @@ -52,7 +52,7 @@ var Header = React.createClass({

{ this.state.title }

-
@@ -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( diff --git a/js/component/link.js b/js/component/link.js index 99ce1e627..6b2f890bc 100644 --- a/js/component/link.js +++ b/js/component/link.js @@ -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 ? : '', - 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 = ( + + {'icon' in this.props ? : null} + {{this.props.label}} + {'badge' in this.props ? {this.props.badge} : null} + + ); + } return ( - - {this.props.icon ? icon : '' } - {this.props.label} - {this.props.badge ? {this.props.badge} : '' } + + {content} ); } -}); - -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 ? : '', - className = this.props.className + - (this.props.button ? ' button-block button-' + this.props.button : '') + - (this.props.hidden ? ' hidden' : '') + - (this.props.disabled ? ' disabled' : ''); - - return ( - - - {this.props.icon ? icon : '' } - {this.props.label} - - {(!this.props.tooltip ? null : - - {this.props.tooltip} - - )} - - ); - } -}); - -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 ( - - - -

Downloading to:

-
{this.state.filePath}
-
- - You don't have enough LBRY credits to pay for this stream. - - - LBRY was unable to download the stream lbry://{this.props.streamName}. - -
- ); - } -}); - -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 ( - - - - You don't have enough LBRY credits to pay for this stream. - - - ); - } -}); +}); \ No newline at end of file diff --git a/js/component/load_screen.js b/js/component/load_screen.js index 57d8e4f50..d8dedbe3d 100644 --- a/js/component/load_screen.js +++ b/js/component/load_screen.js @@ -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 ( -
+
LBRY -
+

- {this.props.details} + {this.props.isWarning ? : null} {this.props.details} + {this.props.isWarning + ?
+ : null}
); diff --git a/js/component/menu.js b/js/component/menu.js index a3670646d..cfa6b9df7 100644 --- a/js/component/menu.js +++ b/js/component/menu.js @@ -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 ( -
- {this.props.children} -
- ); - } -}); - -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 ? : null); return ( - {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 ( +
+ this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={this.onMenuIconClick} /> + {this.state.menuOpen + ?
this._menuDiv = div} className="menu"> + {this.props.children} +
+ : null} +
+ ); + } +}); \ No newline at end of file diff --git a/js/component/splash.js b/js/component/splash.js index bcb045468..29cb65234 100644 --- a/js/component/splash.js +++ b/js/component/splash.js @@ -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 ; diff --git a/js/component/tooltip.js b/js/component/tooltip.js new file mode 100644 index 000000000..eb7d7d4fb --- /dev/null +++ b/js/component/tooltip.js @@ -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 ( + +
+ {this.props.label} + +
+ {this.props.body} +
+ + ); + } +}); \ No newline at end of file diff --git a/js/lbry.js b/js/lbry.js index 9563d4195..2800bac44 100644 --- a/js/lbry.js +++ b/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; diff --git a/js/page/claim_code.js b/js/page/claim_code.js index 41b1e571a..dcdb03cf9 100644 --- a/js/page/claim_code.js +++ b/js/page/claim_code.js @@ -133,7 +133,7 @@ var ClaimCodePage = React.createClass({ - 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.` diff --git a/js/page/discover.js b/js/page/discover.js index af9792236..de59d09d7 100644 --- a/js/page/discover.js +++ b/js/page/discover.js @@ -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( - - ); + 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( + + ); + } }); return (
{rows}
@@ -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 ( -
-
-
- -
-
- {this.state.cost !== null - ? - - - : null} - -

- - - {this.props.title} - - -

-
- {this.props.mediaType == 'video' ? : null} - -
-

- - {this.props.description} - -

-
-
- { - !obscureNsfw || !this.state.isHovered ? null : -
-

- This content is Not Safe For Work. - To view adult content, please change your . -

-
- } -
- ); - } -}); - -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 (
- -
); - } -}); - 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 (

Featured Content

- - - - - + + + + +
-

Community Content

- - - - - +

+ Community Content + +

+ + + + +
); diff --git a/js/page/file-list.js b/js/page/file-list.js new file mode 100644 index 000000000..95304535c --- /dev/null +++ b/js/page/file-list.js @@ -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 ( +
+ +
+ ); + } else if (!this.state.fileInfos.length) { + return ( +
+ You haven't downloaded anything from LBRY yet. Go ! +
+ ); + } else { + return ( +
+ +
+ ); + } + } +}); + +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 ( +
+ +
+ ); + } + else if (!this.state.fileInfos.length) { + return ( +
+ You haven't published anything to LBRY yet. Try ! +
+ ); + } + else { + return ( +
+ +
+ ); + } + } +}); + +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(); + } + + return ( +
+ + Sort by { ' ' } + + + + + + + {content} +
+ ); + } +}); \ No newline at end of file diff --git a/js/page/my_files.js b/js/page/my_files.js deleted file mode 100644 index 2d6b28e66..000000000 --- a/js/page/my_files.js +++ /dev/null @@ -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 ( -
- -
- {/* @TODO: Switch to OS specific wording */} - - -
-
- - Are you sure you'd like to delete {this.props.title}? This will {this.props.completed ? ' stop the download and ' : ''} - permanently remove the file from your system. - -
- ); - } -}); - -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 = { 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 = - } else { - var watchButton = null; - } - - return ( -
-
-
- -
-
-

{this.props.pending ? this.props.title : {this.props.title}}

- {this.props.pending ? This file is pending confirmation - : ( -
-
- { ' ' } - {this.props.completed - ? (this.props.isMine - ? 'Published' - : 'Download complete') - : (parseInt(this.props.ratioLoaded * 100) + '%')} -
{ pauseLink }
-
{ watchButton }
-
- ) - } -
-
- {this.props.pending ? null : -
- - -
- } -
-
-
- ); - } -}); - -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 ( -
- -
- ); - } else if (!this.state.filesInfo.length) { - return ( -
- {this.props.show == 'downloaded' - ? You haven't downloaded anything from LBRY yet. Go ! - : You haven't published anything to LBRY yet.} -
- ); - } 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(); - } - } - return ( -
- - Sort by { ' ' } - - - - - - - {content} -
- ); - } -}); - - -export default MyFilesPage; diff --git a/js/page/publish.js b/js/page/publish.js index 70e1b851d..d1d48bc3a 100644 --- a/js/page/publish.js +++ b/js/page/publish.js @@ -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, }; diff --git a/js/page/referral.js b/js/page/referral.js index f3eb0cab1..29f273593 100644 --- a/js/page/referral.js +++ b/js/page/referral.js @@ -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.'}
- {this.state.failureReason} diff --git a/js/page/settings.js b/js/page/settings.js index 81403492b..b192f15c9 100644 --- a/js/page/settings.js +++ b/js/page/settings.js @@ -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({

Content

NSFW content may include nudity, intense sexuality, profanity, or other adult content. @@ -122,6 +126,17 @@ var SettingsPage = React.createClass({
+
+

Search

+
+
+ Would you like search results to include items that are not currently available for download? +
+ +
+

Share Diagnostic Data

-
- {mediaType == 'video' ? : null} - -
+
diff --git a/package.json b/package.json index 40ef3d434..b36fac6f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scss/_canvas.scss b/scss/_canvas.scss index fd24a2236..a5082d0d9 100644 --- a/scss/_canvas.scss +++ b/scss/_canvas.scss @@ -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; diff --git a/scss/_global.scss b/scss/_global.scss index ad5854a36..201409835 100644 --- a/scss/_global.scss +++ b/scss/_global.scss @@ -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); diff --git a/scss/_gui.scss b/scss/_gui.scss index 965d2eb9e..be4fefcbf 100644 --- a/scss/_gui.scss +++ b/scss/_gui.scss @@ -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; +} diff --git a/scss/all.scss b/scss/all.scss index e02fd6d3d..452cbbcac 100644 --- a/scss/all.scss +++ b/scss/all.scss @@ -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"; \ No newline at end of file diff --git a/scss/component/_file-actions.scss b/scss/component/_file-actions.scss new file mode 100644 index 000000000..4eda16b51 --- /dev/null +++ b/scss/component/_file-actions.scss @@ -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; +} \ No newline at end of file diff --git a/scss/component/_file-tile.scss b/scss/component/_file-tile.scss new file mode 100644 index 000000000..a5c73a175 --- /dev/null +++ b/scss/component/_file-tile.scss @@ -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; +} \ No newline at end of file diff --git a/scss/component/_load-screen.scss b/scss/component/_load-screen.scss new file mode 100644 index 000000000..85093bb0d --- /dev/null +++ b/scss/component/_load-screen.scss @@ -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; +} diff --git a/scss/component/_menu.scss b/scss/component/_menu.scss new file mode 100644 index 000000000..68ceb5d7d --- /dev/null +++ b/scss/component/_menu.scss @@ -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; + } +} \ No newline at end of file diff --git a/scss/_table.scss b/scss/component/_table.scss similarity index 97% rename from scss/_table.scss rename to scss/component/_table.scss index 899010d60..9d60cf6e8 100644 --- a/scss/_table.scss +++ b/scss/component/_table.scss @@ -1,3 +1,5 @@ +@import "../global"; + table.table-standard { word-wrap: break-word; max-width: 100%; diff --git a/scss/component/_tooltip.scss b/scss/component/_tooltip.scss new file mode 100644 index 000000000..9a6ccd7da --- /dev/null +++ b/scss/component/_tooltip.scss @@ -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; +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 34b4bdffc..03ccc2372 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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= - loader: 'babel?cacheDirectory' + test: /\.jsx?$/, + loader: 'babel', + query: { + cacheDirectory: true, + presets:[ 'es2015', 'react', 'stage-2' ] + } } ] }