Early Access app #46

Merged
kauffj merged 51 commits from rewards into master 2017-04-17 16:02:53 +02:00
58 changed files with 2975 additions and 1605 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ dist
/app/node_modules
/build/venv
/lbry-app-venv
/lbry-venv
/daemon/build
/daemon/venv
/daemon/requirements.txt

View file

@ -62,9 +62,9 @@ function getPidsForProcessName(name) {
}
function createWindow () {
win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary
win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
win.maximize()
//win.webContents.openDevTools()
win.webContents.openDevTools();
win.loadURL(`file://${__dirname}/dist/index.html`)
win.on('closed', () => {
win = null

4
doitagain.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
rm -rf ~/.lbrynet/
rm -rf ~/.lbryum/
./node_modules/.bin/electron app

2
lbry

@ -1 +1 @@
Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f
Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1

2
lbryum

@ -1 +1 @@
Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325
Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739

View file

@ -7,13 +7,12 @@ import HelpPage from './page/help.js';
import WatchPage from './page/watch.js';
import ReportPage from './page/report.js';
import StartPage from './page/start.js';
import ClaimCodePage from './page/claim_code.js';
import ReferralPage from './page/referral.js';
import RewardsPage from './page/rewards.js';
import RewardPage from './page/reward.js';
import WalletPage from './page/wallet.js';
import ShowPage from './page/show.js';
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';
@ -38,17 +37,11 @@ var App = React.createClass({
message: 'Error message',
data: 'Error data',
},
_fullScreenPages: ['watch'],
_upgradeDownloadItem: null,
_isMounted: false,
_version: null,
// Temporary workaround since electron-dl throws errors when you try to get the filename
getDefaultProps: function() {
return {
address: window.location.search
};
},
getUpdateUrl: function() {
switch (process.platform) {
case 'darwin':
@ -87,7 +80,7 @@ var App = React.createClass({
var match, param, val, viewingPage, pageArgs,
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
return Object.assign(this.getViewingPageAndArgs(this.props.address), {
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
errorInfo: null,
modal: null,
@ -112,6 +105,8 @@ var App = React.createClass({
if (target.matches('a[href^="?"]')) {
event.preventDefault();
if (this._isMounted) {
history.pushState({}, document.title, target.getAttribute('href'));
this.registerHistoryPop();
this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
}
}
@ -153,6 +148,11 @@ var App = React.createClass({
componentWillUnmount: function() {
this._isMounted = false;
},
registerHistoryPop: function() {
window.addEventListener("popstate", function() {
this.setState(this.getViewingPageAndArgs(location.pathname));
}.bind(this));
},
handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
@ -231,14 +231,12 @@ var App = React.createClass({
case 'wallet':
case 'send':
case 'receive':
case 'claim':
case 'referral':
case 'rewards':
return {
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?claim' : 'Claim Beta Code',
'?referral' : 'Check Referral Credit',
'?rewards': 'Rewards',
};
case 'downloaded':
case 'published':
@ -258,8 +256,6 @@ var App = React.createClass({
return <SettingsPage />;
case 'help':
return <HelpPage />;
case 'watch':
return <WatchPage uri={this.state.pageArgs} />;
case 'report':
return <ReportPage />;
case 'downloaded':
@ -268,10 +264,8 @@ var App = React.createClass({
return <FileListPublished />;
case 'start':
return <StartPage />;
case 'claim':
return <ClaimCodePage />;
case 'referral':
return <ReferralPage />;
case 'rewards':
return <RewardsPage />;
case 'wallet':
case 'send':
case 'receive':
@ -284,7 +278,7 @@ var App = React.createClass({
return <DeveloperPage />;
case 'discover':
default:
return <DiscoverPage {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
return <DiscoverPage showWelcome={this.state.justRegistered} {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
}
},
render: function() {
@ -293,7 +287,7 @@ var App = React.createClass({
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
return (
this.state.viewingPage == 'watch' ?
this._fullScreenPages.includes(this.state.viewingPage) ?
mainContent :
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />

246
ui/js/component/auth.js Normal file
View file

@ -0,0 +1,246 @@
import React from 'react';
import lbryio from '../lbryio.js';
import Modal from './modal.js';
import ModalPage from './modal-page.js';
import {Link, RewardLink} from '../component/link.js';
import {FormField, FormRow} from '../component/form.js';
import {CreditAmount} from '../component/common.js';
import rewards from '../rewards.js';
const SubmitEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
email: '',
submitting: false
};
},
handleEmailChanged: function(event) {
this.setState({
email: event.target.value,
});
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => {
this.props.onEmailSaved();
}, (error) => {
if (this._emailRow) {
this._emailRow.showError(error.message)
}
this.setState({ submitting: false });
});
},
render: function() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<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">
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
</form>
</section>
);
}
});
const ConfirmEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
code: '',
submitting: false,
errorMessage: null,
};
},
handleCodeChanged: function(event) {
this.setState({
code: event.target.value,
});
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
const onSubmitError = function(error) {
if (this._codeRow) {
this._codeRow.showError(error.message)
}
this.setState({ submitting: false });
}.bind(this)
lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then((userEmail) => {
if (userEmail.IsVerified) {
this.props.onEmailConfirmed();
} else {
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
}
}, onSubmitError);
},
render: function() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
helper="A verification code is required to access this version."/>
<div className="form-row-submit">
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
</form>
</section>
);
}
});
const WelcomeStage = React.createClass({
propTypes: {
endAuth: React.PropTypes.func,
},
getInitialState: function() {
return {
hasReward: false,
rewardAmount: null,
}
},
onRewardClaim: function(reward) {
console.log(reward);
this.setState({
hasReward: true,
rewardAmount: reward.amount
})
},
render: function() {
return (
!this.state.hasReward ?
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<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 controlled by its users -- you -- through the power of blockchain and decentralization.</p>
<p>Thanks for making it possible! 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} />
</div>
</section>
</Modal> :
<Modal type="alert" overlayClassName="modal-overlay modal-overlay--clear" isOpen={true} contentLabel="Welcome to LBRY" {...this.props} onConfirmed={this.props.endAuth}>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>You earned a reward of <CreditAmount amount={this.state.rewardAmount} label={false} /> LBRY credits, or <em>LBC</em>.</p>
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
</section>
</Modal>
);
}
});
const ErrorStage = React.createClass({
render: function() {
return (
<section>
<p>An error was encountered that we cannot continue from.</p>
<p>At least we're earning the name beta.</p>
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
</section>
);
}
});
const PendingStage = React.createClass({
render: function() {
return (
<section>
<p>Preparing for first access <span className="busy-indicator"></span></p>
</section>
);
}
});
export const AuthOverlay = React.createClass({
_stages: {
pending: PendingStage,
error: ErrorStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage
},
getInitialState: function() {
return {
stage: null,
stageProps: {}
};
},
endAuth: function() {
this.setState({
stage: null
});
},
componentWillMount: function() {
lbryio.authenticate().then(function(user) {
if (!user.HasVerifiedEmail) { //oops I fucked this up
this.setState({
stage: "email",
stageProps: {
onEmailSaved: function() {
this.setState({
stage: "confirm",
stageProps: {
onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this)
}
})
}.bind(this)
}
})
} else {
lbryio.call('reward', 'list', {}).then(function(userRewards) {
userRewards.filter(function(reward) {
return reward.RewardType == "new_user" && reward.TransactionID;
}).length ?
this.endAuth() :
this.setState({ stage: "welcome" })
}.bind(this));
}
}.bind(this)).catch((err) => {
this.setState({
stage: "error",
stageProps: { errorText: err.message }
})
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
message: err.message,
data: err.stack
}
}));
})
},
render: function() {
if (!this.state.stage) {
return null;
}
const StageContent = this._stages[this.state.stage];
return (
this.state.stage != "welcome" ?
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
<h1>LBRY Early Access</h1>
<StageContent {...this.state.stageProps} />
</ModalPage> :
<StageContent endAuth={this.endAuth} {...this.state.stageProps} />
);
}
});

View file

@ -3,20 +3,18 @@ import lbry from '../lbry.js';
import uri from '../uri.js';
import {Icon} from './common.js';
const ChannelIndicator = React.createClass({
const UriIndicator = React.createClass({
propTypes: {
uri: React.PropTypes.string.isRequired,
hasSignature: React.PropTypes.bool.isRequired,
signatureIsValid: React.PropTypes.bool,
},
render: function() {
if (!this.props.hasSignature) {
return null;
}
const uriObj = uri.parseLbryUri(this.props.uri);
if (!uriObj.isChannel) {
return null;
if (!this.props.hasSignature || !uriObj.isChannel) {
return <span className="empty">Anonymous</span>;
}
const channelUriObj = Object.assign({}, uriObj);
@ -25,7 +23,6 @@ const ChannelIndicator = React.createClass({
let icon, modifier;
if (this.props.signatureIsValid) {
icon = 'icon-check-circle';
modifier = 'valid';
} else {
icon = 'icon-times-circle';
@ -33,11 +30,13 @@ const ChannelIndicator = React.createClass({
}
return (
<span>
by <strong>{channelUri}</strong> {' '}
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} />
{channelUri} {' '}
{ !this.props.signatureIsValid ?
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
'' }
</span>
);
}
});
export default ChannelIndicator;
export default UriIndicator;

View file

@ -54,31 +54,86 @@ export let BusyMessage = React.createClass({
}
});
var creditAmountStyle = {
color: '#216C2A',
fontWeight: 'bold',
fontSize: '0.8em'
}, estimateStyle = {
fontSize: '0.8em',
color: '#aaa',
};
export let CurrencySymbol = React.createClass({
render: function() { return <span>LBC</span>; }
});
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 style={creditAmountStyle}>{formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'}</span>
{ this.props.isEstimate ? <span style={estimateStyle}> (est)</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>
);
}
});
@ -131,6 +186,9 @@ export let Thumbnail = React.createClass({
this._isMounted = false;
},
render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
const className = this.props.className ? this.props.className : '',
otherProps = Object.assign({}, this.props)
delete otherProps.className;
return <img ref="img" onError={this.handleError} {...otherProps} className={className} src={this.state.imageUri} />
},
});

View file

@ -55,7 +55,7 @@ var Drawer = React.createClass({
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<DrawerItem href="?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href="?wallet" subPages={['send', 'receive', 'rewards']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav>

View file

@ -2,66 +2,13 @@ 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 {Modal} from './modal.js';
import {FormField} from './form.js';
import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js';
const {shell} = require('electron');
let WatchLink = React.createClass({
propTypes: {
uri: React.PropTypes.string,
downloadStarted: React.PropTypes.bool,
},
startVideo: function() {
window.location = '?watch=' + this.props.uri;
},
handleClick: function() {
this.setState({
loading: true,
});
if (this.props.downloadStarted) {
this.startVideo();
} else {
lbry.getCostInfo(this.props.uri, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
loading: false,
});
} else {
this.startVideo();
}
});
});
}
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<div className="button-set-item">
<Link button="primary" disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} />
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
</div>
);
}
});
let FileActionsRow = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
@ -79,7 +26,7 @@ let FileActionsRow = React.createClass({
menuOpen: false,
deleteChecked: false,
attemptingDownload: false,
attemptingRemove: false
attemptingRemove: false,
}
},
onFileInfoUpdate: function(fileInfo) {
@ -95,14 +42,14 @@ let FileActionsRow = React.createClass({
attemptingDownload: true,
attemptingRemove: false
});
lbry.getCostInfo(this.props.uri, ({cost}) => {
lbry.getCostInfo(this.props.uri).then(({cost}) => {
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 +58,11 @@ let FileActionsRow = React.createClass({
});
}
});
} else {
this.setState({
attemptingDownload: false,
modal: 'affirmPurchase'
})
}
});
});
@ -153,6 +105,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,
@ -198,9 +157,6 @@ let FileActionsRow = React.createClass({
return (
<div>
{this.props.contentType && this.props.contentType.startsWith('video/')
? <WatchLink uri={this.props.uri} downloadStarted={!!this.state.fileInfo} />
: null}
{this.state.fileInfo !== null || this.state.fileInfo.isMine
? linkBlock
: null}
@ -209,6 +165,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.
@ -220,7 +180,7 @@ let FileActionsRow = React.createClass({
<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>
<p>Are you sure you'd like to remove <cite>{this.props.metadata ? this.props.metadata.title : this.props.uri}</cite> from LBRY?</p>
<label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
</Modal>
@ -261,6 +221,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({
@ -294,7 +255,7 @@ export let FileActions = React.createClass({
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
contentType={this.props.contentType} />
: <div>
<div className="button-set-item empty">This file is not currently available.</div>
<div className="button-set-item empty">Content unavailable.</div>
<ToolTip label="Why?"
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
className="button-set-item" />

View file

@ -3,56 +3,8 @@ 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 ChannelIndicator 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() {
if (this.state.cost === null)
{
return null;
}
return (
<span className="file-tile__cost">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
);
}
});
import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
import UriIndicator from '../component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({
@ -61,7 +13,7 @@ export let FileTileStream = React.createClass({
propTypes: {
uri: React.PropTypes.string,
metadata: React.PropTypes.object.isRequired,
metadata: React.PropTypes.object,
contentType: React.PropTypes.string.isRequired,
outpoint: React.PropTypes.string,
hasSignature: React.PropTypes.bool,
@ -117,42 +69,41 @@ export let FileTileStream = React.createClass({
}
},
render: function() {
console.log('rendering.')
if (this.state.isHidden) {
console.log('hidden, so returning null')
return null;
}
console.log("inside FileTileStream. metadata is", this.props.metadata)
const lbryUri = uri.normalizeLbryUri(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = typeof metadata == 'object';
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : lbryUri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card-content file-tile__row"}>
<div className="span3">
<a href={'?show=' + lbryUri}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a>
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card__inner file-tile__row"}>
<div className="span3 file-tile__thumbnail-container">
<a href={'?show=' + lbryUri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + (title || this.props.uri)} /></a>
</div>
<div className="span9">
<div className="card__title-primary">
{ !this.props.hidePrice
? <FilePrice uri={this.props.uri} />
: null}
<div className="meta"><a href={'?show=' + this.props.uri}>{lbryUri}</a></div>
<h3 className="file-tile__title">
<h3>
<a href={'?show=' + this.props.uri}>
<TruncatedText lines={1}>
{title}
</TruncatedText>
</a>
</h3>
<ChannelIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
</div>
<div className="card__actions">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
<div className="card__content">
<p className="file-tile__description">
<TruncatedText lines={3}>
<TruncatedText lines={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
@ -160,6 +111,7 @@ export let FileTileStream = React.createClass({
</p>
</div>
</div>
</div>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
@ -173,6 +125,108 @@ export let FileTileStream = React.createClass({
}
});
export let FileCardStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
_metadata: null,
propTypes: {
uri: React.PropTypes.string,
claimInfo: React.PropTypes.object,
outpoint: 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,
hasSignature: false,
}
},
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
}
},
componentWillUnmount: function() {
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
onFileInfoUpdate: function(fileInfo) {
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
this.setState({
isHidden: true
});
}
},
handleMouseOver: function() {
this.setState({
hovered: true,
});
},
handleMouseOut: function() {
this.setState({
hovered: false,
});
},
render: function() {
if (this.state.isHidden) {
return null;
}
const lbryUri = uri.normalizeLbryUri(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : lbryUri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
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">
<a href={primaryUrl} className="card__link">
<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} metadata={metadata} /></span> : null}
<UriIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
</div>
</div>
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</div>
</a>
{this.state.showNsfwHelp && this.state.hovered
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
</p>
</div>
: null}
</div>
</section>
);
}
});
export let FileTile = React.createClass({
_isMounted: false,
@ -191,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,
});
}
});
@ -210,7 +265,11 @@ export let FileTile = React.createClass({
const {txid, nout, has_signature, signature_is_valid,
value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo;
return <FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
return this.props.displayStyle == 'card' ?
<FileCardStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props}/> :
<FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props} />;
}
});

View file

@ -1,28 +1,32 @@
import React from 'react';
import {Icon} from './common.js';
var requiredFieldWarningStyle = {
color: '#cc0000',
transition: 'opacity 400ms ease-in',
};
var formFieldCounter = 0,
formFieldNestedLabelTypes = ['radio', 'checkbox'];
var FormField = React.createClass({
function formFieldId() {
return "form-field-" + (++formFieldCounter);
}
export let FormField = React.createClass({
_fieldRequiredText: 'This field is required',
_type: null,
_element: null,
propTypes: {
type: React.PropTypes.string.isRequired,
hidden: React.PropTypes.bool,
prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool
},
getInitialState: function() {
return {
adviceState: 'hidden',
adviceText: null,
isError: null,
errorMessage: null,
}
},
componentWillMount: function() {
if (['text', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
this._element = 'input';
this._type = this.props.type;
} else if (this.props.type == 'text-number') {
@ -33,25 +37,11 @@ var FormField = React.createClass({
this._element = this.props.type;
}
},
showAdvice: function(text) {
showError: function(text) {
this.setState({
adviceState: 'shown',
adviceText: text,
isError: true,
errorMessage: text,
});
setTimeout(() => {
this.setState({
adviceState: 'fading',
});
setTimeout(() => {
this.setState({
adviceState: 'hidden',
});
}, 450);
}, 5000);
},
warnRequired: function() {
this.showAdvice(this._fieldRequiredText);
},
focus: function() {
this.refs.field.focus();
@ -60,7 +50,8 @@ var FormField = React.createClass({
if (this.props.type == 'checkbox') {
return this.refs.field.checked;
} else if (this.props.type == 'file') {
return this.refs.field.files[0].path;
return this.refs.field.files.length && this.refs.field.files[0].path ?
this.refs.field.files[0].path : null;
} else {
return this.refs.field.value;
}
@ -70,45 +61,94 @@ var FormField = React.createClass({
},
render: function() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props);
delete otherProps.type;
delete otherProps.hidden;
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
elementId = this.props.id ? this.props.id : formFieldId(),
renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
return (
!this.props.hidden
? <div className="form-field-container">
<this._element type={this._type} className="form-field" name={this.props.name} ref="field" placeholder={this.props.placeholder}
className={'form-field--' + this.props.type + ' ' + (this.props.className || '')}
delete otherProps.type;
delete otherProps.label;
delete otherProps.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
{...otherProps}>
{this.props.children}
</this._element>
<FormFieldAdvice field={this.refs.field} state={this.state.adviceState}>{this.state.adviceText}</FormFieldAdvice>
</div>
: null
);
}
});
</this._element>;
var FormFieldAdvice = React.createClass({
return <div className="form-field">
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
{ renderElementInsideLabel ?
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
{element}
{this.props.label}
</label> :
element }
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})
export let FormRow = React.createClass({
_fieldRequiredText: 'This field is required',
propTypes: {
state: React.PropTypes.string.isRequired,
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
// helper: React.PropTypes.html,
},
getInitialState: function() {
return {
isError: false,
errorMessage: null,
}
},
showError: function(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
showRequiredError: function() {
this.showError(this._fieldRequiredText);
},
clearError: function(text) {
this.setState({
isError: false,
errorMessage: ''
});
},
getValue: function() {
return this.refs.field.getValue();
},
getSelectedElement: function() {
return this.refs.field.getSelectedElement();
},
focus: function() {
this.refs.field.focus();
},
render: function() {
return (
this.props.state != 'hidden'
? <div className="form-field-advice-container">
<div className={'form-field-advice' + (this.props.state == 'fading' ? ' form-field-advice--fading' : '')}>
<Icon icon="icon-caret-up" className="form-field-advice__arrow" />
<div className="form-field-advice__content-container">
<span className="form-field-advice__content">
{this.props.children}
</span>
</div>
</div>
</div>
: null
);
}
});
const fieldProps = Object.assign({}, this.props),
elementId = formFieldId(),
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
export default FormField;
if (!renderLabelInFormField) {
delete fieldProps.label;
}
delete fieldProps.helper;
return <div className="form-row">
{ this.props.label && !renderLabelInFormField ?
<div className={"form-row__label-row " + (this.props.labelPrefix ? "form-row__label-row--prefix" : "") }>
<label htmlFor={elementId} className={"form-field__label " + (this.state.isError ? 'form-field__label--error' : '')}>
{this.props.label}
</label>
</div> : '' }
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
{ !this.state.isError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})

View file

@ -1,5 +1,6 @@
import React from 'react';
import {Link} from './link.js';
import {Icon} from './common.js';
var Header = React.createClass({
getInitialState: function() {
@ -52,6 +53,7 @@ var Header = React.createClass({
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
<h1>{ this.state.title }</h1>
<div className="header-search">
<Icon icon="icon-search" />
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
placeholder="Find movies, music, games, and more"/>
</div>

View file

@ -1,5 +1,7 @@
import React from 'react';
import {Icon} from './common.js';
import Modal from '../component/modal.js';
import rewards from '../rewards.js';
export let Link = React.createClass({
propTypes: {
@ -53,3 +55,79 @@ export let Link = React.createClass({
);
}
});
export let RewardLink = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func
},
refreshClaimable: function() {
switch(this.props.type) {
case 'new_user':
this.setState({ claimable: true });
return;
case 'first_publish':
lbry.claim_list_mine().then(function(list) {
this.setState({
claimable: list.length > 0
})
}.bind(this));
return;
}
},
componentWillMount: function() {
this.refreshClaimable();
},
getInitialState: function() {
return {
claimable: true,
pending: false,
errorMessage: null
}
},
claimReward: function() {
this.setState({
pending: true
})
rewards.claimReward(this.props.type).then((reward) => {
this.setState({
pending: false,
errorMessage: null
})
if (this.props.onRewardClaim) {
this.props.onRewardClaim(reward);
}
}).catch((error) => {
this.setState({
errorMessage: error.message,
pending: false
})
})
},
clearError: function() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure()
}
this.setState({
errorMessage: null
})
},
render: function() {
return (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable }
label={ this.state.pending ? "Claiming..." : "Claim Reward"} onClick={this.claimReward} />}
{this.state.errorMessage ?
<Modal isOpen={true} contentLabel="Reward Claim Error" className="error-modal" onConfirmed={this.clearError}>
{this.state.errorMessage}
</Modal>
: ''}
</div>
);
}
});

View file

@ -0,0 +1,18 @@
import React from 'react';
import ReactModal from 'react-modal';
export const ModalPage = React.createClass({
render: function() {
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal-page'}
overlayClassName="modal-overlay">
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
});
export default ModalPage;

View file

@ -6,6 +6,7 @@ import {Link} from './link.js';
export const Modal = React.createClass({
propTypes: {
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
overlay: React.PropTypes.bool,
onConfirmed: React.PropTypes.func,
onAborted: React.PropTypes.func,
confirmButtonLabel: React.PropTypes.string,
@ -16,6 +17,7 @@ export const Modal = React.createClass({
getDefaultProps: function() {
return {
type: 'alert',
overlay: true,
confirmButtonLabel: 'OK',
abortButtonLabel: 'Cancel',
confirmButtonDisabled: false,
@ -26,7 +28,7 @@ export const Modal = React.createClass({
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal'}
overlayClassName={(this.props.overlayClassName || '') + ' modal-overlay'}>
overlayClassName={[null, undefined, ""].indexOf(this.props.overlayClassName) === -1 ? this.props.overlayClassName : 'modal-overlay'}>
<div>
{this.props.children}
</div>

21
ui/js/component/notice.js Normal file
View file

@ -0,0 +1,21 @@
import React from 'react';
export const Notice = React.createClass({
propTypes: {
isError: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
isError: false,
};
},
render: function() {
return (
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
{this.props.children}
</section>
);
},
});
export default Notice;

View file

@ -0,0 +1,57 @@
import React from 'react';
import lbry from '../lbry.js';
export const SnackBar = React.createClass({
_displayTime: 5, // in seconds
_hideTimeout: null,
getInitialState: function() {
return {
snacks: []
}
},
handleSnackReceived: function(event) {
// if (this._hideTimeout) {
// clearTimeout(this._hideTimeout);
// }
let snacks = this.state.snacks;
snacks.push(event.detail);
this.setState({ snacks: snacks});
},
componentWillMount: function() {
document.addEventListener('globalNotice', this.handleSnackReceived);
},
componentWillUnmount: function() {
document.removeEventListener('globalNotice', this.handleSnackReceived);
},
render: function() {
if (!this.state.snacks.length) {
this._hideTimeout = null; //should be unmounting anyway, but be safe?
return null;
}
let snack = this.state.snacks[0];
if (this._hideTimeout === null) {
this._hideTimeout = setTimeout(function() {
this._hideTimeout = null;
let snacks = this.state.snacks;
snacks.shift();
this.setState({ snacks: snacks });
}.bind(this), this._displayTime * 1000);
}
return (
<div className="snack-bar">
{snack.message}
{snack.linkText && snack.linkTarget ?
<a className="snack-bar__action" href={snack.linkTarget}>{snack.linkText}</a> : ''}
</div>
);
},
});
export default SnackBar;

View file

@ -13,11 +13,12 @@ var SplashScreen = React.createClass({
isLagging: false,
}
},
updateStatus: function(was_lagging=false) {
lbry.getDaemonStatus(this._updateStatusCallback);
updateStatus: function() {
lbry.status().then(this._updateStatusCallback);
},
_updateStatusCallback: function(status) {
if (status.code == 'started') {
const startupStatus = status.startup_status
if (startupStatus.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
@ -34,20 +35,28 @@ var SplashScreen = React.createClass({
return;
}
this.setState({
details: status.message + (status.is_lagging ? '' : '...'),
isLagging: status.is_lagging,
details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
isLagging: startupStatus.is_lagging,
});
setTimeout(() => {
this.updateStatus(status.is_lagging);
this.updateStatus();
}, 500);
},
componentDidMount: function() {
lbry.connect((connected) => {
lbry.connect().then((isConnected) => {
if (isConnected) {
this.updateStatus();
});
} else {
this.setState({
isLagging: true,
message: "Failed to connect to LBRY",
details: "LBRY was unable to start and connect properly."
})
}
})
},
render: function() {
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />;
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
}
});

View file

@ -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');
@ -10,24 +10,37 @@ const menu = remote.require('./menu/main-menu');
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object.
*/
function savePendingPublish(name) {
function savePendingPublish({name, channel_name}) {
let lbryUri;
if (channel_name) {
lbryUri = uri.buildLbryUri({name: channel_name, path: name}, false);
} else {
lbryUri = uri.buildLbryUri({name: name}, false);
}
const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublish = {
claim_id: 'pending_claim_' + name,
txid: 'pending_' + name,
name, channel_name,
claim_id: 'pending_claim_' + lbryUri,
txid: 'pending_' + lbryUri,
nout: 0,
outpoint: 'pending_' + name + ':0',
name: name,
outpoint: 'pending_' + lbryUri + ':0',
time: Date.now(),
};
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
return newPendingPublish;
}
function removePendingPublish({name, outpoint}) {
setLocal('pendingPublishes', getPendingPublishes().filter(
(pub) => pub.name != name && pub.outpoint != outpoint
));
/**
* If there is a pending publish with the given name or outpoint, remove it.
* A channel name may also be provided along with name.
*/
function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
function pubMatches(pub) {
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
}
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
}
/**
@ -36,61 +49,30 @@ function removePendingPublish({name, outpoint}) {
*/
function getPendingPublishes() {
const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = [];
for (let pendingPublish of pendingPublishes) {
if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) {
newPendingPublishes.push(pendingPublish);
}
}
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
setLocal('pendingPublishes', newPendingPublishes);
return newPendingPublishes
return newPendingPublishes;
}
/**
* Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found
* but it has timed out), returns null.
* Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
* provided along withe the name. If no pending publish is found, returns null.
*/
function getPendingPublish({name, outpoint}) {
function getPendingPublish({name, channel_name, outpoint}) {
const pendingPublishes = getPendingPublishes();
const pendingPublishIndex = pendingPublishes.findIndex(
({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint
);
const pendingPublish = pendingPublishes[pendingPublishIndex];
if (pendingPublishIndex == -1) {
return null;
} else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) {
// Pending publish timed out, so remove it from the stored list and don't match
const newPendingPublishes = pendingPublishes.slice();
newPendingPublishes.splice(pendingPublishIndex, 1);
setLocal('pendingPublishes', newPendingPublishes);
return null;
} else {
return pendingPublish;
}
return pendingPublishes.find(
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
) || null;
}
function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) {
return {
name: name,
outpoint: outpoint,
claim_id: claim_id,
txid: txid,
nout: nout,
};
function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) {
return {name, outpoint, claim_id, txid, nout, channel_name};
}
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
return {
name: name,
outpoint: outpoint,
claim_id: claim_id,
metadata: "Attempting publication",
};
return {name, outpoint, claim_id, metadata: null};
}
window.pptdfi = pendingPublishToDummyFileInfo;
let lbry = {
isConnected: false,
@ -116,30 +98,37 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall
jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
}
//core
lbry.connect = function(callback)
{
lbry._connectPromise = null;
lbry.connect = function() {
if (lbry._connectPromise === null) {
lbry._connectPromise = new Promise((resolve, reject) => {
// 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 {
resolve(true);
}
else {
if (tryNum <= 600) { // Move # of tries into constant or config option
setTimeout(function () {
checkDaemonStarted(tryNum + 1);
}, 500);
} else {
callback(false);
}, tryNum < 100 ? 200 : 1000);
}
else {
reject(new Error("Unable to connect to LBRY"));
}
}
});
}
checkDaemonStarted();
});
}
return lbry._connectPromise;
}
lbry.isDaemonAcceptingConnections = function (callback) {
@ -147,10 +136,6 @@ lbry.isDaemonAcceptingConnections = function (callback) {
lbry.call('status', {}, () => callback(true), null, () => callback(false))
};
lbry.getDaemonStatus = function (callback) {
lbry.call('daemon_status', {}, callback);
};
lbry.checkFirstRun = function(callback) {
lbry.call('is_first_run', {}, callback);
}
@ -190,23 +175,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.getStream = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('get', { 'name': name }, callback);
};
lbry.getClaimInfo = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
@ -235,7 +203,6 @@ lbry.getPeersForBlobHash = function(blobHash, callback) {
});
}
lbry.getCostInfo = function(lbryUri, callback, errorCallback) {
/**
* Takes a LBRY URI; will first try and calculate a total cost using
* Lighthouse. If Lighthouse can't be reached, it just retrives the
@ -246,59 +213,48 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) {
* - includes_data: Boolean; indicates whether or not the data fee info
* from Lighthouse is included.
*/
if (!name) {
throw new Error(`Name required.`);
lbry.costPromiseCache = {}
lbry.getCostInfo = function(lbryUri) {
if (lbry.costPromiseCache[lbryUri] === undefined) {
const COST_INFO_CACHE_KEY = 'cost_info_cache';
lbry.costPromiseCache[lbryUri] = new Promise((resolve, reject) => {
let costInfoCache = getSession(COST_INFO_CACHE_KEY, {})
if (!lbryUri) {
reject(new Error(`URI required.`));
}
function getCostWithData(name, size, callback, errorCallback) {
lbry.stream_cost_estimate({name, size}).then((cost) => {
callback({
cost: cost,
includesData: true,
});
}, errorCallback);
if (costInfoCache[lbryUri] && costInfoCache[lbryUri].cost) {
return resolve(costInfoCache[lbryUri])
}
function getCostNoData(name, callback, errorCallback) {
lbry.stream_cost_estimate({name}).then((cost) => {
callback({
function getCost(lbryUri, size) {
lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => {
costInfoCache[lbryUri] = {
cost: cost,
includesData: false,
});
}, errorCallback);
includesData: size !== null,
};
setSession(COST_INFO_CACHE_KEY, costInfoCache);
resolve(costInfoCache[lbryUri]);
}, reject);
}
const uriObj = uri.parseLbryUri(lbryUri);
const name = uriObj.path || uriObj.name;
lighthouse.get_size_for_name(name).then((size) => {
getCostWithData(name, size, callback, errorCallback);
if (size) {
getCost(name, size);
}
else {
getCost(name, null);
}
}, () => {
getCostNoData(name, callback, errorCallback);
getCost(name, null);
});
});
}
lbry.getFeaturedDiscoverNames = function(callback) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', 'https://api.lbry.io/discover/list', true);
xhr.onload = () => {
if (xhr.status === 200) {
var responseData = JSON.parse(xhr.responseText);
if (responseData.data) //new signature, once api.lbry.io is updated
{
resolve(responseData.data);
}
else
{
resolve(responseData);
}
} else {
reject(Error('Failed to fetch featured names.'));
}
};
xhr.send();
});
return lbry.costPromiseCache[lbryUri];
}
lbry.getMyClaims = function(callback) {
@ -365,12 +321,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
returnedPending = true;
if (publishedCallback) {
savePendingPublish(params.name);
savePendingPublish({name: params.name, channel_name: params.channel_name});
publishedCallback(true);
}
if (fileListedCallback) {
savePendingPublish(params.name);
const {name, channel_name} = params;
savePendingPublish({name: params.name, channel_name: params.channel_name});
fileListedCallback(true);
}
}, 2000);
@ -430,6 +387,10 @@ lbry.getClientSettings = function() {
lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem('setting_' + setting);
if (setting == 'showDeveloperMenu')
{
return true;
}
return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
}
@ -522,7 +483,7 @@ lbry.stop = function(callback) {
lbry.fileInfo = {};
lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000;
lbry._fileInfoSubscribeInterval = 500000;
lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = [];
@ -534,6 +495,7 @@ lbry._updateClaimOwnershipCache = function(claimId) {
return match || claimInfo.claim_id == claimId;
});
});
};
lbry._updateFileInfoSubscribers = function(outpoint) {
@ -629,14 +591,14 @@ lbry.showMenuIfNeeded = function() {
*/
lbry.file_list = function(params={}) {
return new Promise((resolve, reject) => {
const {name, outpoint} = params;
const {name, channel_name, outpoint} = params;
/**
* If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file.
*/
if (outpoint !== undefined) {
if (outpoint) {
const pendingPublish = getPendingPublish({outpoint});
if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
@ -645,14 +607,8 @@ lbry.file_list = function(params={}) {
}
lbry.call('file_list', params, (fileInfos) => {
// Remove any pending publications that are now listed in the file manager
removePendingPublishIfNeeded({name, channel_name, outpoint});
const pendingPublishes = getPendingPublishes();
for (let {name: itemName} of fileInfos) {
if (pendingPublishes.find(() => name == itemName)) {
removePendingPublish({name: name});
}
}
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]);
}, reject, reject);
@ -662,16 +618,43 @@ lbry.file_list = function(params={}) {
lbry.claim_list_mine = function(params={}) {
return new Promise((resolve, reject) => {
lbry.call('claim_list_mine', params, (claims) => {
// Filter out pending publishes when the name is already in the file manager
const dummyClaims = getPendingPublishes().filter(
(pub) => !claims.find(({name}) => name == pub.name)
).map(pendingPublishToDummyClaim);
for (let {name, channel_name, txid, nout} of claims) {
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
}
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]);
}, reject, reject);
}, reject, reject)
});
}
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) => {
// jsonrpc.call(lbry.daemonConnectionString, "get", [params], resolve, reject, reject);
// });
// };
// }
lbry = new Proxy(lbry, {
get: function(target, name) {
if (name in target) {

170
ui/js/lbryio.js Normal file
View file

@ -0,0 +1,170 @@
import {getLocal, getSession, setSession, setLocal} from './utils.js';
import lbry from './lbry.js';
const querystring = require('querystring');
const lbryio = {
_accessToken: getLocal('accessToken'),
_authenticationPromise: null,
_user : null,
enabled: false
};
const CONNECTION_STRING = 'https://api.lbry.io/';
const mocks = {
'reward_type.get': ({name}) => {
return {
name: 'link_github',
title: 'Link your GitHub account',
description: 'Link LBRY to your GitHub account',
value: 50,
claimed: false,
};
}
};
lbryio.call = function(resource, action, params={}, method='get') {
return new Promise((resolve, reject) => {
if (!lbryio.enabled && (resource != 'discover' || action != 'list')) {
reject(new Error("LBRY interal API is disabled"))
return
}
/* temp code for mocks */
if (`${resource}.${action}` in mocks) {
resolve(mocks[`${resource}.${action}`](params));
return;
}
/* end temp */
const xhr = new XMLHttpRequest;
xhr.addEventListener('error', function (event) {
reject(new Error("Something went wrong making an internal API call."));
});
xhr.addEventListener('timeout', function() {
reject(new Error('XMLHttpRequest connection timed out'));
});
xhr.addEventListener('load', function() {
const response = JSON.parse(xhr.responseText);
if (!response.success) {
if (reject) {
reject(new Error(response.error));
} else {
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
connectionString: connectionString,
method: action,
params: params,
message: response.error.message,
... response.error.data ? {data: response.error.data} : {},
}
}));
}
} else {
resolve(response.data);
}
});
// For social media auth:
//const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth:
const fullParams = {app_id: lbryio._accessToken, ...params};
if (method == 'get') {
xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true);
xhr.send();
} else if (method == 'post') {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring.stringify(fullParams));
}
});
};
lbryio.setAccessToken = (token) => {
setLocal('accessToken', token)
lbryio._accessToken = token
}
lbryio.authenticate = function() {
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
ID: 1,
HasVerifiedEmail: true
})
})
}
if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry.status().then(({installation_id}) => {
//temp hack for installation_ids being wrong
installation_id += "Y".repeat(96 - installation_id.length)
function setCurrentUser() {
lbryio.call('user', 'me').then((data) => {
lbryio.user = data
resolve(data)
}).catch(function(err) {
lbryio.setAccessToken(null);
if (!getSession('reloadedOnFailedAuth')) {
setSession('reloadedOnFailedAuth', true)
window.location.reload();
} else {
reject(err);
}
})
}
if (!lbryio._accessToken) {
lbryio.call('user', 'new', {
language: 'en',
app_id: installation_id,
}, 'post').then(function(responseData) {
if (!responseData.ID) {
reject(new Error("Received invalid authentication response."));
}
lbryio.setAccessToken(installation_id)
setCurrentUser()
}).catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id)
setCurrentUser()
})
} else {
setCurrentUser()
}
// if (!lbryio._
//(data) => {
// resolve(data)
// localStorage.setItem('accessToken', ID);
// localStorage.setItem('appId', installation_id);
// this.setState({
// registrationCheckComplete: true,
// justRegistered: true,
// });
//});
// lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => {
// // TODO: deal with case where user exists already with the same app ID, but we have no access token.
// // Possibly merge in to the existing user with the same app ID.
// })
}).catch(reject);
});
}
return lbryio._authenticationPromise;
}
export default lbryio;

View file

@ -1,8 +1,8 @@
import lbry from './lbry.js';
import jsonrpc from './jsonrpc.js';
const queryTimeout = 5000;
const maxQueryTries = 5;
const queryTimeout = 3000;
const maxQueryTries = 2;
const defaultServers = [
'http://lighthouse4.lbry.io:50005',
'http://lighthouse5.lbry.io:50005',
@ -20,12 +20,9 @@ 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}`);
}
if (connectTryNum >= maxQueryTries) {
errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`));
return;
}
/**
@ -48,7 +45,7 @@ function call(method, params, callback, errorCallback) {
}, () => {
connectTryNum++;
call(method, params, callback, errorCallback);
});
}, queryTimeout);
}
const lighthouse = new Proxy({}, {

View file

@ -1,9 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import lbry from './lbry.js';
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js';
import App from './app.js';
import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js';
const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu');
@ -16,31 +19,24 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
var init = function() {
let init = function() {
window.lbry = lbry;
window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas');
lbry.connect().then(function(isConnected) {
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
})
function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
ReactDOM.render(<div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div>, canvas)
}
var canvas = document.getElementById('canvas');
if (window.sessionStorage.getItem('loaded') == 'y') {
ReactDOM.render(<App/>, canvas)
onDaemonReady();
} else {
ReactDOM.render(
<SplashScreen message="Connecting" onLoadDone={function() {
// Redirect to the claim code page if needed. Find somewhere better for this logic
if (!localStorage.getItem('claimCodeDone') && window.location.search == '' || window.location.search == '?' || window.location.search == 'discover') {
lbry.getBalance((balance) => {
if (balance <= 0) {
window.location.href = '?claim';
} else {
ReactDOM.render(<App/>, canvas);
}
});
} else {
ReactDOM.render(<App/>, canvas);
}
}}/>,
canvas
);
ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas);
}
};

View file

@ -1,158 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import {Link} from '../component/link.js';
var claimCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, claimCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ClaimCodePage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
activationCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
return;
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
return;
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'codeRedeemed',
referralCredits: response.referralCredits,
activationCredits: response.activationCredits,
});
} else {
this.setState({
submitting: false,
modal: 'codeRedeemFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
handleSkip: function() {
this.setState({
modal: 'skipped',
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Claim your beta invitation code</h2>
<section style={claimCodeContentStyle}>
<p>Thanks for beta testing LBRY! Enter your invitation code and email address below to receive your initial
LBRY credits.</p>
<p>You will be added to our mailing list (if you're not already on it) and will be eligible for future rewards for beta testers.</p>
</section>
<section>
<section><label style={claimCodeLabelStyle} htmlFor="code">Invitation code</label><input name="code" ref="code" /></section>
<section><label style={claimCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<Link button="alt" label="Skip" disabled={this.state.submitting} onClick={this.handleSkip} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'missingCode'} contentLabel="Invitation code required"
onConfirmed={this.closeModal}>
Please enter an invitation code or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'missingEmail'} contentLabel="Email required"
onConfirmed={this.closeModal}>
Please enter an email address or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemFailed'} contentLabel="Failed to redeem code"
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
onConfirmed={this.handleFinished}>
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.`
: (this.state.activationCredits > 0
? `${this.state.activationCredits} credits will be added to your balance shortly.`
: 'The credits will be added to your balance shortly.')}
</Modal>
<Modal isOpen={this.state.modal == 'skipped'} contentLabel="Welcome to LBRY"
onConfirmed={this.handleFinished}>
Welcome to LBRY! You can visit the Wallet page to redeem an invite code at any time.
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Could not connect"
onConfirmed={this.closeModal}>
<p>LBRY couldn't connect to our servers to confirm your invitation code. Please check your internet connection.</p>
If you continue to have problems, you can still browse LBRY and visit the Settings page to redeem your code later.
</Modal>
</main>
);
}
});
export default ClaimCodePage;

View file

@ -1,6 +1,6 @@
import lbry from '../lbry.js';
import React from 'react';
import FormField from '../component/form.js';
import {FormField} from '../component/form.js';
import {Link} from '../component/link.js';
const fs = require('fs');

View file

@ -1,5 +1,6 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import lighthouse from '../lighthouse.js';
import {FileTile} from '../component/file-tile.js';
import {Link} from '../component/link.js';
@ -58,45 +59,58 @@ var SearchResults = React.createClass({
}
});
var featuredContentLegendStyle = {
fontSize: '12px',
color: '#aaa',
verticalAlign: '15%',
};
const communityCategoryToolTipText = ('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!');
var FeaturedCategory = React.createClass({
render: function() {
return (<div className="card-row card-row--small">
{ this.props.category ?
<h3 className="card-row__header">{this.props.category}
{ this.props.category == "community" ?
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
: '' }</h3>
: '' }
{ this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" uri={name} /> }) }
</div>)
}
})
var FeaturedContent = React.createClass({
getInitialState: function() {
return {
featuredNames: [],
featuredUris: {},
failed: false
};
},
componentWillMount: function() {
lbry.getFeaturedDiscoverNames().then((featuredNames) => {
this.setState({ featuredNames: featuredNames });
lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
let featuredUris = {}
Categories.forEach((category) => {
if (Uris[category] && Uris[category].length) {
featuredUris[category] = Uris[category]
}
})
this.setState({ featuredUris: featuredUris });
}, () => {
this.setState({
failed: true
})
});
},
render: function() {
const toolTipText = ('Community Content is a public space where anyone can share content with the ' +
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!');
return (
<div className="row-fluid">
<div className="span6">
<h3>Featured Content</h3>
{ this.state.featuredNames.map(name => <FileTile key={name} uri={name} />) }
</div>
<div className="span6">
<h3>
Community Content
<ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/>
</h3>
<FileTile uri="one" />
<FileTile uri="two" />
<FileTile uri="three" />
<FileTile uri="four" />
<FileTile uri="five" />
</div>
this.state.failed ?
<div className="empty">Failed to load landing content.</div> :
<div>
{
Object.keys(this.state.featuredUris).map(function(category) {
return this.state.featuredUris[category].length ?
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
'';
}.bind(this))
}
</div>
);
}
@ -105,6 +119,10 @@ var FeaturedContent = React.createClass({
var DiscoverPage = React.createClass({
userTypingTimer: null,
propTypes: {
showWelcome: React.PropTypes.bool.isRequired,
},
componentDidUpdate: function() {
if (this.props.query != this.state.query)
{
@ -112,6 +130,12 @@ var DiscoverPage = React.createClass({
}
},
getDefaultProps: function() {
return {
showWelcome: false,
}
},
componentWillReceiveProps: function(nextProps, nextState) {
if (nextProps.query != nextState.query)
{
@ -128,8 +152,15 @@ var DiscoverPage = React.createClass({
lighthouse.search(query).then(this.searchCallback);
},
handleWelcomeDone: function() {
this.setState({
welcomeComplete: true,
});
},
componentWillMount: function() {
document.title = "Discover";
if (this.props.query) {
// Rendering with a query already typed
this.handleSearchChanged(this.props.query);
@ -138,6 +169,7 @@ var DiscoverPage = React.createClass({
getInitialState: function() {
return {
welcomeComplete: false,
results: [],
query: this.props.query,
searching: ('query' in this.props) && (this.props.query.length > 0)

View file

@ -2,8 +2,10 @@ import React from 'react';
import lbry from '../lbry.js';
import uri from '../uri.js';
import {Link} from '../component/link.js';
import FormField from '../component/form.js';
import {FormField} from '../component/form.js';
import {FileTileStream} from '../component/file-tile.js';
import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import {BusyMessage, Thumbnail} from '../component/common.js';
@ -32,6 +34,9 @@ export let FileListDownloaded = React.createClass({
});
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.fileInfos === null) {
return (
@ -63,8 +68,22 @@ export let FileListPublished = React.createClass({
fileInfos: null,
};
},
_requestPublishReward: function() {
lbryio.call('reward', 'list', {}).then(function(userRewards) {
//already rewarded
if (userRewards.filter(function (reward) {
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
}).length) {
return;
}
else {
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
}
});
},
componentDidMount: function () {
this._isMounted = true;
this._requestPublishReward();
document.title = "Published Files";
lbry.claim_list_mine().then((claimInfos) => {
@ -80,6 +99,9 @@ export let FileListPublished = React.createClass({
});
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function () {
if (this.state.fileInfos === null) {
return (
@ -161,20 +183,28 @@ export let FileList = React.createClass({
seenUris = {};
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
for (let {outpoint, name, channel_name, metadata: {stream: {metadata}}, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
if (!metadata || seenUris[name]) {
for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
if (seenUris[name] || !claim_id) {
continue;
}
let streamMetadata;
if (metadata) {
streamMetadata = metadata.stream.metadata;
} else {
streamMetadata = null;
}
let fileUri;
if (channel_name === undefined) {
if (!channel_name) {
fileUri = uri.buildLbryUri({name});
} else {
fileUri = uri.buildLbryUri({name: channel_name, path: name});
}
seenUris[name] = true;
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={fileUri} hideOnRemove={true}
hidePrice={this.props.hidePrices} metadata={metadata} contentType={mime_type}
hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
}

View file

@ -51,28 +51,39 @@ var HelpPage = React.createClass({
return (
<main className="page">
<section className="card">
<div className="card__title-primary">
<h3>Read the FAQ</h3>
</div>
<div className="card__content">
<p>Our FAQ answers many common questions.</p>
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p>
</div>
</section>
<section className="card">
<div className="card__title-primary">
<h3>Get Live Help</h3>
</div>
<div className="card__content">
<p>
Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room.
</p>
<p>
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" />
</p>
</div>
</section>
<section className="card">
<h3>Report a Bug</h3>
<div className="card__title-primary"><h3>Report a Bug</h3></div>
<div className="card__content">
<p>Did you find something wrong?</p>
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<div className="meta">Thanks! LBRY is made by its users.</div>
</div>
</section>
{!ver ? null :
<section className="card">
<h3>About</h3>
<div className="card__title-primary"><h3>About</h3></div>
<div className="card__content">
{ver.lbrynet_update_available || ver.lbryum_update_available ?
<p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p>
: <p>Your copy of LBRY is up to date.</p>
@ -101,6 +112,7 @@ var HelpPage = React.createClass({
</tr>
</tbody>
</table>
</div>
</section>
}
</main>

View file

@ -1,17 +1,20 @@
import React from 'react';
import lbry from '../lbry.js';
import uri from '../uri.js';
import FormField from '../component/form.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({
_requiredFields: ['name', 'bid', 'meta_title', 'meta_author', 'meta_license', 'meta_description'],
_requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'],
_updateChannelList: function(channel) {
// 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).then(() => {}, () => {})
this.setState({
channels: channels,
... channel ? {channel} : {}
@ -27,20 +30,24 @@ var PublishPage = React.createClass({
submitting: true,
});
var checkFields = this._requiredFields.slice();
let checkFields = this._requiredFields;
if (!this.state.myClaimExists) {
checkFields.push('file');
checkFields.unshift('file');
}
var missingFieldFound = false;
let missingFieldFound = false;
for (let fieldName of checkFields) {
var field = this.refs[fieldName];
if (field.getValue() === '') {
field.warnRequired();
const field = this.refs[fieldName];
if (field) {
if (field.getValue() === '' || field.getValue() === false) {
field.showRequiredError();
if (!missingFieldFound) {
field.focus();
missingFieldFound = true;
}
} else {
field.clearError();
}
}
}
@ -61,14 +68,16 @@ var PublishPage = React.createClass({
var metadata = {};
}
for (let metaField of ['title', 'author', 'description', 'thumbnail', 'license', 'license_url', 'language', 'nsfw']) {
for (let metaField of ['title', 'description', 'thumbnail', 'license', 'license_url', 'language']) {
var value = this.refs['meta_' + metaField].getValue();
if (value !== '') {
metadata[metaField] = value;
}
}
var licenseUrl = this.refs.meta_license_url.getValue();
metadata.nsfw = Boolean(parseInt(!!this.refs.meta_nsfw.getValue()));
const licenseUrl = this.refs.meta_license_url.getValue();
if (licenseUrl) {
metadata.license_url = licenseUrl;
}
@ -111,17 +120,18 @@ var PublishPage = React.createClass({
channels: null,
rawName: '',
name: '',
bid: '',
bid: 1,
hasFile: false,
feeAmount: '',
feeCurrency: 'USD',
channel: 'anonymous',
newChannelName: '@',
newChannelBid: '',
nameResolved: false,
newChannelBid: 10,
nameResolved: null,
myClaimExists: null,
topClaimValue: 0.0,
myClaimValue: 0.0,
myClaimMetadata: null,
myClaimExists: null,
copyrightNotice: '',
otherLicenseDescription: '',
otherLicenseUrl: '',
@ -162,35 +172,39 @@ var PublishPage = React.createClass({
}
if (!lbry.nameIsValid(rawName, false)) {
this.refs.name.showAdvice('LBRY names must contain only letters, numbers and dashes.');
this.refs.name.showError('LBRY names must contain only letters, numbers and dashes.');
return;
}
const name = rawName.toLowerCase();
this.setState({
rawName: rawName,
name: name,
nameResolved: null,
myClaimExists: null,
});
lbry.getMyClaim(name, (myClaimInfo) => {
if (name != this.state.name) {
// A new name has been typed already, so bail
return;
}
this.setState({
rawName: rawName,
myClaimExists: !!myClaimInfo,
});
const name = rawName.toLowerCase();
lbry.getMyClaim(name, (myClaimInfo) => {
if (name != this.refs.name.getValue().toLowerCase()) {
// A new name has been typed already, so bail
return;
}
lbry.resolve({uri: name}).then((claimInfo) => {
if (name != this.refs.name.getValue()) {
if (name != this.state.name) {
return;
}
if (!claimInfo) {
this.setState({
name: name,
nameResolved: false,
myClaimExists: false,
});
} else {
const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount);
const newState = {
name: name,
nameResolved: true,
topClaimValue: parseFloat(claimInfo.claim.amount),
myClaimExists: !!myClaimInfo,
@ -237,7 +251,7 @@ var PublishPage = React.createClass({
isFee: feeEnabled
});
},
handeLicenseChange: function(event) {
handleLicenseChange: function(event) {
var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type');
var newState = {
copyrightChosen: licenseType == 'copyright',
@ -245,8 +259,7 @@ var PublishPage = React.createClass({
};
if (licenseType == 'copyright') {
var author = this.refs.meta_author.getValue();
newState.copyrightNotice = 'Copyright ' + (new Date().getFullYear()) + (author ? ' ' + author : '');
newState.copyrightNotice = 'All rights reserved.'
}
this.setState(newState);
@ -277,8 +290,10 @@ var PublishPage = React.createClass({
const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value);
if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) {
this.refs.newChannelName.showAdvice('LBRY channel names must contain only letters, numbers and dashes.');
this.refs.newChannelName.showError('LBRY channel names must contain only letters, numbers and dashes.');
return;
} else {
this.refs.newChannelName.clearError()
}
this.setState({
@ -290,9 +305,14 @@ var PublishPage = React.createClass({
newChannelBid: event.target.value,
});
},
handleTOSChange: function(event) {
this.setState({
TOSAgreed: event.target.checked,
});
},
handleCreateChannelClick: function (event) {
if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showAdvice('LBRY channel names must be at least 4 characters in length.');
this.refs.newChannelName.showError('LBRY channel names must be at least 4 characters in length.');
return;
}
@ -311,7 +331,7 @@ var PublishPage = React.createClass({
}, 5000);
}, (error) => {
// TODO: better error handling
this.refs.newChannelName.showAdvice('Unable to create channel due to an internal error.');
this.refs.newChannelName.showError('Unable to create channel due to an internal error.');
this.setState({
creatingChannel: false,
});
@ -334,136 +354,66 @@ var PublishPage = React.createClass({
},
componentDidUpdate: function() {
},
// Also getting a type warning here too
onFileChange: function() {
if (this.refs.file.getValue()) {
this.setState({ hasFile: true })
} else {
this.setState({ hasFile: false })
}
},
getNameBidHelpText: function() {
if (!this.state.name) {
return "Select a URL for this publish.";
} else if (this.state.nameResolved === false) {
return "This URL is unused.";
} else if (this.state.myClaimExists) {
return "You have already used this URL. Publishing to it again will update your previous publish."
} else if (this.state.topClaimValue) {
return <span>A deposit of at least <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit ' : 'credits '}
is required to win <strong>{this.state.name}</strong>. However, you can still get a permanent URL for any amount.</span>
} else {
return '';
}
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
if (this.state.channels === null) {
return null;
}
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
return (
<main ref="page">
<form onSubmit={this.handleSubmit}>
<section className="card">
<h4>LBRY Name</h4>
<div className="form-row">
<FormField type="text" ref="name" value={this.state.rawName} onChange={this.handleNameChange} />
{
(!this.state.name
? null
: (!this.state.nameResolved
? <em> The name <strong>{this.state.name}</strong> is available.</em>
: (this.state.myClaimExists
? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em>
: <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>)))
}
<div className="help">What LBRY name would you like to claim for this file?</div>
</div>
</section>
<section className="card">
<h4>Channel</h4>
<div className="form-row">
<FormField type="select" onChange={this.handleChannelChange} value={this.state.channel}>
<option key="anonymous" value="anonymous">Anonymous</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">New channel...</option>
</FormField>
{this.state.channel == 'new'
? <section>
<label>Name <FormField type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} /></label>
<label>Bid amount <FormField type="text-number" onChange={this.handleNewChannelBidChange} value={this.state.newChannelBid} /> LBC</label>
<Link button="primary" label={!this.state.creatingChannel ? 'Create channel' : 'Creating channel...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
</section>
: null}
<div className="help">What channel would you like to publish this file under?</div>
</div>
</section>
<section className="card">
<h4>Choose File</h4>
<FormField name="file" ref="file" type="file" />
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null }
</section>
<section className="card">
<h4>Bid Amount</h4>
<div className="form-row">
Credits <FormField ref="bid" type="text-number" onChange={this.handleBidChange} value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} />
<div className="help">How much would you like to bid for this name?
{ !this.state.nameResolved ? <span> Since this name is not currently resolved, you may bid as low as you want, but higher bids help prevent others from claiming your name.</span>
: (this.state.topClaimIsMine ? <span> You currently control this name with a bid of <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.</span>
: (this.state.myClaimExists ? <span> You have a non-winning bid on this name for <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.
To control this name, you'll need to increase your bid to more than <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</span>
: <span> You must bid over <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'} to claim this name.</span>)) }
<div className="card__title-primary">
<h4>Content</h4>
<div className="card__subtitle">
What are you publishing?
</div>
</div>
</section>
<section className="card">
<h4>Fee</h4>
<div className="form-row">
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> No fee
</label>
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(true) } } checked={this.state.isFee} /> { !this.state.isFee ? 'Choose fee...' : 'Fee ' }
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="text-number" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField>
</span>
</label>
<div className="help">
<p>How much would you like to charge for this file?</p>
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
<div className="card__content">
<FormRow name="file" label="File" ref="file" type="file" onChange={this.onFileChange}
helper={this.state.myClaimExists ? "If you don't choose a file, the file from your existing claim will be used." : null}/>
</div>
{ !this.state.hasFile ? '' :
<div>
<div className="card__content">
<FormRow label="Title" type="text" ref="meta_title" name="title" placeholder="Titular Title" />
</div>
</section>
<section className="card">
<h4>Your Content</h4>
<div className="form-row">
<label htmlFor="title">Title</label><FormField type="text" ref="meta_title" name="title" placeholder="My Show, Episode 1" />
<div className="card__content">
<FormRow type="text" label="Thumbnail URL" ref="meta_thumbnail" name="thumbnail" placeholder="http://spee.ch/mylogo" />
</div>
<div className="form-row">
<label htmlFor="author">Author</label><FormField type="text" ref="meta_author" name="author" placeholder="My Company, Inc." />
<div className="card__content">
<FormRow label="Description" type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
</div>
<div className="form-row">
<label htmlFor="license">License</label><FormField type="select" ref="meta_license" name="license" onChange={this.handeLicenseChange}>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option>Public Domain</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormField>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
</div>
{this.state.copyrightChosen
? <div className="form-row">
<label htmlFor="copyright-notice" value={this.state.copyrightNotice}>Copyright notice</label><FormField type="text" name="copyright-notice" value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
</div>
: null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-description">License description</label><FormField type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
</div>
: null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-url">License URL</label> <FormField type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
</div>
: null}
<div className="form-row">
<label htmlFor="language">Language</label> <FormField type="select" defaultValue="en" ref="meta_language" name="language">
<div className="card__content">
<FormRow label="Language" type="select" defaultValue="en" ref="meta_language" name="language">
<option value="en">English</option>
<option value="zh">Chinese</option>
<option value="fr">French</option>
@ -471,22 +421,132 @@ var PublishPage = React.createClass({
<option value="jp">Japanese</option>
<option value="ru">Russian</option>
<option value="es">Spanish</option>
</FormRow>
</div>
<div className="card__content">
<FormRow type="select" label="Maturity" defaultValue="en" ref="meta_nsfw" name="nsfw">
{/* <option value=""></option> */}
<option value="0">All Ages</option>
<option value="1">Adults Only</option>
</FormRow>
</div>
</div>}
</section>
<section className="card">
<div className="card__title-primary">
<h4>Access</h4>
<div className="card__subtitle">
How much does this content cost?
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<label className="form-row__label">Price</label>
</div>
<FormRow label="Free" type="radio" name="isFree" value="1" onChange={ () => { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} />
<FormField type="radio" name="isFree" label={!this.state.isFee ? 'Choose price...' : 'Price ' }
onChange={ () => { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} />
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="number" className="form-field__input--inline" step="0.01" placeholder="1.00" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField>
</div>
<div className="form-row">
<label htmlFor="description">Description</label> <FormField type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
</div>
<div className="form-row">
<label><FormField type="checkbox" ref="meta_nsfw" name="nsfw" placeholder="Description of your content" /> Not Safe For Work</label>
</span>
{ this.state.isFee ?
<div className="form-field__helper">
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
</div> : '' }
<FormRow label="License" type="select" ref="meta_license" name="license" onChange={this.handleLicenseChange}>
<option></option>
<option>Public Domain</option>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormRow>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
{this.state.copyrightChosen
? <FormRow label="Copyright notice" type="text" name="copyright-notice"
value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License description" type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License URL" type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
: null}
</div>
</section>
<section className="card">
<div className="card__title-primary">
<h4>Identity</h4>
<div className="card__subtitle">
Who created this content?
</div>
</div>
<div className="card__content">
<FormRow type="select" tabIndex="1" onChange={this.handleChannelChange} value={this.state.channel}>
<option key="anonymous" value="anonymous">Anonymous</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">New identity...</option>
</FormRow>
</div>
{this.state.channel == 'new' ?
<div className="card__content">
<FormRow label="Name" type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} />
<FormRow label="Deposit"
postfix="LBC"
step="0.01"
type="number"
helper={lbcInputHelp}
onChange={this.handleNewChannelBidChange}
value={this.state.newChannelBid} />
<div className="form-row-submit">
<Link button="primary" label={!this.state.creatingChannel ? 'Create identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
</div>
</div>
: null}
</section>
<section className="card">
<h4>Additional Content Information (Optional)</h4>
<div className="form-row">
<label htmlFor="meta_thumbnail">Thumbnail URL</label> <FormField type="text" ref="meta_thumbnail" name="thumbnail" placeholder="http://mycompany.com/images/ep_1.jpg" />
<div className="card__title-primary">
<h4>Address</h4>
<div className="card__subtitle">Where should this content permanently reside? <Link label="Read more" href="https://lbry.io/faq/naming" />.</div>
</div>
<div className="card__content">
<FormRow prefix="lbry://" type="text" ref="name" placeholder="myname" value={this.state.rawName} onChange={this.handleNameChange}
helper={this.getNameBidHelpText()} />
</div>
{ this.state.rawName ?
<div className="card__content">
<FormRow ref="bid"
type="number"
step="0.01"
label="Deposit"
postfix="LBC"
onChange={this.handleBidChange}
value={this.state.bid}
placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100}
helper={lbcInputHelp} />
</div> : '' }
</section>
<section className="card">
<div className="card__title-primary">
<h4>Terms of Service</h4>
</div>
<div className="card__content">
<FormRow label={
<span>I agree to the <Link href="https://www.lbry.io/termsofservice" label="LBRY terms of service" checked={this.state.TOSAgreed} /></span>
} type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} />
</div>
</section>
@ -500,7 +560,7 @@ var PublishPage = React.createClass({
<Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published"
onConfirmed={this.handlePublishStartedConfirmed}>
<p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p>
You will now be taken to your My Files page, where your newly published file will be listed. The file will take a few minutes to appear for other LBRY users; until then it will be listed as "pending."
<p>The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.</p>
</Modal>
<Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file"
onConfirmed={this.closeModal}>

View file

@ -1,130 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import Modal from '../component/modal.js';
var referralCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, referralCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ReferralPage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'referralInfo',
referralCredits: response.referralCredits,
});
} else {
this.setState({
submitting: false,
modal: 'lookupFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io/check', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Check your referral credits</h2>
<section style={referralCodeContentStyle}>
<p>Have you referred others to LBRY? Enter your referral code and email address below to check how many credits you've earned!</p>
<p>As a reminder, your referral code is the same as your LBRY invitation code.</p>
</section>
<section>
<section><label style={referralCodeLabelStyle} htmlFor="code">Referral code</label><input name="code" ref="code" /></section>
<section><label style={referralCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'referralInfo'} contentLabel="Credit earnings"
onConfirmed={this.handleFinished}>
{this.state.referralCredits > 0
? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!`
: 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
</Modal>
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Couldn't confirm referral code"
onConfirmed={this.closeModal}>
LBRY couldn't connect to our servers to confirm your referral code. Please check your internet connection.
</Modal>
</main>
);
}
});
export default ReferralPage;

126
ui/js/page/reward.js Normal file
View file

@ -0,0 +1,126 @@
import React from 'react';
import lbryio from '../lbryio.js';
import {Link} from '../component/link.js';
import Notice from '../component/notice.js';
import {CreditAmount} from '../component/common.js';
//
// const {shell} = require('electron');
// const querystring = require('querystring');
//
// const GITHUB_CLIENT_ID = '6baf581d32bad60519';
//
// const LinkGithubReward = React.createClass({
// propTypes: {
// onClaimed: React.PropTypes.func,
// },
// _launchLinkPage: function() {
// /* const githubAuthParams = {
// client_id: GITHUB_CLIENT_ID,
// redirect_uri: 'https://lbry.io/',
// scope: 'user:email,public_repo',
// allow_signup: false,
// }
// shell.openExternal('https://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */
// shell.openExternal('https://lbry.io');
// },
// handleConfirmClicked: function() {
// this.setState({
// confirming: true,
// });
//
// lbry.get_new_address().then((address) => {
// lbryio.call('reward', 'new', {
// reward_type: 'new_developer',
// access_token: '**access token here**',
// wallet_address: address,
// }, 'post').then((response) => {
// console.log('response:', response);
//
// this.props.onClaimed(); // This will trigger another API call to show that we succeeded
//
// this.setState({
// confirming: false,
// error: null,
// });
// }, (error) => {
// console.log('failed with error:', error);
// this.setState({
// confirming: false,
// error: error,
// });
// });
// });
// },
// getInitialState: function() {
// return {
// confirming: false,
// error: null,
// };
// },
// render: function() {
// return (
// <section>
// <p><Link button="alt" label="Link with GitHub" onClick={this._launchLinkPage} /></p>
// <section className="reward-page__details">
// <p>This will open a browser window where you can authorize GitHub to link your account to LBRY. This will record your email (no spam) and star the LBRY repo.</p>
// <p>Once you're finished, you may confirm you've linked the account to receive your reward.</p>
// </section>
// {this.state.error
// ? <Notice isError>
// {this.state.error.message}
// </Notice>
// : null}
//
// <Link button="primary" label={this.state.confirming ? 'Confirming...' : 'Confirm'}
// onClick={this.handleConfirmClicked} />
// </section>
// );
// }
// });
//
// const RewardPage = React.createClass({
// propTypes: {
// name: React.PropTypes.string.isRequired,
// },
// _getRewardType: function() {
// lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => {
// this.setState({
// rewardType: rewardType,
// });
// });
// },
// getInitialState: function() {
// return {
// rewardType: null,
// };
// },
// componentWillMount: function() {
// this._getRewardType();
// },
// render: function() {
// if (!this.state.rewardType) {
// return null;
// }
//
// let Reward;
// if (this.props.name == 'link_github') {
// Reward = LinkGithubReward;
// }
//
// const {title, description, value} = this.state.rewardType;
// return (
// <main>
// <section className="card">
// <h2>{title}</h2>
// <CreditAmount amount={value} />
// <p>{this.state.rewardType.claimed
// ? <span class="empty">This reward has been claimed.</span>
// : description}</p>
// <Reward onClaimed={this._getRewardType} />
// </section>
// </main>
// );
// }
// });
//
// export default RewardPage;

73
ui/js/page/rewards.js Normal file
View file

@ -0,0 +1,73 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js';
import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func
},
render: function() {
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3>
</div>
<div className="card__actions">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div>
</section>
);
}
});
var RewardsPage = React.createClass({
componentWillMount: function() {
this.loadRewards()
},
getInitialState: function() {
return {
userRewards: null,
failed: null
};
},
loadRewards: function() {
lbryio.call('reward', 'list', {}).then((userRewards) => {
this.setState({
userRewards: userRewards,
});
}, () => {
this.setState({failed: true })
});
},
render: function() {
console.log(this.state.userRewards);
return (
<main>
<form onSubmit={this.handleSubmit}>
{!this.state.userRewards
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
})}
</form>
</main>
);
}
});
export default RewardsPage;

View file

@ -1,52 +1,53 @@
import React from 'react';
import {FormField, FormRow} from '../component/form.js';
import lbry from '../lbry.js';
var settingsRadioOptionStyles = {
display: 'block',
marginLeft: '13px'
}, settingsCheckBoxOptionStyles = {
display: 'block',
marginLeft: '13px'
}, settingsNumberFieldStyles = {
width: '40px'
}, downloadDirectoryLabelStyles = {
fontSize: '.9em',
marginLeft: '13px'
}, downloadDirectoryFieldStyles= {
width: '300px'
};
var SettingsPage = React.createClass({
_onSettingSaveSuccess: function() {
// This is bad.
// document.dispatchEvent(new CustomEvent('globalNotice', {
// detail: {
// message: "Settings saved",
// },
// }))
},
setDaemonSetting: function(name, value) {
lbry.setDaemonSetting(name, value, this._onSettingSaveSuccess)
},
setClientSetting: function(name, value) {
lbry.setClientSetting(name, value)
this._onSettingSaveSuccess()
},
onRunOnStartChange: function (event) {
lbry.setDaemonSetting('run_on_startup', event.target.checked);
this.setDaemonSetting('run_on_startup', event.target.checked);
},
onShareDataChange: function (event) {
lbry.setDaemonSetting('share_debug_info', event.target.checked);
this.setDaemonSetting('share_debug_info', event.target.checked);
},
onDownloadDirChange: function(event) {
lbry.setDaemonSetting('download_directory', event.target.value);
this.setDaemonSetting('download_directory', event.target.value);
},
onMaxUploadPrefChange: function(isLimited) {
if (!isLimited) {
lbry.setDaemonSetting('max_upload', 0.0);
this.setDaemonSetting('max_upload', 0.0);
}
this.setState({
isMaxUpload: isLimited
});
},
onMaxUploadFieldChange: function(event) {
lbry.setDaemonSetting('max_upload', Number(event.target.value));
this.setDaemonSetting('max_upload', Number(event.target.value));
},
onMaxDownloadPrefChange: function(isLimited) {
if (!isLimited) {
lbry.setDaemonSetting('max_download', 0.0);
this.setDaemonSetting('max_download', 0.0);
}
this.setState({
isMaxDownload: isLimited
});
},
onMaxDownloadFieldChange: function(event) {
lbry.setDaemonSetting('max_download', Number(event.target.value));
this.setDaemonSetting('max_download', Number(event.target.value));
},
getInitialState: function() {
return {
@ -71,84 +72,130 @@ var SettingsPage = React.createClass({
lbry.setClientSetting('showNsfw', event.target.checked);
},
onShowUnavailableChange: function(event) {
lbry.setClientSetting('showUnavailable', event.target.checked);
},
render: function() {
if (!this.state.daemonSettings) {
return null;
}
/*
<section className="card">
<div className="card__content">
<h3>Run on Startup</h3>
</div>
<div className="card__content">
<FormRow type="checkbox"
onChange={this.onRunOnStartChange}
defaultChecked={this.state.daemonSettings.run_on_startup}
label="Run LBRY automatically when I start my computer" />
</div>
</section>
*/
return (
<main>
<section className="card">
<h3>Run on Startup</h3>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onRunOnStartChange} defaultChecked={this.state.daemonSettings.run_on_startup} /> Run LBRY automatically when I start my computer
</label>
</section>
<section className="card">
<div className="card__content">
<h3>Download Directory</h3>
<div className="help">Where would you like the files you download from LBRY to be saved?</div>
<input style={downloadDirectoryFieldStyles} type="text" name="download_directory" defaultValue={this.state.daemonSettings.download_directory} onChange={this.onDownloadDirChange}/>
</div>
<div className="card__content">
<FormRow type="text"
name="download_directory"
defaultValue={this.state.daemonSettings.download_directory}
helper="LBRY downloads will be saved here."
onChange={this.onDownloadDirChange} />
</div>
</section>
<section className="card">
<div className="card__content">
<h3>Bandwidth Limits</h3>
<div className="form-row">
<h4>Max Upload</h4>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxUpload}/> Unlimited
</label>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxUpload}/> { this.state.isMaxUpload ? 'Up to' : 'Choose limit...' }
<span className={ this.state.isMaxUpload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_upload} style={settingsNumberFieldStyles} onChange={this.onMaxUploadFieldChange}/> MB/s</span>
</label>
</div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">Max Upload</div></div>
<FormRow type="radio"
name="max_upload_pref"
onChange={this.onMaxUploadPrefChange.bind(this, false)}
defaultChecked={!this.state.isMaxUpload}
label="Unlimited" />
<div className="form-row">
<h4>Max Download</h4>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited
</label>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxDownload}/> { this.state.isMaxDownload ? 'Up to' : 'Choose limit...' }
<span className={ this.state.isMaxDownload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_download} style={settingsNumberFieldStyles} onChange={this.onMaxDownloadFieldChange}/> MB/s</span>
</label>
<FormField type="radio"
name="max_upload_pref"
onChange={this.onMaxUploadPrefChange.bind(this, true)}
defaultChecked={this.state.isMaxUpload}
label={ this.state.isMaxUpload ? 'Up to' : 'Choose limit...' } />
{ this.state.isMaxUpload ?
<FormField type="number"
min="0"
step=".5"
defaultValue={this.state.daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange}
/>
: ''
}
{ this.state.isMaxUpload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">Max Download</div></div>
<FormRow label="Unlimited"
type="radio"
name="max_download_pref"
onChange={this.onMaxDownloadPrefChange.bind(this, false)}
defaultChecked={!this.state.isMaxDownload} />
<div className="form-row">
<FormField type="radio"
name="max_download_pref"
onChange={this.onMaxDownloadPrefChange.bind(this, true)}
defaultChecked={this.state.isMaxDownload}
label={ this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } />
{ this.state.isMaxDownload ?
<FormField type="number"
min="0"
step=".5"
defaultValue={this.state.daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange}
/>
: ''
}
{ this.state.isMaxDownload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>Content</h3>
<div className="form-row">
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content
</label>
<div className="help">
NSFW content may include nudity, intense sexuality, profanity, or other adult content.
By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction.
</div>
<div className="card__content">
<FormRow type="checkbox"
onChange={this.onShowUnavailableChange}
defaultChecked={this.state.showUnavailable}
label="Show unavailable content in search results" />
</div>
<div className="card__content">
<FormRow label="Show NSFW content" type="checkbox"
onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw}
helper="NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. " />
</div>
</section>
<section className="card">
<h3>Search</h3>
<div className="form-row">
<div className="help">
Would you like search results to include items that are not currently available for download?
</div>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} />
Show unavailable content in search results
</label>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>Share Diagnostic Data</h3>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.share_debug_info} />
Help make LBRY better by contributing diagnostic data about my usage
</label>
</div>
<div className="card__content">
<FormRow type="checkbox"
onChange={this.onShareDataChange}
defaultChecked={this.state.daemonSettings.share_debug_info}
label="Help make LBRY better by contributing diagnostic data about my usage" />
</div>
</section>
</main>
);
}
});
export default SettingsPage;

View file

@ -2,50 +2,29 @@ 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>
@ -57,49 +36,11 @@ var FormatItem = React.createClass({
</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>
);
}
});
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>
<div className="meta">{this.props.uri}</div>
<h2>{this.props.metadata.title}</h2>
{/* 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: {
@ -109,6 +50,8 @@ var ShowPage = React.createClass({
return {
metadata: null,
contentType: null,
hasSignature: false,
signatureIsValid: false,
cost: null,
costIncludesData: null,
uriLookupComplete: null,
@ -118,16 +61,18 @@ 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}}}}}) => {
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,
@ -135,23 +80,50 @@ var ShowPage = React.createClass({
});
},
render: function() {
if (this.state.metadata == null) {
return null;
}
const
metadata = this.state.uriLookupComplete ? this.state.metadata : null,
title = this.state.uriLookupComplete ? metadata.title : this._uri;
return (
<main>
<section className="card">
{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!
</div>
)}
<main className="constrained-page">
<section className="show-page-media">
{ this.state.contentType && this.state.contentType.startsWith('video/') ?
<Video className="video-embedded" uri={this._uri} metadata={metadata} /> :
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
</section>
</main>);
<section className="card">
<div className="card__inner">
<div className="card__title-identity">
<span style={{float: "right"}}><FilePrice uri={this._uri} metadata={metadata} /></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={metadata} contentType={this.state.contentType} />
</div>
</div> : '' }
</div>
{ this.state.uriLookupComplete ?
<div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata.description}
</div>
</div>
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
</div>
{ metadata ?
<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>
);
}
});

View file

@ -2,12 +2,9 @@ import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import Modal from '../component/modal.js';
import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
var addressRefreshButtonStyle = {
fontSize: '11pt',
};
var AddressSection = React.createClass({
_refreshAddress: function(event) {
if (typeof event !== 'undefined') {
@ -27,12 +24,12 @@ var AddressSection = React.createClass({
event.preventDefault();
}
lbry.getNewAddress((address) => {
lbry.wallet_new_address().then(function(address) {
window.localStorage.setItem('wallet_address', address);
this.setState({
address: address,
});
});
}.bind(this))
},
getInitialState: function() {
@ -60,12 +57,20 @@ var AddressSection = React.createClass({
render: function() {
return (
<section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3>
<Address address={this.state.address} /> <Link text="Get new address" icon='icon-refresh' onClick={this._getNewAddress} style={addressRefreshButtonStyle} />
<input type='submit' className='hidden' />
</div>
<div className="card__content">
<Address address={this.state.address} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
</div>
</div>
</section>
);
@ -143,26 +148,25 @@ var SendToAddressSection = React.createClass({
return (
<section className="card">
<form onSubmit={this.handleSubmit}>
<div className="card__title-primary">
<h3>Send Credits</h3>
<div className="form-row">
<label htmlFor="amount">Amount</label>
<input id="amount" type="text" size="10" onChange={this.setAmount}></input>
</div>
<div className="form-row">
<label htmlFor="address">Recipient address</label>
<input id="address" type="text" size="60" onChange={this.setAddress}></input>
<div className="card__content">
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
</div>
<div className="form-row form-row-submit">
<div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
<input type='submit' className='hidden' />
</div>
{
this.state.results ?
<div className="form-row">
<div className="card__content">
<h4>Results</h4>
{this.state.results}
</div>
: ''
</div> : ''
}
</form>
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
@ -231,7 +235,10 @@ var TransactionList = React.createClass({
}
return (
<section className="card">
<div className="card__title-primary">
<h3>Transaction History</h3>
</div>
<div className="card__content">
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
{ this.state.transactionItems && rows.length > 0 ?
@ -250,6 +257,7 @@ var TransactionList = React.createClass({
</table>
: ''
}
</div>
</section>
);
}
@ -290,9 +298,13 @@ var WalletPage = React.createClass({
return (
<main className="page">
<section className="card">
<div className="card__title-primary">
<h3>Balance</h3>
</div>
<div className="card__content">
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
</div>
</section>
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }

View file

@ -1,14 +1,97 @@
import React from 'react';
import {Icon} from '../component/common.js';
import {Icon, Thumbnail} from '../component/common.js';
import {Link} from '../component/link.js';
import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import lbryio from '../lbryio.js';
import rewards from '../rewards.js';
import LoadScreen from '../component/load_screen.js'
const fs = require('fs');
const VideoStream = require('videostream');
export let WatchLink = React.createClass({
propTypes: {
uri: React.PropTypes.string,
downloadStarted: React.PropTypes.bool,
onGet: React.PropTypes.func
},
getInitialState: function() {
affirmedPurchase: false
},
onAffirmPurchase: function() {
lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
var WatchPage = React.createClass({
lbryio.call('file', 'view', {
uri: this.props.uri,
outpoint: streamInfo.outpoint,
claimId: streamInfo.claim_id
})
});
if (this.props.onGet) {
this.props.onGet()
}
},
onWatchClick: function() {
this.setState({
loading: true
});
lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
attemptingDownload: false,
});
} else if (cost <= 0.01) {
this.onAffirmPurchase()
} else {
this.setState({
modal: 'affirmPurchase'
});
}
});
});
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
loading: false,
modal: null,
});
},
render: function() {
return (<div>
<Link button={ this.props.button ? this.props.button : null }
disabled={this.state.loading}
label={this.props.label ? this.props.label : ""}
className={this.props.className}
icon="icon-play"
onClick={this.onWatchClick} />
<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>
<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>
</div>);
}
});
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,
@ -21,19 +104,26 @@ var WatchPage = React.createClass({
return {
downloadStarted: false,
readyToPlay: false,
loadStatusMessage: "Requesting stream",
isPlaying: false,
isPurchased: false,
loadStatusMessage: "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it",
mimeType: null,
controlsShown: false,
};
},
componentDidMount: function() {
onGet: function() {
lbry.get({uri: this.props.uri}).then((fileInfo) => {
this._outpoint = fileInfo.outpoint;
this.updateLoadStatus();
});
this.setState({
isPlaying: true
})
},
handleBackClicked: function() {
history.back();
componentDidMount: function() {
if (this.props.autoplay) {
this.start()
}
},
handleMouseMove: function() {
if (this._controlsTimeout) {
@ -93,6 +183,9 @@ var WatchPage = React.createClass({
return fs.createReadStream(status.download_path, opts)
}
};
rewards.claimNextPurchaseReward()
var elem = this.refs.video;
var videostream = VideoStream(mediaFile, elem);
elem.play();
@ -101,26 +194,15 @@ var WatchPage = React.createClass({
},
render: function() {
return (
!this.state.readyToPlay
? <LoadScreen message={'Loading video...'} details={this.state.loadStatusMessage} />
: <main className="video full-screen" onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
<video controls width="100%" height="100%" id="video" ref="video"></video>
{this.state.controlsShown
? <div className="video__overlay">
<div className="video__back">
<Link icon="icon-arrow-circle-o-left" className="video__back-link" onClick={this.handleBackClicked}/>
<div className="video__back-label">
<Icon icon="icon-caret-left" className="video__back-label-arrow" />
<div className="video__back-label-content">
Back to LBRY
<div className={"video " + this.props.className + (this.state.isPlaying && this.state.readyToPlay ? " video--active" : " video--hidden")}>{
this.state.isPlaying ?
!this.state.readyToPlay ?
<span>this is the world's world loading screen and we shipped our software with it anyway... <br/><br/>{this.state.loadStatusMessage}</span> :
<video controls id="video" ref="video"></video> :
<div className="video__cover" style={{backgroundImage: 'url("' + this.props.metadata.thumbnail + '")'}}>
<WatchLink className="video__play-button" uri={this.props.uri} onGet={this.onGet} icon="icon-play"></WatchLink>
</div>
</div>
</div>
</div>
: null}
</main>
}</div>
);
}
});
export default WatchPage;
})

125
ui/js/rewards.js Normal file
View file

@ -0,0 +1,125 @@
import lbry from './lbry.js';
import lbryio from './lbryio.js';
function rewardMessage(type, amount) {
return {
new_developer: `You earned ${amount} for registering as a new developer.`,
new_user: `You earned ${amount} LBC new user reward.`,
confirm_email: `You earned ${amount} LBC for verifying your email address.`,
new_channel: `You earned ${amount} LBC for creating a publisher identity.`,
first_stream: `You earned ${amount} LBC for streaming your first video.`,
many_downloads: `You earned ${amount} LBC for downloading some of the things.`,
first_publish: `You earned ${amount} LBC for making your first publication.`,
}[type];
}
const rewards = {};
rewards.TYPE_NEW_DEVELOPER = "new_developer",
rewards.TYPE_NEW_USER = "new_user",
rewards.TYPE_CONFIRM_EMAIL = "confirm_email",
rewards.TYPE_FIRST_CHANNEL = "new_channel",
rewards.TYPE_FIRST_STREAM = "first_stream",
rewards.TYPE_MANY_DOWNLOADS = "many_downloads",
rewards.TYPE_FIRST_PUBLISH = "first_publish";
rewards.claimReward = function (type) {
function requestReward(resolve, reject, params) {
if (!lbryio.enabled) {
reject(new Error("Rewards are not enabled."))
return;
}
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 rewards.TYPE_FIRST_CHANNEL:
lbry.claim_list_mine().then(function(claims) {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] == '@' && claim.txid.length
})
console.log(claim);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params)
} else {
reject(new Error("Please create a channel identity first."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_PUBLISH:
lbry.claim_list_mine().then((claims) => {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] != '@' && claim.txid.length
})
if (claim) {
params.transaction_id = claim.txid
requestReward(resolve, reject, params)
} else {
reject(claims.length ?
new Error("Please publish something and wait for confirmation by the network to claim this reward.") :
new Error("Please publish something to claim this reward."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
}
rewards.claimNextPurchaseReward = function() {
let types = {}
types[rewards.TYPE_FIRST_STREAM] = false
types[rewards.TYPE_MANY_DOWNLOADS] = false
lbryio.call('reward', 'list', {}).then((userRewards) => {
userRewards.forEach((reward) => {
if (types[reward.RewardType] === false && reward.TransactionID) {
types[reward.RewardType] = true
}
})
let unclaimedType = Object.keys(types).find((type) => {
return types[type] === false;
})
if (unclaimedType) {
rewards.claimReward(unclaimedType);
}
}, () => { });
}
export default rewards;

View file

@ -13,3 +13,19 @@ export function getLocal(key) {
export function setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
/**
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet.
*/
export function getSession(key, fallback=undefined) {
const itemRaw = sessionStorage.getItem(key);
return itemRaw === null ? fallback : JSON.parse(itemRaw);
}
/**
* Thin wrapper around localStorage.setItem(). Converts value to JSON.
*/
export function setSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}

View file

@ -11,7 +11,7 @@ body
line-height: $font-line-height;
}
$drawer-width: 240px;
$drawer-width: 220px;
#drawer
{
@ -39,12 +39,8 @@ $drawer-width: 240px;
.badge
{
float: right;
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
margin-top: $spacing-vertical * 0.25 - 2;
border-radius: 2px;
background: $color-money;
}
}
.drawer-item-selected
@ -53,6 +49,23 @@ $drawer-width: 240px;
color: $color-primary;
}
}
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount
{
font-weight: bold;
color: $color-money;
}
.credit-amount--estimate {
font-style: italic;
color: $color-meta-light;
}
#drawer-handle
{
padding: $spacing-vertical / 2;
@ -60,6 +73,11 @@ $drawer-width: 240px;
text-align: center;
}
#window
{
position: relative; /*window has it's own z-index inside of it*/
z-index: 1;
}
#window.drawer-closed
{
#drawer { display: none }
@ -100,12 +118,28 @@ $drawer-width: 240px;
.header-search
{
margin-left: 60px;
$padding-adjust: 36px;
text-align: center;
.icon {
position: absolute;
top: $spacing-vertical * 1.5 / 2 + 2px; //hacked
margin-left: -$padding-adjust + 14px; //hacked
}
input[type="search"] {
position: relative;
left: -$padding-adjust;
background: rgba(255, 255, 255, 0.3);
color: white;
width: 400px;
height: $spacing-vertical * 1.5;
line-height: $spacing-vertical * 1.5;
padding-left: $padding-adjust + 3;
padding-right: 3px;
@include border-radius(2px);
@include placeholder-color(#e8e8e8);
&:focus {
box-shadow: $focus-box-shadow;
}
}
}
@ -158,25 +192,11 @@ nav.sub-header
main
{
padding: $spacing-vertical;
}
h2
&.constrained-page
{
margin-bottom: $spacing-vertical;
}
h3, h4
{
margin-bottom: $spacing-vertical / 2;
margin-top: $spacing-vertical;
&:first-child
{
margin-top: 0;
}
}
.meta
{
+ h2, + h3, + h4
{
margin-top: 0;
max-width: $width-page-constrained;
margin-left: auto;
margin-right: auto;
}
}
}
@ -197,48 +217,6 @@ $header-icon-size: 1.5em;
padding: 0 6px 0 18px;
}
.card {
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical;
background: $color-bg;
box-shadow: $default-box-shadow;
border-radius: 2px;
}
.card-obscured
{
position: relative;
}
.card-obscured .card-content {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical / 2;
}
.full-screen
{
width: 100%;

View file

@ -6,17 +6,20 @@ $padding-button: 12px;
$padding-text-link: 4px;
$color-primary: #155B4A;
$color-primary-light: saturate(lighten($color-primary, 50%), 20%);
$color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000;
$color-black-transparent: rgba(32,32,32,0.9);
$color-help: rgba(0,0,0,.6);
$color-notice: #921010;
$color-warning: #ffffff;
$color-notice: #8a6d3b;
$color-error: #a94442;
$color-load-screen-text: #c3c3c3;
$color-canvas: #f5f5f5;
$color-bg: #ffffff;
$color-bg-alt: #D9D9D9;
$color-money: #216C2A;
$color-meta-light: #505050;
$color-form-border: rgba(160,160,160,.5);
$font-size: 16px;
$font-line-height: 1.3333;
@ -25,10 +28,16 @@ $mobile-width-threshold: 801px;
$max-content-width: 1000px;
$max-text-width: 660px;
$width-page-constrained: 800px;
$height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5;
$height-video-embedded: $width-page-constrained * 9 / 16;
$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);
$transition-standard: .225s ease;
$blur-intensity: 8px;

View file

@ -21,7 +21,7 @@
&:hover
{
opacity: $hover-opacity;
transition: opacity .225s ease;
transition: opacity $transition-standard;
text-decoration: underline;
.icon {
text-decoration: none;
@ -38,6 +38,7 @@
text-align: center;
}
/*
section
{
margin-bottom: $spacing-vertical;
@ -46,17 +47,10 @@ section
margin-bottom: 0;
}
&:only-child {
/* If it's an only child, assume it's part of a React layout that will handle the last child condition on its own */
margin-bottom: $spacing-vertical;
}
}
main h1 {
font-size: 2.0em;
margin-bottom: $spacing-vertical;
margin-top: $spacing-vertical*2;
font-family: 'Raleway', sans-serif;
}
*/
h2 {
font-size: 1.75em;
@ -76,11 +70,6 @@ sup, sub {
sup { top: -0.4em; }
sub { top: 0.4em; }
label {
cursor: default;
display: block;
}
code {
font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace;
background-color: #eee;
@ -104,23 +93,6 @@ p
opacity: 0.7;
}
input[type="text"], input[type="search"], textarea
{
@include placeholder {
color: lighten($color-text-dark, 60%);
}
border: 2px solid rgba(160,160,160,.5);
padding-left: 5px;
padding-right: 5px;
box-sizing: border-box;
-webkit-appearance: none;
}
input[type="text"], input[type="search"]
{
line-height: $spacing-vertical - 4;
height: $spacing-vertical * 1.5;
}
.truncated-text {
display: inline-block;
}
@ -144,75 +116,6 @@ input[type="text"], input[type="search"]
}
}
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
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;
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}
.icon:only-child {
position: relative;
top: 0.16em;
@ -235,87 +138,6 @@ input[type="text"], input[type="search"]
font-style: italic;
}
.form-row
{
+ .form-row
{
margin-top: $spacing-vertical / 2;
}
.help
{
margin-top: $spacing-vertical / 2;
}
+ .form-row-submit
{
margin-top: $spacing-vertical;
}
}
.form-field-container {
display: inline-block;
}
.form-field--text {
width: 330px;
}
.form-field--text-number {
width: 50px;
}
.form-field-advice-container {
position: relative;
}
.form-field-advice {
position: absolute;
top: 0px;
left: 0px;
display: flex;
flex-direction: column;
white-space: nowrap;
transition: opacity 400ms ease-in;
}
.form-field-advice--fading {
opacity: 0;
}
.form-field-advice__arrow {
text-align: left;
padding-left: 18px;
font-size: 22px;
line-height: 0.3;
color: darken($color-primary, 5%);
}
.form-field-advice__content-container {
display: inline-block;
}
.form-field-advice__content {
display: inline-block;
padding: 5px;
border-radius: 2px;
background-color: darken($color-primary, 5%);
color: #fff;
}
.form-field-label {
width: 118px;
text-align: right;
vertical-align: top;
display: inline-block;
}
.sort-section {
display: block;
margin-bottom: 5px;
@ -325,79 +147,3 @@ input[type="text"], input[type="search"]
font-size: 0.85em;
color: $color-help;
}
.modal-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
max-width: 250px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -3,10 +3,14 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel
margin:0;
padding:0;
}
input:focus, textarea:focus
:focus
{
outline: 0;
}
input::-webkit-search-cancel-button {
/* Remove default */
-webkit-appearance: none;
}
table
{
border-collapse: collapse;
@ -30,6 +34,7 @@ input, textarea, select
font-family:inherit;
font-size:inherit;
font-weight:inherit;
border: 0 none;
}
img {
width: auto\9;

View file

@ -5,11 +5,21 @@
@import "_canvas";
@import "_gui";
@import "component/_table";
@import "component/_button.scss";
@import "component/_card.scss";
@import "component/_file-actions.scss";
@import "component/_file-tile.scss";
@import "component/_form-field.scss";
@import "component/_menu.scss";
@import "component/_tooltip.scss";
@import "component/_load-screen.scss";
@import "component/_channel-indicator.scss";
@import "component/_notice.scss";
@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";

View file

@ -0,0 +1,78 @@
@import "../global";
$button-focus-shift: 12%;
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
padding-left: 5px;
}
}
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary
{
$color-button-text: white;
color: darken($color-button-text, $button-focus-shift * 0.5);
background-color: $color-primary;
box-shadow: $default-box-shadow;
&:focus {
color: $color-button-text;
//box-shadow: $focus-box-shadow;
background-color: mix(black, $color-primary, $button-focus-shift)
}
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}

View file

@ -0,0 +1,147 @@
@import "../global";
$padding-card-horizontal: $spacing-vertical * 2/3;
.card {
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
background: $color-bg;
box-shadow: $default-box-shadow;
border-radius: 2px;
margin-bottom: $spacing-vertical * 2/3;
overflow: auto;
}
.card--obscured
{
position: relative;
}
.card--obscured .card__inner {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card__title-primary {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical;
}
.card__title-identity {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions {
padding: 0 $padding-card-horizontal;
}
.card__actions {
margin-top: $spacing-vertical * 2/3;
}
.card__actions--bottom {
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions--form-submit {
margin-top: $spacing-vertical;
margin-bottom: $spacing-vertical * 2/3;
}
.card__content {
margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
}
.card__subtext {
color: #444;
margin-top: 12px;
font-size: 0.9em;
margin-top: $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;
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
$card-link-scaling: 1.1;
.card__link {
display: block;
}
.card--link:hover {
position: relative;
z-index: 1;
box-shadow: $focus-box-shadow;
transform: scale($card-link-scaling);
transform-origin: 50% 50%;
overflow-x: visible;
overflow-y: visible;
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
}
$width-card-small: $spacing-vertical * 12;
$height-card-small: $spacing-vertical * 15;
.card--small {
width: $width-card-small;
overflow-x: hidden;
white-space: normal;
}
.card--small .card__media {
height: $width-card-small * 9 / 16;
}
.card__subtitle {
color: $color-help;
font-size: 0.85em;
line-height: $font-line-height * 1 / 0.85;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
padding: $spacing-vertical / 2;
}
.card-row {
> .card {
vertical-align: top;
display: inline-block;
margin-right: $spacing-vertical / 3;
}
+ .card-row {
margin-top: $spacing-vertical * 1/3;
}
}
.card-row--small {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding-left: 20px;
margin-left: -20px; /*hacky way to give space for hover */
}
.card-row__header {
margin-bottom: $spacing-vertical / 3;
}

View file

@ -1,5 +1,5 @@
@import "../global";
.channel-indicator__icon--invalid {
color: #b01c2e;
color: $color-error;
}

View file

@ -1,29 +1,35 @@
@import "../global";
$height-file-tile: $spacing-vertical * 8;
.file-tile__row {
height: $spacing-vertical * 7;
height: $height-file-tile;
.credit-amount {
float: right;
}
//Hack! Remove below!
.card__title-primary {
margin-top: $spacing-vertical * 2/3;
}
.file-tile__row--unavailable {
opacity: 0.5;
}
.file-tile__thumbnail {
max-width: 100%;
max-height: $spacing-vertical * 7;
max-height: $height-file-tile;
vertical-align: middle;
display: block;
margin-left: auto;
margin-right: auto;
}
.file-tile__thumbnail-container
{
height: $height-file-tile;
@include absolute-center();
}
.file-tile__title {
font-weight: bold;
}
.file-tile__cost {
float: right;
}
.file-tile__description {
color: #444;
margin-top: 12px;

View file

@ -0,0 +1,141 @@
@import "../global";
$width-input-border: 2px;
.form-row-submit
{
margin-top: $spacing-vertical;
}
.form-row__label-row {
margin-top: $spacing-vertical * 5/6;
margin-bottom: $spacing-vertical * 1/6;
line-height: 1;
font-size: 0.9 * $font-size;
}
.form-row__label-row--prefix {
float: left;
margin-right: 5px;
}
.form-field {
display: inline-block;
input[type="checkbox"],
input[type="radio"] {
cursor: pointer;
}
select {
transition: outline $transition-standard;
cursor: pointer;
box-sizing: border-box;
padding-left: 5px;
padding-right: 5px;
height: $spacing-vertical;
&:focus {
outline: $width-input-border solid $color-primary;
}
}
textarea,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
@include placeholder {
color: lighten($color-text-dark, 60%);
}
transition: all $transition-standard;
cursor: pointer;
padding-left: 1px;
padding-right: 1px;
box-sizing: border-box;
-webkit-appearance: none;
&[readonly] {
background-color: #bbb;
}
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
border-bottom: $width-input-border solid $color-form-border;
line-height: 1px;
padding-top: $spacing-vertical * 1/3;
padding-bottom: $spacing-vertical * 1/3;
&.form-field__input--error {
border-color: $color-error;
}
&.form-field__input--inline {
padding-top: 0;
padding-bottom: 0;
border-bottom-width: 1px;
margin-left: 8px;
margin-right: 8px;
}
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="date"]:focus {
border-color: $color-primary;
}
textarea {
border: $width-input-border solid $color-form-border;
}
}
.form-field__label {
&[for] { cursor: pointer; }
> input[type="checkbox"], input[type="radio"] {
margin-right: 6px;
}
}
.form-field__label--error {
color: $color-error;
}
.form-field__input-text {
width: 330px;
}
.form-field__prefix {
margin-right: 4px;
}
.form-field__postfix {
margin-left: 4px;
}
.form-field__input-number {
width: 70px;
text-align: right;
}
.form-field__input-textarea {
width: 330px;
}
.form-field__error, .form-field__helper {
margin-top: $spacing-vertical * 1/3;
font-size: 0.8em;
transition: opacity $transition-standard;
}
.form-field__error {
color: $color-error;
}
.form-field__helper {
color: $color-help;
}

View file

@ -23,7 +23,7 @@
}
.load-screen__details--warning {
color: $color-warning;
color: white;
}
.load-screen__cancel-link {

View file

@ -0,0 +1,51 @@
@import "../global";
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
/*
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
top: 25px;
left: 25px;
right: 25px;
bottom: 25px;
}
*/
.modal-page__content {
h1, h2 {
margin-bottom: $spacing-vertical / 2;
}
h3, h4 {
margin-bottom: $spacing-vertical / 4;
}
}

View file

@ -0,0 +1,81 @@
@import "../global";
.modal-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal-overlay--clear {
background-color: transparent;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
padding: $spacing-vertical;
box-shadow: $default-box-shadow;
max-width: 400px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -0,0 +1,18 @@
@import "../global";
.notice {
padding: 10px 20px;
border: 1px solid #000;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
border-radius: 5px;
color: #468847;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.notice--error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}

View file

@ -0,0 +1,42 @@
@import "../global";
$padding-snack-horizontal: $spacing-vertical;
.snack-bar {
$height-snack: $spacing-vertical * 2;
$padding-snack-vertical: $spacing-vertical / 4;
line-height: $height-snack - $padding-snack-vertical * 2;
padding: $padding-snack-vertical $padding-snack-horizontal;
position: fixed;
top: $spacing-vertical;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
min-width: 300px;
max-width: 500px;
background: $color-black-transparent;
color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 2px;
transition: all $transition-standard;
z-index: 10000; /*hack to get it over react modal */
}
.snack-bar__action {
display: inline-block;
text-transform: uppercase;
color: $color-primary-light;
margin: 0px 0px 0px $padding-snack-horizontal;
min-width: min-content;
&:hover {
text-decoration: underline;
}
}

View file

@ -0,0 +1,53 @@
video {
object-fit: contain;
box-sizing: border-box;
max-height: 100%;
max-width: 100%;
}
.video {
background: #000;
color: white;
}
.video-embedded {
max-width: $width-page-constrained;
max-height: $height-video-embedded;
height: $height-video-embedded;
video {
height: 100%;
}
&.video--hidden {
height: $height-video-embedded;
}
&.video--active {
/*background: none;*/
}
}
.video__cover {
text-align: center;
height: 100%;
width: 100%;
background-size: auto 100%;
background-position: center center;
background-repeat: no-repeat;
position: relative;
&:hover {
.video__play-button { @include absolute-center(); }
}
}
.video__play-button {
position: absolute;
width: 100%;
height: 100%;
cursor: pointer;
display: none;
font-size: $spacing-vertical * 3;
color: white;
z-index: 1;
background: $color-black-transparent;
left: 0;
top: 0;
}

View file

@ -0,0 +1,5 @@
@import "../global";
.reward-page__details {
background-color: lighten($color-canvas, 1.5%);
}

9
ui/scss/page/_show.scss Normal file
View file

@ -0,0 +1,9 @@
@import "../global";
.show-page-media {
text-align: center;
margin-bottom: $spacing-vertical;
img {
max-width: 100%;
}
}

View file

@ -1,6 +1,3 @@
.video {
background: #000;
}
.video__overlay {
position: absolute;