Seed Support #56
20 changed files with 410 additions and 225 deletions
2
lbry
2
lbry
|
@ -1 +1 @@
|
|||
Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f
|
||||
Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1
|
2
lbryum
2
lbryum
|
@ -1 +1 @@
|
|||
Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325
|
||||
Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739
|
|
@ -40,7 +40,7 @@ const SubmitEmailStage = React.createClass({
|
|||
return (
|
||||
<section>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="webmaster@toplbryfan.com"
|
||||
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="admin@toplbryfan.com"
|
||||
name="email" value={this.state.email}
|
||||
onChange={this.handleEmailChanged} />
|
||||
<div className="form-row-submit">
|
||||
|
@ -125,9 +125,9 @@ const WelcomeStage = React.createClass({
|
|||
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
|
||||
<section>
|
||||
<h3 className="modal__header">Welcome to LBRY.</h3>
|
||||
<p>LBRY is kind of like a centaur. Totally normal up top, and <em>way different</em> underneath.</p>
|
||||
<p>Using LBRY is like dating a centaur. Totally normal up top, and <em>way different</em> underneath.</p>
|
||||
<p>On the upper level, LBRY is like other popular video and media sites.</p>
|
||||
<p>Below, LBRY is like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- that is, you -- and no one else.</p>
|
||||
<p>Below, LBRY is like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- you -- and no one else.</p>
|
||||
<p>Thanks for being a part of it! Here's a nickel, kid.</p>
|
||||
<div style={{textAlign: "center", marginBottom: "12px"}}>
|
||||
<RewardLink type="new_user" button="primary" onRewardClaim={this.onRewardClaim} onRewardFailure={this.props.endAuth} />
|
||||
|
@ -180,7 +180,7 @@ export const AuthOverlay = React.createClass({
|
|||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
stage: "pending",
|
||||
stage: null,
|
||||
stageProps: {}
|
||||
};
|
||||
},
|
||||
|
|
|
@ -60,20 +60,84 @@ export let CurrencySymbol = React.createClass({
|
|||
|
||||
export let CreditAmount = React.createClass({
|
||||
propTypes: {
|
||||
amount: React.PropTypes.number,
|
||||
precision: React.PropTypes.number
|
||||
amount: React.PropTypes.number.isRequired,
|
||||
precision: React.PropTypes.number,
|
||||
label: React.PropTypes.bool
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
precision: 1,
|
||||
label: true,
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision ? this.props.precision : 1);
|
||||
var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
|
||||
return (
|
||||
<span className="credit-amount">
|
||||
<span>{formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'}</span>
|
||||
{ this.props.isEstimate ? <span style={estimateStyle}>*</span> : null }
|
||||
<span>
|
||||
{formattedAmount}
|
||||
{this.props.label ?
|
||||
(parseFloat(formattedAmount) == 1.0 ? ' credit' : ' credits') : '' }
|
||||
</span>
|
||||
{ this.props.isEstimate ? <span className="credit-amount__estimate" title="This is an estimate and does not include data fees">*</span> : null }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export let FilePrice = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
metadata: React.PropTypes.object,
|
||||
uri: React.PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
cost: null,
|
||||
isEstimate: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
lbry.getCostInfo(this.props.uri).then(({cost, includesData}) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
isEstimate: includesData,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
// If we get an error looking up cost information, do nothing
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.cost === null && this.props.metadata) {
|
||||
if (!this.props.metadata.fee) {
|
||||
return <span className="credit-amount">free</span>;
|
||||
} else {
|
||||
if (this.props.metadata.fee.currency === "LBC") {
|
||||
return <CreditAmount label={false} amount={this.props.metadata.fee.amount} isEstimate={true} />
|
||||
} else if (this.props.metadata.fee.currency === "USD") {
|
||||
return <span className="credit-amount">???</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
this.state.cost !== null ?
|
||||
<CreditAmount amount={this.state.cost} label={false} isEstimate={!this.state.isEstimate}/> :
|
||||
<span className="credit-amount">???</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var addressStyle = {
|
||||
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 {Modal} from './modal.js';
|
||||
import {FormField} from './form.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {DropDownMenu, DropDownMenuItem} from './menu.js';
|
||||
|
@ -25,7 +25,7 @@ let WatchLink = React.createClass({
|
|||
if (this.props.downloadStarted) {
|
||||
this.startVideo();
|
||||
} else {
|
||||
lbry.getCostInfo(this.props.uri, ({cost}) => {
|
||||
lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
||||
lbry.getBalance((balance) => {
|
||||
if (cost > balance) {
|
||||
this.setState({
|
||||
|
@ -79,7 +79,8 @@ let FileActionsRow = React.createClass({
|
|||
menuOpen: false,
|
||||
deleteChecked: false,
|
||||
attemptingDownload: false,
|
||||
attemptingRemove: false
|
||||
attemptingRemove: false,
|
||||
affirmedPurchase: false
|
||||
}
|
||||
},
|
||||
onFileInfoUpdate: function(fileInfo) {
|
||||
|
@ -95,14 +96,16 @@ let FileActionsRow = React.createClass({
|
|||
attemptingDownload: true,
|
||||
attemptingRemove: false
|
||||
});
|
||||
lbry.getCostInfo(this.props.uri, ({cost}) => {
|
||||
lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
||||
console.log(cost);
|
||||
console.log(this.props.uri);
|
||||
lbry.getBalance((balance) => {
|
||||
if (cost > balance) {
|
||||
this.setState({
|
||||
modal: 'notEnoughCredits',
|
||||
attemptingDownload: false,
|
||||
});
|
||||
} else {
|
||||
} else if (this.state.affirmedPurchase) {
|
||||
lbry.get({uri: this.props.uri}).then((streamInfo) => {
|
||||
if (streamInfo === null || typeof streamInfo !== 'object') {
|
||||
this.setState({
|
||||
|
@ -111,6 +114,11 @@ let FileActionsRow = React.createClass({
|
|||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
attemptingDownload: false,
|
||||
modal: 'affirmPurchase'
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -153,6 +161,13 @@ let FileActionsRow = React.createClass({
|
|||
attemptingDownload: false
|
||||
});
|
||||
},
|
||||
onAffirmPurchase: function() {
|
||||
this.setState({
|
||||
affirmedPurchase: true,
|
||||
modal: null
|
||||
});
|
||||
this.tryDownload();
|
||||
},
|
||||
openMenu: function() {
|
||||
this.setState({
|
||||
menuOpen: !this.state.menuOpen,
|
||||
|
@ -209,6 +224,10 @@ let FileActionsRow = React.createClass({
|
|||
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
|
||||
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
|
||||
</DropDownMenu> : '' }
|
||||
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
|
||||
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
|
||||
Confirm you want to purchase this bro.
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
|
||||
onConfirmed={this.closeModal}>
|
||||
You don't have enough LBRY credits to pay for this stream.
|
||||
|
@ -261,6 +280,7 @@ export let FileActions = React.createClass({
|
|||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
||||
|
||||
lbry.get_availability({uri: this.props.uri}, (availability) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
|
|
|
@ -3,52 +3,9 @@ import lbry from '../lbry.js';
|
|||
import uri from '../uri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {FileActions} from '../component/file-actions.js';
|
||||
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js';
|
||||
import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
|
||||
import UriIndicator from '../component/channel-indicator.js';
|
||||
|
||||
let FilePrice = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
|
||||
lbry.getCostInfo(this.props.uri, ({cost, includesData}) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
console.log('error from getCostInfo callback:', err)
|
||||
// If we get an error looking up cost information, do nothing
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
this.state.cost !== null ?
|
||||
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/> :
|
||||
<span className="credit-amount">...</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/*should be merged into FileTile once FileTile is refactored to take a single id*/
|
||||
export let FileTileStream = React.createClass({
|
||||
_fileInfoSubscribeId: null,
|
||||
|
@ -234,7 +191,7 @@ export let FileCardStream = React.createClass({
|
|||
const isConfirmed = typeof metadata == 'object';
|
||||
const title = isConfirmed ? metadata.title : lbryUri;
|
||||
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
||||
const primaryUrl = '?watch=' + lbryUri;
|
||||
const primaryUrl = '?show=' + lbryUri;
|
||||
return (
|
||||
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
||||
<div className="card__inner">
|
||||
|
@ -242,7 +199,7 @@ export let FileCardStream = React.createClass({
|
|||
<div className="card__title-identity">
|
||||
<h5><TruncatedText lines={1}>{title}</TruncatedText></h5>
|
||||
<div className="card__subtitle">
|
||||
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={this.props.uri} /></span> : null}
|
||||
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span> : null}
|
||||
<UriIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
|
||||
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
|
||||
</div>
|
||||
|
@ -288,11 +245,12 @@ export let FileTile = React.createClass({
|
|||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
|
||||
lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => {
|
||||
if (this._isMounted && claimInfo.value.stream.metadata) {
|
||||
lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
|
||||
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
|
||||
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
|
||||
// In case of a failed lookup, metadata will be null, in which case the component will never display
|
||||
this.setState({
|
||||
claimInfo: claimInfo,
|
||||
claimInfo: resolutionInfo.claim,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import lighthouse from './lighthouse.js';
|
||||
import jsonrpc from './jsonrpc.js';
|
||||
import uri from './uri.js';
|
||||
import {getLocal, setLocal} from './utils.js';
|
||||
import {getLocal, getSession, setSession, setLocal} from './utils.js';
|
||||
|
||||
const {remote} = require('electron');
|
||||
const menu = remote.require('./menu/main-menu');
|
||||
|
@ -93,7 +93,6 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall
|
|||
jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
|
||||
}
|
||||
|
||||
|
||||
//core
|
||||
lbry._connectPromise = null;
|
||||
lbry.connect = function() {
|
||||
|
@ -171,16 +170,6 @@ lbry.sendToAddress = function(amount, address, callback, errorCallback) {
|
|||
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
|
||||
}
|
||||
|
||||
lbry.resolveName = function(name, callback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('resolve_name', { 'name': name }, callback, () => {
|
||||
// For now, assume any error means the name was not resolved
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
lbry.getClaimInfo = function(name, callback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
|
@ -209,7 +198,7 @@ lbry.getPeersForBlobHash = function(blobHash, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
lbry.getCostInfo = function(lbryUri, callback, errorCallback) {
|
||||
lbry.getCostInfo = function(lbryUri) {
|
||||
/**
|
||||
* Takes a LBRY URI; will first try and calculate a total cost using
|
||||
* Lighthouse. If Lighthouse can't be reached, it just retrives the
|
||||
|
@ -223,24 +212,29 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) {
|
|||
if (!lbryUri) {
|
||||
throw new Error(`URI required.`);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
function getCost(lbryUri, size) {
|
||||
lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: size !== null,
|
||||
});
|
||||
}, reject);
|
||||
}
|
||||
|
||||
function getCost(lbryUri, size, callback, errorCallback) {
|
||||
lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: size !== null,
|
||||
});
|
||||
}, errorCallback);
|
||||
}
|
||||
const uriObj = uri.parseLbryUri(lbryUri);
|
||||
const name = uriObj.path || uriObj.name;
|
||||
|
||||
const uriObj = uri.parseLbryUri(lbryUri);
|
||||
const name = uriObj.path || uriObj.name;
|
||||
|
||||
lighthouse.get_size_for_name(name).then((size) => {
|
||||
getCost(name, size, callback, errorCallback);
|
||||
}, () => {
|
||||
getCost(name, null, callback, errorCallback);
|
||||
});
|
||||
lighthouse.get_size_for_name(name).then((size) => {
|
||||
if (size) {
|
||||
getCost(name, size);
|
||||
} else {
|
||||
getCost(name, null);
|
||||
}
|
||||
}, () => {
|
||||
getCost(name, null);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
lbry.getMyClaims = function(callback) {
|
||||
|
@ -615,6 +609,25 @@ lbry.claim_list_mine = function(params={}) {
|
|||
});
|
||||
}
|
||||
|
||||
lbry.resolve = function(params={}) {
|
||||
const claimCacheKey = 'resolve_claim_cache',
|
||||
claimCache = getSession(claimCacheKey, {})
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!params.uri) {
|
||||
throw "Resolve has hacked cache on top of it that requires a URI"
|
||||
}
|
||||
if (params.uri && claimCache[params.uri]) {
|
||||
resolve(claimCache[params.uri]);
|
||||
} else {
|
||||
lbry.call('resolve', params, function(data) {
|
||||
claimCache[params.uri] = data;
|
||||
setSession(claimCacheKey, claimCache)
|
||||
resolve(data)
|
||||
}, reject)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// lbry.get = function(params={}) {
|
||||
// return function(params={}) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -21,11 +21,7 @@ function getServers() {
|
|||
|
||||
function call(method, params, callback, errorCallback) {
|
||||
if (connectTryNum > maxQueryTries) {
|
||||
if (connectFailedCallback) {
|
||||
connectFailedCallback();
|
||||
} else {
|
||||
throw new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`);
|
||||
}
|
||||
errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,7 +44,7 @@ function call(method, params, callback, errorCallback) {
|
|||
}, () => {
|
||||
connectTryNum++;
|
||||
call(method, params, callback, errorCallback);
|
||||
});
|
||||
}, queryTimeout);
|
||||
}
|
||||
|
||||
const lighthouse = new Proxy({}, {
|
||||
|
|
|
@ -4,6 +4,7 @@ import uri from '../uri.js';
|
|||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import rewards from '../rewards.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import Modal from '../component/modal.js';
|
||||
|
||||
var PublishPage = React.createClass({
|
||||
|
@ -26,6 +27,7 @@ var PublishPage = React.createClass({
|
|||
// Calls API to update displayed list of channels. If a channel name is provided, will select
|
||||
// that channel at the same time (used immediately after creating a channel)
|
||||
lbry.channel_list_mine().then((channels) => {
|
||||
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL)
|
||||
this.setState({
|
||||
channels: channels,
|
||||
... channel ? {channel} : {}
|
||||
|
@ -348,7 +350,7 @@ var PublishPage = React.createClass({
|
|||
},
|
||||
componentWillMount: function() {
|
||||
this._updateChannelList();
|
||||
this._requestPublishReward();
|
||||
// this._requestPublishReward();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Publish";
|
||||
|
@ -510,7 +512,7 @@ var PublishPage = React.createClass({
|
|||
onChange={this.handleNewChannelBidChange}
|
||||
value={this.state.newChannelBid} />
|
||||
<div className="form-row-submit">
|
||||
<Link button="primary" label={!this.state.creatingChannel ? 'Creating identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
|
||||
<Link button="primary" label={!this.state.creatingChannel ? 'Create identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
|
|
|
@ -2,105 +2,45 @@ import React from 'react';
|
|||
import lbry from '../lbry.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import uri from '../uri.js';
|
||||
import {CreditAmount, Thumbnail} from '../component/common.js';
|
||||
import {Video} from '../page/watch.js'
|
||||
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
|
||||
import {FileActions} from '../component/file-actions.js';
|
||||
import {Link} from '../component/link.js';
|
||||
|
||||
var formatItemImgStyle = {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
display: 'block',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: '5px',
|
||||
};
|
||||
import UriIndicator from '../component/channel-indicator.js';
|
||||
|
||||
var FormatItem = React.createClass({
|
||||
propTypes: {
|
||||
metadata: React.PropTypes.object,
|
||||
contentType: React.PropTypes.string,
|
||||
cost: React.PropTypes.number,
|
||||
uri: React.PropTypes.string,
|
||||
outpoint: React.PropTypes.string,
|
||||
costIncludesData: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
const {thumbnail, author, title, description, language, license} = this.props.metadata;
|
||||
const mediaType = lbry.getMediaType(this.props.contentType);
|
||||
var costIncludesData = this.props.costIncludesData;
|
||||
var cost = this.props.cost || 0.0;
|
||||
|
||||
return (
|
||||
<div className="row-fluid">
|
||||
<div className="span4">
|
||||
<Thumbnail src={thumbnail} alt={'Photo for ' + title} style={formatItemImgStyle} />
|
||||
</div>
|
||||
<div className="span8">
|
||||
<p>{description}</p>
|
||||
<section>
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Content-Type</td><td>{this.props.contentType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cost</td><td><CreditAmount amount={cost} isEstimate={!costIncludesData}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td><td>{author}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Language</td><td>{language}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>License</td><td>{license}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<FileActions uri={this._uri} outpoint={this.props.outpoint} metadata={this.props.metadata} contentType={this.props.contentType} />
|
||||
<section>
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Content-Type</td><td>{this.props.contentType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td><td>{author}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Language</td><td>{language}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>License</td><td>{license}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var FormatsSection = React.createClass({
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
outpoint: React.PropTypes.string,
|
||||
metadata: React.PropTypes.object,
|
||||
contentType: React.PropTypes.string,
|
||||
cost: React.PropTypes.number,
|
||||
costIncludesData: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
if(this.props.metadata == null)
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<h2>Sorry, no results found for "{name}".</h2>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.props.metadata.thumbnail ? <div style={{backgroundImage: this.props.metadata.thumbnail}}></div> : '' }
|
||||
<h1>{this.props.metadata.title}</h1>
|
||||
<div className="meta">{this.props.uri}</div>
|
||||
{/* In future, anticipate multiple formats, just a guess at what it could look like
|
||||
// var formats = this.props.metadata.formats
|
||||
// return (<tbody>{formats.map(function(format,i){ */}
|
||||
<FormatItem metadata={this.props.metadata} contentType={this.props.contentType} cost={this.props.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.props.costIncludesData} />
|
||||
{/* })}</tbody>); */}
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
|
||||
var ShowPage = React.createClass({
|
||||
let ShowPage = React.createClass({
|
||||
_uri: null,
|
||||
|
||||
propTypes: {
|
||||
|
@ -110,6 +50,8 @@ var ShowPage = React.createClass({
|
|||
return {
|
||||
metadata: null,
|
||||
contentType: null,
|
||||
hasSignature: false,
|
||||
signatureIsValid: false,
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
uriLookupComplete: null,
|
||||
|
@ -119,16 +61,19 @@ var ShowPage = React.createClass({
|
|||
this._uri = uri.normalizeLbryUri(this.props.uri);
|
||||
document.title = this._uri;
|
||||
|
||||
lbry.resolve({uri: this._uri}).then(({txid, nout, claim: {value: {stream: {metadata, source: {contentType}}}}}) => {
|
||||
lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
|
||||
console.log({txid, nout, claim: {value: {stream: {metadata, source: {contentType}}}}} );
|
||||
this.setState({
|
||||
outpoint: txid + ':' + nout,
|
||||
metadata: metadata,
|
||||
hasSignature: has_signature,
|
||||
signatureIsValid: signature_is_valid,
|
||||
contentType: contentType,
|
||||
uriLookupComplete: true,
|
||||
});
|
||||
});
|
||||
|
||||
lbry.getCostInfo(this._uri, ({cost, includesData}) => {
|
||||
lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
|
@ -140,17 +85,51 @@ var ShowPage = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
// <div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
|
||||
|
||||
const
|
||||
metadata = this.state.uriLookupComplete ? this.state.metadata : null,
|
||||
title = this.state.uriLookupComplete ? metadata.title : this._uri;
|
||||
|
||||
return (
|
||||
<main>
|
||||
{this.state.uriLookupComplete ? (
|
||||
<FormatsSection uri={this._uri} outpoint={this.state.outpoint} metadata={this.state.metadata} cost={this.state.cost} costIncludesData={this.state.costIncludesData} contentType={this.state.contentType} />
|
||||
) : (
|
||||
<div>
|
||||
<h2>No content</h2>
|
||||
There is no content available at <strong>{this._uri}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="?report" label="report a bug" />. Thanks!
|
||||
<main className="constrained-page">
|
||||
<section className="show-page-media">
|
||||
{ this.props.contentType && this.props.contentType.startsWith('video/') ?
|
||||
<Video className="video-embedded" uri={this._uri} /> :
|
||||
<Thumbnail src={metadata.thumbnail} /> }
|
||||
</section>
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity">
|
||||
<span style={{float: "right"}}><FilePrice uri={this._uri} /></span>
|
||||
<h1>{title}</h1>
|
||||
{ this.state.uriLookupComplete ?
|
||||
<div>
|
||||
<div className="card__subtitle">
|
||||
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={this.state.metadata} contentType={this.state.contentType} />
|
||||
</div>
|
||||
</div> : '' }
|
||||
</div>
|
||||
)}
|
||||
</main>);
|
||||
{ this.state.uriLookupComplete ?
|
||||
<div>
|
||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||
{metadata.description}
|
||||
</div>
|
||||
</div>
|
||||
: <BusyMessage message="Loading..." /> }
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData} />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,105 @@ import LoadScreen from '../component/load_screen.js'
|
|||
const fs = require('fs');
|
||||
const VideoStream = require('videostream');
|
||||
|
||||
export let Video = React.createClass({
|
||||
_isMounted: false,
|
||||
_controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
|
||||
_controlsHideTimeout: null,
|
||||
_outpoint: null,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
downloadStarted: false,
|
||||
readyToPlay: false,
|
||||
loadStatusMessage: "Requesting stream",
|
||||
mimeType: null,
|
||||
controlsShown: false,
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
lbry.get({uri: this.props.uri}).then((fileInfo) => {
|
||||
this._outpoint = fileInfo.outpoint;
|
||||
this.updateLoadStatus();
|
||||
});
|
||||
},
|
||||
handleMouseMove: function() {
|
||||
if (this._controlsTimeout) {
|
||||
clearTimeout(this._controlsTimeout);
|
||||
}
|
||||
|
||||
if (!this.state.controlsShown) {
|
||||
this.setState({
|
||||
controlsShown: true,
|
||||
});
|
||||
}
|
||||
this._controlsTimeout = setTimeout(() => {
|
||||
if (!this.isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
controlsShown: false,
|
||||
});
|
||||
}, this._controlsHideDelay);
|
||||
},
|
||||
handleMouseLeave: function() {
|
||||
if (this._controlsTimeout) {
|
||||
clearTimeout(this._controlsTimeout);
|
||||
}
|
||||
|
||||
if (this.state.controlsShown) {
|
||||
this.setState({
|
||||
controlsShown: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
updateLoadStatus: function() {
|
||||
lbry.file_list({
|
||||
outpoint: this._outpoint,
|
||||
full_status: true,
|
||||
}).then(([status]) => {
|
||||
if (!status || status.written_bytes == 0) {
|
||||
// Download hasn't started yet, so update status message (if available) then try again
|
||||
// TODO: Would be nice to check if we have the MOOV before starting playing
|
||||
if (status) {
|
||||
this.setState({
|
||||
loadStatusMessage: status.message
|
||||
});
|
||||
}
|
||||
setTimeout(() => { this.updateLoadStatus() }, 250);
|
||||
} else {
|
||||
this.setState({
|
||||
readyToPlay: true,
|
||||
mimeType: status.mime_type,
|
||||
})
|
||||
return
|
||||
const mediaFile = {
|
||||
createReadStream: function (opts) {
|
||||
// Return a readable stream that provides the bytes
|
||||
// between offsets "start" and "end" inclusive
|
||||
console.log('Stream between ' + opts.start + ' and ' + opts.end + '.');
|
||||
return fs.createReadStream(status.download_path, opts)
|
||||
}
|
||||
};
|
||||
var elem = this.refs.video;
|
||||
var videostream = VideoStream(mediaFile, elem);
|
||||
elem.play();
|
||||
}
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div className={this.props.className}>{
|
||||
!this.state.readyToPlay || true ?
|
||||
<span>this is the world's world loading message and we shipped our software with it anyway... seriously it is actually loading... it might take a while though</span> :
|
||||
<video className={this.props.className} id="video" ref="video"></video>
|
||||
}</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
var WatchPage = React.createClass({
|
||||
_isMounted: false,
|
||||
|
|
|
@ -22,48 +22,67 @@ rewards.TYPE_NEW_DEVELOPER = "new_developer",
|
|||
rewards.TYPE_FIRST_PUBLISH = "first_publish";
|
||||
|
||||
rewards.claimReward = function (type) {
|
||||
|
||||
function requestReward(resolve, reject, params) {
|
||||
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
|
||||
const
|
||||
message = rewardMessage(type, RewardAmount),
|
||||
result = {
|
||||
type: type,
|
||||
amount: RewardAmount,
|
||||
message: message
|
||||
};
|
||||
|
||||
// Display global notice
|
||||
document.dispatchEvent(new CustomEvent('globalNotice', {
|
||||
detail: {
|
||||
message: message,
|
||||
linkText: "Show All",
|
||||
linkTarget: "?rewards",
|
||||
isError: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add more events here to display other places
|
||||
|
||||
resolve(result);
|
||||
}, reject);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry.get_new_address().then((address) => {
|
||||
const params = {
|
||||
reward_type: type,
|
||||
wallet_address: address,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'first_channel':
|
||||
//params.transaction_id = RelevantTransactionID;
|
||||
case rewards.TYPE_FIRST_CHANNEL:
|
||||
lbry.claim_list_mine().then(function(channels) {
|
||||
if (channels.length) {
|
||||
params.transaction_id = channels[0].txid;
|
||||
requestReward(resolve, reject, params)
|
||||
} else {
|
||||
reject(new Error("Please create a channel identity first."))
|
||||
}
|
||||
}).catch(reject)
|
||||
break;
|
||||
|
||||
case 'first_purchase':
|
||||
//params.transaction_id = RelevantTransactionID;
|
||||
// lbry.claim_list_mine().then(function(channels) {
|
||||
// if (channels.length) {
|
||||
// requestReward(resolve, reject, {transaction_id: channels[0].txid})
|
||||
// }
|
||||
// }).catch(reject)
|
||||
break;
|
||||
|
||||
case 'first_channel':
|
||||
//params.transaction_id = RelevantTransactionID;
|
||||
break;
|
||||
|
||||
default:
|
||||
requestReward(resolve, reject, params);
|
||||
}
|
||||
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
|
||||
const
|
||||
message = rewardMessage(type, RewardAmount),
|
||||
result = {
|
||||
type: type,
|
||||
amount: RewardAmount,
|
||||
message: message
|
||||
};
|
||||
|
||||
// Display global notice
|
||||
document.dispatchEvent(new CustomEvent('globalNotice', {
|
||||
detail: {
|
||||
message: message,
|
||||
linkText: "Show All",
|
||||
linkTarget: "?rewards",
|
||||
isError: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add more events here to display other places
|
||||
|
||||
resolve(result);
|
||||
}, reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ export function setLocal(key, value) {
|
|||
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
|
||||
* is not set yet.
|
||||
*/
|
||||
export function getSession(key) {
|
||||
export function getSession(key, fallback=undefined) {
|
||||
const itemRaw = sessionStorage.getItem(key);
|
||||
return itemRaw === null ? undefined : JSON.parse(itemRaw);
|
||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,6 +62,10 @@ $drawer-width: 220px;
|
|||
font-weight: bold;
|
||||
color: $color-money;
|
||||
}
|
||||
.credit-amount--estimate {
|
||||
font-style: italic;
|
||||
color: $color-meta-light;
|
||||
}
|
||||
#drawer-handle
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
|
@ -188,6 +192,12 @@ nav.sub-header
|
|||
main
|
||||
{
|
||||
padding: $spacing-vertical;
|
||||
&.constrained-page
|
||||
{
|
||||
max-width: $width-page-constrained;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@ $max-text-width: 660px;
|
|||
$height-header: $spacing-vertical * 2.5;
|
||||
$height-button: $spacing-vertical * 1.5;
|
||||
|
||||
$width-page-constrained: 800px;
|
||||
|
||||
$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);
|
||||
$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
@import "component/_modal.scss";
|
||||
@import "component/_modal-page.scss";
|
||||
@import "component/_snack-bar.scss";
|
||||
@import "component/_video.scss";
|
||||
@import "page/_developer.scss";
|
||||
@import "page/_watch.scss";
|
||||
@import "page/_reward.scss";
|
||||
@import "page/_show.scss";
|
||||
|
|
|
@ -5,7 +5,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
|
|||
.card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
max-width: $width-page-constrained;
|
||||
background: $color-bg;
|
||||
box-shadow: $default-box-shadow;
|
||||
border-radius: 2px;
|
||||
|
@ -59,6 +59,9 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
|
|||
margin-bottom: $spacing-vertical * 2/3;
|
||||
padding: 0 $padding-card-horizontal;
|
||||
}
|
||||
.card__subtext--allow-newlines {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.card__subtext--two-lines {
|
||||
height: $font-size * 0.9 * $font-line-height * 2;
|
||||
}
|
||||
|
@ -118,7 +121,7 @@ $height-card-small: $spacing-vertical * 15;
|
|||
{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
max-width: $width-page-constrained;
|
||||
padding: $spacing-vertical / 2;
|
||||
}
|
||||
|
||||
|
|
12
ui/scss/component/_video.scss
Normal file
12
ui/scss/component/_video.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
video {
|
||||
border: 1px solid red;
|
||||
object-fill: contain;
|
||||
}
|
||||
|
||||
.video-embedded {
|
||||
max-width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 63%;
|
||||
video {
|
||||
}
|
||||
}
|
6
ui/scss/page/_show.scss
Normal file
6
ui/scss/page/_show.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import "../global";
|
||||
|
||||
.show-page-media {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
Loading…
Reference in a new issue