big refactor of file actions/buttons/tiles

This commit is contained in:
Jeremy Kauffman 2017-01-12 21:03:34 -05:00 committed by Alex Liebowitz
parent 95675cd802
commit b7f23aa0dd
17 changed files with 563 additions and 605 deletions

View file

@ -152,6 +152,6 @@ export let Thumbnail = React.createClass({
this._isMounted = false;
},
render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
},
});

View file

@ -0,0 +1,229 @@
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,
metadata: React.PropTypes.object,
path: React.PropTypes.string,
hidden: React.PropTypes.bool,
onRemoveConfirmed: React.PropTypes.func,
deleteChecked: React.PropTypes.bool,
},
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.state.fileInfo.download_path);
}
},
handleDeleteCheckboxClicked: function(event) {
this.setState({
deleteChecked: event.target.checked,
});
},
handleRevealClicked: function() {
if (this.state.fileInfo && this.state.fileInfo.download_path) {
lbry.revealFile(this.state.fileInfo.download_path);
}
},
handleRemoveClicked: function() {
this.setState({
modal: 'confirmRemove',
});
},
handleRemoveConfirmed: function() {
lbry.deleteFile(this.props.sdHash || this.props.streamName, this.state.deleteChecked);
if (this.props.onRemoveConfirmed) {
this.props.onRemoveConfirmed();
}
this.setState({
modal: null,
fileInfo: false,
attemptingRemove: true,
attemptingDownload: false
});
},
openMenu: function() {
this.setState({
menuOpen: !this.state.menuOpen,
});
},
componentDidMount: function() {
this._isMounted = true;
if ('sdHash' in this.props) {
alert('render by sd hash is broken');
lbry.fileInfoSubscribeByStreamHash(this.props.sdHash, this.fileInfoU);
} else if ('streamName' in this.props) {
this._fileInfoSubscribeId = lbry.fileInfoSubscribeByName(this.props.streamName, this.onFileInfoUpdate);
} else {
throw new Error("No stream name or sd hash passed to FileTile");
}
},
componentWillUnmount: function() {
this._isMounted = false;
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.name, 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.attemptingRemove && this.state.fileInfo !== null;
let linkBlock;
if (this.state.attemptingRemove || (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><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>
);
}
});

View file

@ -1,188 +1,149 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link, DownloadLink, WatchLink} from '../component/link.js';
import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.js';
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js';
let FileTile = React.createClass({
let FilePrice = React.createClass({
_isMounted: false,
_fileInfoCheckInterval: 5000,
propTypes: {
metadata: React.PropTypes.object.isRequired,
fileInfo: React.PropTypes.string,
name: React.PropTypes.string,
sdHash: React.PropTypes.string,
available: React.PropTypes.bool,
isMine: React.PropTypes.bool,
local: React.PropTypes.bool,
cost: React.PropTypes.number,
costIncludesData: React.PropTypes.bool,
hideOnRemove: React.PropTypes.bool,
name: React.PropTypes.string
},
updateFileInfo: function(progress=null) {
const updateFileInfoCallback = ((fileInfo) => {
if (!this._isMounted || 'fileInfo' in this.props) {
/**
* The component was unmounted, or a file info data structure has now been provided by the
* containing component.
*/
return;
}
this.setState({
fileInfo: fileInfo || null,
local: !!fileInfo,
});
setTimeout(() => { this.updateFileInfo() }, this._fileInfoCheckInterval);
});
if ('sdHash' in this.props) {
lbry.getFileInfoBySdHash(this.props.sdHash, updateFileInfoCallback);
this.getIsMineIfNeeded(this.props.sdHash);
} else if ('name' in this.props) {
lbry.getFileInfoByName(this.props.name, (fileInfo) => {
this.getIsMineIfNeeded(fileInfo.sd_hash);
updateFileInfoCallback(fileInfo);
});
} else {
throw new Error("No progress, stream name or sd hash passed to FileTile");
}
},
getIsMineIfNeeded: function(sdHash) {
if (this.state.isMine !== null) {
// The info was already provided by this.props.isMine
return;
}
lbry.getMyClaims((claimsInfo) => {
for (let {value} of claimsInfo) {
if (JSON.parse(value).sources.lbry_sd_hash == sdHash) {
this.setState({
isMine: true,
});
return;
}
}
this.setState({
isMine: false,
});
});
},
getInitialState: function() {
return {
downloading: false,
removeConfirmed: false,
isHovered: false,
cost: null,
costIncludesData: null,
fileInfo: 'fileInfo' in this.props ? this.props.fileInfo : null,
isMine: 'isMine' in this.props ? this.props.isMine : null,
local: 'local' in this.props ? this.props.local : null,
}
},
getDefaultProps: function() {
return {
compact: false,
hideOnRemove: false,
}
},
handleMouseOver: function() {
this.setState({
isHovered: true,
});
},
handleMouseOut: function() {
this.setState({
isHovered: false,
});
},
handleRemoveConfirmed: function() {
this.setState({
removeConfirmed: true,
});
},
componentWillMount: function() {
this.updateFileInfo();
if ('cost' in this.props) {
this.setState({
cost: this.props.cost,
costIncludesData: this.props.costIncludesData,
});
} else {
lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => {
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>
);
}
});
let FileTile = React.createClass({
_isMounted: false,
propTypes: {
name: React.PropTypes.string,
sdHash: React.PropTypes.string,
showPrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool,
hideOnRemove: React.PropTypes.bool
},
getInitialState: function() {
return {
metadata: null,
title: null,
showNsfwHelp: false,
isRemoved: false
}
},
getDefaultProps: function() {
return {
hideOnRemove: false,
obscureNsfw: !lbry.getClientSetting('showNsfw'),
showPrice: true
}
},
handleMouseOver: function() {
if (this.props.obscureNsfw && this.state.metadata && this.state.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
},
handleMouseOut: function() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: false,
});
}
},
onRemove: function() {
this.setState({
isRemoved: true,
});
},
componentDidMount: function() {
this._isMounted = true;
lbry.resolveName(this.props.name, (metadata) => {
if (this._isMounted) {
this.setState({
metadata: metadata,
title: metadata && metadata.title ? metadata.title : ('lbry://' + this.props.name),
});
}
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.isMine === null || this.state.local === null ||
(this.props.hideOnRemove && this.state.removeConfirmed)) {
if (this.state.metadata === null || (this.props.hideOnRemove && this.state.isRemoved)) {
return null;
}
const obscureNsfw = !lbry.getClientSetting('showNsfw') && this.props.metadata.nsfw;
const obscureNsfw = this.props.obscureNsfw && this.state.metadata.nsfw;
let downloadLinkExtraProps = {};
if (this.state.fileInfo === null) {
downloadLinkExtraProps.state = 'not-started';
} else if (!this.state.fileInfo.completed) {
downloadLinkExtraProps.state = 'downloading';
const {written_bytes, total_bytes, path} = this.state.fileInfo;
downloadLinkExtraProps.progress = written_bytes / total_bytes;
} else {
downloadLinkExtraProps.state = 'done';
downloadLinkExtraProps.path = this.state.fileInfo.download_path;
}
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') + (this.props.compact ? 'file-tile--compact' : '')} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className="row-fluid card-content file-tile__row">
<div className="span3">
<a href={'/?show=' + this.props.name}><Thumbnail className="file-tile__thumbnail" src={this.props.metadata.thumbnail} alt={'Photo for ' + (this.props.metadata.title || this.props.name)} /></a>
<a href={'/?show=' + this.props.name}><Thumbnail className="file-tile__thumbnail" src={this.state.metadata.thumbnail} alt={'Photo for ' + (this.state.metadata.title || this.props.name)} /></a>
</div>
<div className="span9">
{this.state.cost !== null && !this.state.local
? <span className="file-tile__cost">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
{ this.props.showPrice
? <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 ' + (this.props.compact ? 'file-tile__title--compact' : '')}>
<h3 className="file-tile__title">
<a href={'/?show=' + this.props.name}>
<TruncatedText lines={3}>
{this.props.metadata.title}
<TruncatedText lines={2}>
{this.state.metadata.title}
</TruncatedText>
</a>
</h3>
<div>
{this.props.metadata.content_type.startsWith('video/') ? <WatchLink streamName={this.props.name} button="primary" /> : null}
{!this.props.isMine
? <DownloadLink streamName={this.props.name} metadata={this.props.metadata} button="text"
onRemoveConfirmed={this.handleRemoveConfirmed} {... downloadLinkExtraProps}/>
: null}
</div>
<FileActions streamName={this.props.name} metadata={this.state.metadata} />
<p className="file-tile__description">
<TruncatedText lines={3}>
{this.props.metadata.description}
{this.state.metadata.description}
</TruncatedText>
</p>
</div>
</div>
{obscureNsfw && this.state.isHovered
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.

View file

@ -1,11 +1,6 @@
import React from 'react';
import lbry from '../lbry.js';
import FormField from './form.js';
import Modal from './modal.js';
import {Menu, MenuItem} from './menu.js';
import {Icon, ToolTip} from './common.js';
export let Link = React.createClass({
propTypes: {
label: React.PropTypes.string,
@ -110,264 +105,3 @@ export let ToolTipLink = React.createClass({
);
}
});
export let DropDown = React.createClass({
propTypes: {
onCaretClick: React.PropTypes.func,
},
handleCaretClicked: function(event) {
/**
* The menu handles caret clicks via a window event listener, so we just need to prevent clicks
* on the caret from bubbling up to the link
*/
this.setState({
menuOpen: !this.state.menuOpen,
});
event.stopPropagation();
return false;
},
closeMenu: function(event) {
this.setState({
menuOpen: false,
});
},
getInitialState: function() {
return {
menuOpen: false,
};
},
render: function() {
const {onCaretClick, ...other} = this.props;
return (
<div>
<Link {...other}>
<span className="link-label">{this.props.label}</span>
<Icon icon="icon-caret-down" fixed={true} onClick={this.handleCaretClicked} />
</Link>
{this.state.menuOpen
? <Menu onClickOut={this.closeMenu}>
{this.props.children}
</Menu>
: null}
</div>
);
}
});
export let DownloadLink = React.createClass({
propTypes: {
type: React.PropTypes.string,
streamName: React.PropTypes.string,
sdHash: React.PropTypes.string,
metadata: React.PropTypes.object,
label: React.PropTypes.string,
button: React.PropTypes.string,
state: React.PropTypes.oneOf(['not-started', 'downloading', 'done']),
progress: React.PropTypes.number,
path: React.PropTypes.string,
hidden: React.PropTypes.bool,
deleteChecked: React.PropTypes.bool,
onRemoveConfirmed: React.PropTypes.func,
},
tryDownload: function() {
this.setState({
attemptingDownload: true,
});
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,
});
} else {
this.setState({
filePath: streamInfo.path,
attemptingDownload: false,
});
}
});
}
});
});
},
openMenu: function() {
this.setState({
menuOpen: !this.state.menuOpen,
});
},
handleDeleteCheckboxClicked: function(event) {
this.setState({
deleteChecked: event.target.checked,
});
},
handleRevealClicked: function() {
lbry.revealFile(this.props.path);
},
handleRemoveClicked: function() {
this.setState({
modal: 'confirmRemove',
});
},
handleRemoveConfirmed: function() {
lbry.deleteFile(this.props.sdHash || this.props.streamName, this.state.deleteChecked);
if (this.props.onRemoveConfirmed) {
this.props.onRemoveConfirmed();
}
this.setState({
modal: null,
attemptingRemove: true,
});
},
getDefaultProps: function() {
return {
state: 'not-started',
hideOnDelete: false,
}
},
getInitialState: function() {
return {
filePath: null,
modal: null,
menuOpen: false,
deleteChecked: false,
attemptingDownload: false,
attemptingRemove: false,
}
},
closeModal: function() {
this.setState({
modal: null,
})
},
handleClick: function() {
if (this.props.state == 'not-started') {
this.tryDownload();
} else if (this.props.state == 'done') {
lbry.openFile(this.props.path);
}
},
render: function() {
const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder';
const dropDownItems = [
<MenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />,
<MenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />,
];
let linkBlock;
if (this.state.attemptingRemove || this.props.state == 'not-started') {
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.handleClick} />;
} else if (this.state.attemptingDownload) {
linkBlock = <Link button="text" className="button-download button-download--bg"
label="Connecting..." icon="icon-download" />
} else if (this.props.state == 'downloading') {
const label = `${parseInt(this.props.progress * 100)}% complete`;
linkBlock = (
<span>
<DropDown button="download" className="button-download--bg" label={label} icon="icon-download"
onClick={this.handleClick}>
{dropDownItems}
</DropDown>
<DropDown button="download" className="button-download--fg" label={label} icon="icon-download"
onClick={this.handleClick} style={{width: `${this.props.progress * 100}%`}}>
{dropDownItems}
</DropDown>
</span>
);
} else if (this.props.state == 'done') {
linkBlock = (
<DropDown button="alt" label="Open" onClick={this.handleClick} onCaretClick={this.openMenu}>
{dropDownItems}
</DropDown>
);
} else {
throw new Error(`Unknown download state ${this.props.state} passed to DownloadLink`);
}
return (
<span className="button-container">
{linkBlock}
<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>
</span>
);
}
});
export let WatchLink = React.createClass({
propTypes: {
type: React.PropTypes.string,
streamName: React.PropTypes.string,
label: React.PropTypes.string,
button: React.PropTypes.string,
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 (
<div 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>
</div>
);
}
});

View file

@ -1,35 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Icon} from './common.js';
import {Link} from '../component/link.js';
export let Menu = React.createClass({
propTypes: {
onClickOut: React.PropTypes.func.isRequired,
},
handleWindowClick: function(e) {
if (!this._div.contains(e.target)) {
// Menu is open and user clicked outside of it
this.props.onClickOut();
}
},
componentDidMount: function() {
window.addEventListener('click', this.handleWindowClick, false);
},
componentWillUnmount: function() {
window.removeEventListener('click', this.handleWindowClick, false);
},
render: function() {
const {onClickOut, ...other} = this.props;
return (
<div ref={(div) => this._div = div} className={'menu ' + (this.props.className || '')}
{... other}>
{this.props.children}
</div>
);
}
});
export let MenuItem = React.createClass({
export let DropDownMenuItem = React.createClass({
propTypes: {
href: React.PropTypes.string,
label: React.PropTypes.string,
@ -45,7 +18,7 @@ export let MenuItem = React.createClass({
var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null);
return (
<a className="button-text menu__menu-item" onClick={this.props.onClick}
<a className="menu__menu-item" onClick={this.props.onClick}
href={this.props.href || 'javascript:'} label={this.props.label}>
{this.props.iconPosition == 'left' ? icon : null}
{this.props.label}
@ -54,3 +27,55 @@ 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() {
this.setState({
menuOpen: !this.state.menuOpen,
});
if (!this.state.menuOpen && !this._isWindowClickBound) {
this._isWindowClickBound = true;
window.addEventListener('click', this.handleWindowClick, false);
}
return false;
},
handleWindowClick: function(e) {
if (this.state.menuOpen &&
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
console.log('menu closing disabled due to auto close on click, fix me');
return;
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.checkAddressIsMine = function(address, callback) {
lbry.checkAddressIsMine = function(address, callback) {
lbry.call('address_is_mine', {address: address}, callback);
}
@ -457,5 +457,56 @@ lbry.stop = function(callback) {
lbry.call('stop', {}, callback);
};
lbry.fileInfo = {};
lbry._fileInfoSubscribeIdCounter = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000;
lbry._claimIdOwnershipCache = {}; // should be claimId!!! But not
lbry._updateClaimOwnershipCache = function(claimId) {
lbry.getMyClaims((claimsInfo) => {
lbry._claimIdOwnershipCache[claimId] = !!claimsInfo.reduce(function(match, claimInfo) {
return match || claimInfo.claim_id == claimId;
});
});
};
lbry._updateSubscribedFileInfoByName = function(name) {
lbry.getFileInfoByName(name, (fileInfo) => {
if (fileInfo) {
if (this._claimIdOwnershipCache[fileInfo.claim_id] === undefined) {
lbry._updateClaimOwnershipCache(fileInfo.claim_id);
}
fileInfo.isMine = !!this._claimIdOwnershipCache[fileInfo.claim_id];
}
this._fileInfoSubscribeCallbacks[name].forEach(function(callback) {
callback(fileInfo);
});
});
setTimeout(() => { this._updateSubscribedFileInfoByName(name) }, lbry._fileInfoSubscribeInterval);
}
lbry.fileInfoSubscribeByName = function(name, callback) {
if (!lbry._fileInfoSubscribeCallbacks[name])
{
lbry._fileInfoSubscribeCallbacks[name] = [];
}
const subscribeId = ++lbry._fileInfoSubscribeIdCounter;
lbry._fileInfoSubscribeCallbacks[name][subscribeId] = callback;
lbry._updateSubscribedFileInfoByName(name);
return subscribeId;
}
// lbry.fileInfoSubscribeByStreamHash = function(sdHash, callback) {
// lbry.getFileInfoBySdHash(this.props.sdHash, this.updateFileInfoCallback);
// this.getIsMineIfNeeded(this.props.sdHash);
// setTimeout(() => { this.updateFileInfo() }, this._fileInfoCheckInterval);
// }
lbry.fileInfoUnsubscribe = function(name, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[name][subscribeId];
}
export default lbry;

View file

@ -2,8 +2,8 @@ import React from 'react';
import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js';
import FileTile from '../component/file-tile.js';
import {Link, ToolTipLink, DownloadLink, WatchLink} from '../component/link.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 = {
color: '#888',
@ -53,57 +53,6 @@ var SearchResults = React.createClass({
}
});
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}>
<FileTile name={this.props.name} metadata={this.state.metadata} compact />
</div>);
}
});
var featuredContentLegendStyle = {
fontSize: '12px',
color: '#aaa',
@ -116,21 +65,21 @@ var FeaturedContent = React.createClass({
<div className="row-fluid">
<div className="span6">
<h3>Featured Content</h3>
<FeaturedContentItem name="bellflower" />
<FeaturedContentItem name="itsadisaster" />
<FeaturedContentItem name="dopeman" />
<FeaturedContentItem name="smlawncare" />
<FeaturedContentItem name="cinemasix" />
<FileTile name="bellflower" />
<FileTile name="itsadisaster" />
<FileTile name="dopeman" />
<FileTile name="smlawncare" />
<FileTile name="cinemasix" />
</div>
<div className="span6">
<h3>Community Content <ToolTipLink style={featuredContentLegendStyle} label="What's this?"
tooltip='Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!' /></h3>
<FeaturedContentItem name="one" />
<FeaturedContentItem name="two" />
<FeaturedContentItem name="three" />
<FeaturedContentItem name="four" />
<FeaturedContentItem name="five" />
<FileTile name="one" />
<FileTile name="two" />
<FileTile name="three" />
<FileTile name="four" />
<FileTile name="five" />
</div>
</div>
);

View file

@ -178,7 +178,7 @@ var MyFilesPage = React.createClass({
seenUris[lbry_uri] = true;
content.push(<FileTile name={lbry_uri} sdHash={sd_hash} isMine={this.props.show == 'published'} local={true} hideOnRemove={true}
content.push(<FileTile name={lbry_uri} sdHash={sd_hash} isMine={this.props.show == 'published'} showPrice={false} hideOnRemove={true}
metadata={metadata} completed={completed} stopped={stopped} pending={pending} path={download_path}
{... this.state.filesAvailable !== null ? {available: this.state.filesAvailable[sd_hash]} : {}} />);
}
@ -199,5 +199,4 @@ var MyFilesPage = React.createClass({
}
});
export default MyFilesPage;
export default MyFilesPage;

View file

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

View file

@ -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;

View file

@ -2,6 +2,8 @@
$spacing-vertical: 24px;
$padding-button: 12px;
$color-primary: #155B4A;
$color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000;
@ -18,7 +20,8 @@ $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);

View file

@ -1,6 +1,6 @@
@import "global";
@mixin text-link($color: $color-primary, $hover-opacity: 0.70, $mirror: false) {
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
color: $color;
.icon
{
@ -28,18 +28,7 @@
}
}
@if $mirror == false {
color: $color;
}
@else {
color: $color-bg;
background-color: $color;
position: absolute;
white-space: nowrap;
overflow: hidden;
top: 0px;
left: 0px;
}
color: $color;
}
.icon-fixed-width {
@ -156,16 +145,15 @@ input[type="text"], input[type="search"]
+ .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;
@ -184,37 +172,28 @@ input[type="text"], input[type="search"]
padding-left: 5px;
}
}
.button-block
{
cursor: pointer;
}
.button-primary
{
color: white;
background-color: $color-primary;
box-shadow: $default-box-shadow;
padding: 0 12px;
padding: 0 $padding-button;
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
padding: 0 12px;
padding: 0 $padding-button;
}
.button-download
{
padding: 0 6px;
text-decoration: none !important;
&.button-download--bg {
@include text-link(darken($color-primary, 1%));
}
&.button-download--fg {
@include text-link(darken($color-primary, 1%), $mirror: true);
}
}
.button-cancel
{
padding: 0 12px;
padding: 0 $padding-button;
}
.button-text
{
@ -378,11 +357,6 @@ input[type="text"], input[type="search"]
background: rgba(#000, .88);
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
@ -397,56 +371,12 @@ input[type="text"], input[type="search"]
word-break: break-all;
}
.menu {
position: fixed;
white-space: nowrap;
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
padding: $spacing-vertical;
border-radius: 2px;
.error-modal {
max-width: none;
width: 400px;
}
.menu__menu-item {
display: block;
text-decoration: none !important;
&:hover {
text-decoration: underline !important;
}
}
.file-tile--compact {
height: 180px;
}
.file-tile__row {
height: 24px * 7;
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
max-height: 400px;
overflow-y: hidden;
}
.file-tile__thumbnail {
max-width: 100%;
max-height: 24px * 7;
display: block;
margin-left: auto;
margin-right: auto;
}
.file-tile__title {
font-weight: bold;
}
.file-tile__title--compact {
font-size: 1.25em;
line-height: 1.15;
}
.file-tile__cost {
float: right;
}
.file-tile__description {
color: #444;
margin-top: 12px;
font-size: 0.9em;
}

View file

@ -3,6 +3,9 @@
@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 "page/_developer.scss";

View file

@ -0,0 +1,29 @@
@import "../global";
$color-download: #444;
.file-actions--stub
{
height: $height-button;
}
.file-actions__download-status-bar
{
padding-right: $padding-button;
padding-left: $padding-button;
position: relative;
color: $color-download;
}
.file-actions__download-status-bar-overlay
{
padding-right: $padding-button;
padding-left: $padding-button;
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 {
word-wrap: break-word;
max-width: 100%;