Redux proof of concept

This commit is contained in:
6ea86b96 2017-04-07 12:15:22 +07:00 committed by Jeremy Kauffman
parent d160710d20
commit 0d3647c709
43 changed files with 1471 additions and 565 deletions

View file

@ -1,5 +1,8 @@
const {app, BrowserWindow, ipcMain} = require('electron'); const {app, BrowserWindow, ipcMain} = require('electron');
const url = require('url'); const url = require('url');
require('electron-debug')({showDevTools: true});
const path = require('path'); const path = require('path');
const jayson = require('jayson'); const jayson = require('jayson');
const semver = require('semver'); const semver = require('semver');

View file

@ -41,7 +41,8 @@
}, },
"devDependencies": { "devDependencies": {
"electron": "^1.4.15", "electron": "^1.4.15",
"electron-builder": "^11.7.0" "electron-builder": "^11.7.0",
"electron-debug": "^1.1.0"
}, },
"dependencies": {} "dependencies": {}
} }

206
ui/js/actions/app.js Normal file
View file

@ -0,0 +1,206 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import {
selectUpdateUrl,
selectUpgradeDownloadDir,
selectUpgradeDownloadItem,
selectUpgradeFilename,
} from 'selectors/app'
const {remote, ipcRenderer, shell} = require('electron');
const path = require('path');
const app = require('electron').remote.app;
const {download} = remote.require('electron-dl');
const fs = remote.require('fs');
export function doNavigate(path) {
return {
type: types.NAVIGATE,
data: {
path: path
}
}
}
export function doLogoClick() {
}
export function doOpenDrawer() {
return {
type: types.OPEN_DRAWER
}
}
export function doCloseDrawer() {
return {
type: types.CLOSE_DRAWER
}
}
export function doOpenModal(modal) {
return {
type: types.OPEN_MODAL,
data: {
modal
}
}
}
export function doCloseModal() {
return {
type: types.CLOSE_MODAL,
}
}
export function doUpdateBalance(balance) {
return {
type: types.UPDATE_BALANCE,
data: {
balance: balance
}
}
}
export function doUpdateDownloadProgress(percent) {
return {
type: types.UPGRADE_DOWNLOAD_PROGRESSED,
data: {
percent: percent
}
}
}
export function doSkipUpgrade() {
return {
type: types.SKIP_UPGRADE
}
}
export function doStartUpgrade() {
return function(dispatch, getState) {
const state = getState()
const upgradeDownloadPath = selectUpgradeDownloadDir(state)
ipcRenderer.send('upgrade', upgradeDownloadPath)
}
}
export function doDownloadUpgrade() {
return function(dispatch, getState) {
const state = getState()
// 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);
const upgradeFilename = selectUpgradeFilename(state)
let options = {
onProgress: (p) => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
directory: dir,
};
download(remote.getCurrentWindow(), selectUpdateUrl(state), options)
.then(downloadItem => {
/**
* TODO: get the download path directly from the download object. It should just be
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
* too soon.
*/
const _upgradeDownloadItem = downloadItem;
const _upgradeDownloadPath = path.join(dir, upgradeFilename);
dispatch({
type: types.UPGRADE_DOWNLOAD_COMPLETED,
data: {
dir,
downloadItem
}
})
});
dispatch({
type: types.UPGRADE_DOWNLOAD_STARTED
})
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'downloading'
}
})
}
}
export function doCancelUpgrade() {
return function(dispatch, getState) {
const state = getState()
const upgradeDownloadItem = selectUpgradeDownloadItem(state)
if (upgradeDownloadItem) {
/*
* Right now the remote reference to the download item gets garbage collected as soon as the
* the download is over (maybe even earlier), so trying to cancel a finished download may
* throw an error.
*/
try {
upgradeDownloadItem.cancel();
} catch (err) {
console.error(err)
// Do nothing
}
}
dispatch({ type: types.UPGRADE_CANCELLED })
}
}
export function doCheckUpgradeAvailable() {
return function(dispatch, getState) {
const state = getState()
lbry.checkNewVersionAvailable(({isAvailable}) => {
if (!isAvailable) {
return;
}
lbry.getVersionInfo((versionInfo) => {
dispatch({
type: types.UPDATE_VERSION,
data: {
version: versionInfo.lbrynet_version
}
})
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'upgrade'
}
})
});
});
}
}
export function doAlertError(errorList) {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'error',
error: errorList
}
})
}
}
export function doSearch(term) {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.START_SEARCH,
data: {
searchTerm: term
}
})
}
}

View file

@ -1,321 +1,334 @@
import React from 'react'; import store from 'store.js';
import {Line} from 'rc-progress';
import lbry from './lbry.js'; const env = process.env.NODE_ENV || 'development';
import SettingsPage from './page/settings.js'; const config = require(`./config/${env}`);
import HelpPage from './page/help.js'; const logs = [];
import WatchPage from './page/watch.js'; const app = {
import ReportPage from './page/report.js'; env: env,
import StartPage from './page/start.js'; config: config,
import RewardsPage from './page/rewards.js'; store: store,
import RewardPage from './page/reward.js'; logs: logs,
import WalletPage from './page/wallet.js'; log: function(message) {
import ShowPage from './page/show.js'; console.log(message);
import PublishPage from './page/publish.js'; logs.push(message);
import SearchPage from './page/search.js';
import DiscoverPage from './page/discover.js';
import DeveloperPage from './page/developer.js';
import lbryuri from './lbryuri.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Header from './component/header.js';
import {Modal, ExpandableModal} from './component/modal.js';
import {Link} from './component/link.js';
const {remote, ipcRenderer, shell} = require('electron');
const {download} = remote.require('electron-dl');
const path = require('path');
const app = require('electron').remote.app;
const fs = remote.require('fs');
var App = React.createClass({
_error_key_labels: {
connectionString: 'API connection string',
method: 'Method',
params: 'Parameters',
code: 'Error code',
message: 'Error message',
data: 'Error data',
},
_fullScreenPages: ['watch'],
_storeHistoryOfNextRender: false,
_upgradeDownloadItem: null,
_isMounted: false,
_version: null,
getUpdateUrl: function() {
switch (process.platform) {
case 'darwin':
return 'https://lbry.io/get/lbry.dmg';
case 'linux':
return 'https://lbry.io/get/lbry.deb';
case 'win32':
return 'https://lbry.io/get/lbry.exe';
default:
throw 'Unknown platform';
} }
}, }
// Hard code the filenames as a temporary workaround, because global.app = app;
// electron-dl throws errors when you try to get the filename module.exports = app;
getUpgradeFilename: function() { //
switch (process.platform) { // import React from 'react';
case 'darwin': // import {Line} from 'rc-progress';
return `LBRY-${this._version}.dmg`; //
case 'linux': // import lbry from './lbry.js';
return `LBRY_${this._version}_amd64.deb`; // import SettingsPage from './page/settings.js';
case 'windows': // import HelpPage from './page/help.js';
return `LBRY.Setup.${this._version}.exe`; // import WatchPage from './page/watch.js';
default: // import ReportPage from './page/report.js';
throw 'Unknown platform'; // import StartPage from './page/start.js';
} // import RewardsPage from './page/rewards.js';
}, // import RewardPage from './page/reward.js';
getViewingPageAndArgs: function(address) { // import WalletPage from './page/wallet.js';
// For now, routes are in format ?page or ?page=args // import ShowPage from './page/show.js';
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); // import PublishPage from './page/publish.js';
return { // import SearchPage from './page/search.js';
viewingPage: viewingPage, // import DiscoverPage from './page/discover.js';
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs) // import DeveloperPage from './page/developer.js';
}; // import lbryuri from './lbryuri.js';
}, // import {FileListDownloaded, FileListPublished} from './page/file-list.js';
getInitialState: function() { // import Header from './component/header.js';
return Object.assign(this.getViewingPageAndArgs(window.location.search), { // import {Modal, ExpandableModal} from './component/modal.js';
viewingPage: 'discover', // import {Link} from './component/link.js';
appUrl: null, //
errorInfo: null, //
modal: null, // const {remote, ipcRenderer, shell} = require('electron');
downloadProgress: null, // const {download} = remote.require('electron-dl');
downloadComplete: false, // const path = require('path');
}); // const app = require('electron').remote.app;
}, // const fs = remote.require('fs');
componentWillMount: function() { //
window.addEventListener("popstate", this.onHistoryPop); //
// var App = React.createClass({
document.addEventListener('unhandledError', (event) => { // _error_key_labels: {
this.alertError(event.detail); // connectionString: 'API connection string',
}); // method: 'Method',
// params: 'Parameters',
//open links in external browser and skip full redraw on changing page // code: 'Error code',
document.addEventListener('click', (event) => { // message: 'Error message',
var target = event.target; // data: 'Error data',
while (target && target !== document) { // },
if (target.matches('a[href^="http"]')) { // _fullScreenPages: ['watch'],
event.preventDefault(); // _storeHistoryOfNextRender: false,
shell.openExternal(target.href); //
return; // _upgradeDownloadItem: null,
} // _isMounted: false,
if (target.matches('a[href^="?"]')) { // _version: null,
event.preventDefault(); // getUpdateUrl: function() {
if (this._isMounted) { // switch (process.platform) {
let appUrl = target.getAttribute('href'); // case 'darwin':
this._storeHistoryOfNextRender = true; // return 'https://lbry.io/get/lbry.dmg';
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl })); // case 'linux':
document.body.scrollTop = 0; // return 'https://lbry.io/get/lbry.deb';
} // case 'win32':
} // return 'https://lbry.io/get/lbry.exe';
target = target.parentNode; // default:
} // throw 'Unknown platform';
}); // }
// },
if (!sessionStorage.getItem('upgradeSkipped')) { // // Hard code the filenames as a temporary workaround, because
lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => { // // electron-dl throws errors when you try to get the filename
if (upgradeAvailable) { // getUpgradeFilename: function() {
this._version = remoteVersion; // switch (process.platform) {
this.setState({ // case 'darwin':
modal: 'upgrade', // return `LBRY-${this._version}.dmg`;
}); // case 'linux':
} // return `LBRY_${this._version}_amd64.deb`;
}); // case 'windows':
} // return `LBRY.Setup.${this._version}.exe`;
}, // default:
closeModal: function() { // throw 'Unknown platform';
this.setState({ // }
modal: null, // },
}); // getViewingPageAndArgs: function(address) {
}, // // For now, routes are in format ?page or ?page=args
componentDidMount: function() { // let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
this._isMounted = true; // return {
}, // viewingPage: viewingPage,
componentWillUnmount: function() { // pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
this._isMounted = false; // };
window.removeEventListener("popstate", this.onHistoryPop); // },
}, // getInitialState: function() {
onHistoryPop: function() { // return Object.assign(this.getViewingPageAndArgs(window.location.search), {
this.setState(this.getViewingPageAndArgs(location.search)); // viewingPage: 'discover',
}, // appUrl: null,
onSearch: function(term) { // errorInfo: null,
this._storeHistoryOfNextRender = true; // modal: null,
const isShow = term.startsWith('lbry://'); // downloadProgress: null,
this.setState({ // downloadComplete: false,
viewingPage: isShow ? "show" : "search", // });
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term), // },
pageArgs: term // componentWillMount: function() {
}); // window.addEventListener("popstate", this.onHistoryPop);
}, //
onSubmit: function(uri) { // document.addEventListener('unhandledError', (event) => {
this._storeHistoryOfNextRender = true; // this.alertError(event.detail);
this.setState({ // });
address: uri, //
appUrl: "?show=" + encodeURIComponent(uri), // //open links in external browser and skip full redraw on changing page
viewingPage: "show", // document.addEventListener('click', (event) => {
pageArgs: uri // var target = event.target;
}) // while (target && target !== document) {
}, // if (target.matches('a[href^="http"]')) {
handleUpgradeClicked: function() { // event.preventDefault();
// Make a new directory within temp directory so the filename is guaranteed to be available // shell.openExternal(target.href);
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep); // return;
// }
let options = { // if (target.matches('a[href^="?"]')) {
onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}), // event.preventDefault();
directory: dir, // if (this._isMounted) {
}; // let appUrl = target.getAttribute('href');
download(remote.getCurrentWindow(), this.getUpdateUrl(), options) // this._storeHistoryOfNextRender = true;
.then(downloadItem => { // this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
/** // document.body.scrollTop = 0;
* TODO: get the download path directly from the download object. It should just be // }
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected // }
* too soon. // target = target.parentNode;
*/ // }
// });
this._upgradeDownloadItem = downloadItem; //
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename()); // if (!sessionStorage.getItem('upgradeSkipped')) {
this.setState({ // lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
downloadComplete: true // if (upgradeAvailable) {
}); // this._version = remoteVersion;
}); // this.setState({
this.setState({modal: 'downloading'}); // modal: 'upgrade',
}, // });
handleStartUpgradeClicked: function() { // }
ipcRenderer.send('upgrade', this._upgradeDownloadPath); // });
}, // }
cancelUpgrade: function() { // },
if (this._upgradeDownloadItem) { // closeModal: function() {
/* // this.setState({
* Right now the remote reference to the download item gets garbage collected as soon as the // modal: null,
* the download is over (maybe even earlier), so trying to cancel a finished download may // });
* throw an error. // },
*/ // componentDidMount: function() {
try { // this._isMounted = true;
this._upgradeDownloadItem.cancel(); // },
} catch (err) { // componentWillUnmount: function() {
// Do nothing // this._isMounted = false;
} // window.removeEventListener("popstate", this.onHistoryPop);
} // },
this.setState({ // onHistoryPop: function() {
downloadProgress: null, // this.setState(this.getViewingPageAndArgs(location.search));
downloadComplete: false, // },
modal: null, // onSearch: function(term) {
}); // this._storeHistoryOfNextRender = true;
}, // const isShow = term.startsWith('lbry://');
handleSkipClicked: function() { // this.setState({
sessionStorage.setItem('upgradeSkipped', true); // viewingPage: isShow ? "show" : "search",
this.setState({ // appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
modal: null, // pageArgs: term
}); // });
}, // },
alertError: function(error) { // onSubmit: function(uri) {
var errorInfoList = []; // this._storeHistoryOfNextRender = true;
for (let key of Object.keys(error)) { // this.setState({
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]); // address: uri,
let label = this._error_key_labels[key]; // appUrl: "?show=" + encodeURIComponent(uri),
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>); // viewingPage: "show",
} // pageArgs: uri
// })
this.setState({ // },
modal: 'error', // handleUpgradeClicked: function() {
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>, // // 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);
}, //
getContentAndAddress: function() // let options = {
{ // onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
switch(this.state.viewingPage) // directory: dir,
{ // };
case 'search': // download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />]; // .then(downloadItem => {
case 'settings': // /**
return ["Settings", "icon-gear", <SettingsPage />]; // * TODO: get the download path directly from the download object. It should just be
case 'help': // * downloadItem.getSavePath(), but the copy on the main process is being garbage collected
return ["Help", "icon-question", <HelpPage />]; // * too soon.
case 'report': // */
return ['Report an Issue', 'icon-file', <ReportPage />]; //
case 'downloaded': // this._upgradeDownloadItem = downloadItem;
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />]; // this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
case 'published': // this.setState({
return ["Publishes", "icon-folder", <FileListPublished />]; // downloadComplete: true
case 'start': // });
return ["Start", "icon-file", <StartPage />]; // });
case 'rewards': // this.setState({modal: 'downloading'});
return ["Rewards", "icon-bank", <RewardsPage />]; // },
case 'wallet': // handleStartUpgradeClicked: function() {
case 'send': // ipcRenderer.send('upgrade', this._upgradeDownloadPath);
case 'receive': // },
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />] // cancelUpgrade: function() {
case 'show': // if (this._upgradeDownloadItem) {
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />]; // /*
case 'publish': // * Right now the remote reference to the download item gets garbage collected as soon as the
return ["Publish", "icon-upload", <PublishPage />]; // * the download is over (maybe even earlier), so trying to cancel a finished download may
case 'developer': // * throw an error.
return ["Developer", "icon-file", <DeveloperPage />]; // */
case 'discover': // try {
default: // this._upgradeDownloadItem.cancel();
return ["Home", "icon-home", <DiscoverPage />]; // } catch (err) {
} // // Do nothing
}, // }
render: function() { // }
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress(); // this.setState({
// downloadProgress: null,
lbry.setTitle(address); // downloadComplete: false,
// modal: null,
if (this._storeHistoryOfNextRender) { // });
this._storeHistoryOfNextRender = false; // },
history.pushState({}, document.title, this.state.appUrl); // handleSkipClicked: function() {
} // sessionStorage.setItem('upgradeSkipped', true);
// this.setState({
return ( // modal: null,
this._fullScreenPages.includes(this.state.viewingPage) ? // });
mainContent : // },
<div id="window"> // alertError: function(error) {
<Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} /> // var errorInfoList = [];
<div id="main-content"> // for (let key of Object.keys(error)) {
{mainContent} // let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
</div> // let label = this._error_key_labels[key];
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available" // errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
type="confirm" confirmButtonLabel="Upgrade" abortButtonLabel="Skip" // }
onConfirmed={this.handleUpgradeClicked} onAborted={this.handleSkipClicked}> //
Your version of LBRY is out of date and may be unreliable or insecure. // this.setState({
</Modal> // modal: 'error',
<Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom"> // errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null} // });
<Line percent={this.state.downloadProgress} strokeWidth="4"/> // },
{this.state.downloadComplete ? ( // getContentAndAddress: function()
<div> // {
<br /> // switch(this.state.viewingPage)
<p>Click "Begin Upgrade" to start the upgrade process.</p> // {
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p> // case 'search':
<p>After the install is complete, please reopen the app.</p> // return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
</div> // case 'settings':
) : null } // return ["Settings", "icon-gear", <SettingsPage />];
<div className="modal__buttons"> // case 'help':
{this.state.downloadComplete // return ["Help", "icon-question", <HelpPage />];
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={this.handleStartUpgradeClicked} /> // case 'report':
: null} // return ['Report an Issue', 'icon-file', <ReportPage />];
<Link button="alt" label="Cancel" className="modal__button" onClick={this.cancelUpgrade} /> // case 'downloaded':
</div> // return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
</Modal> // case 'published':
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal" // return ["Publishes", "icon-folder", <FileListPublished />];
overlayClassName="error-modal-overlay" onConfirmed={this.closeModal} // case 'start':
extraContent={this.state.errorInfo}> // return ["Start", "icon-file", <StartPage />];
<h3 className="modal__header">Error</h3> // case 'rewards':
// return ["Rewards", "icon-bank", <RewardsPage />];
<div className="error-modal__content"> // case 'wallet':
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div> // case 'send':
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p> // case 'receive':
</div> // return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
</ExpandableModal> // case 'show':
</div> // return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
); // case 'publish':
} // return ["Publish", "icon-upload", <PublishPage />];
}); // case 'developer':
// return ["Developer", "icon-file", <DeveloperPage />];
// case 'discover':
export default App; // default:
// return ["Home", "icon-home", <DiscoverPage />];
// }
// },
// render: function() {
// let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
//
// lbry.setTitle(address);
//
// if (this._storeHistoryOfNextRender) {
// this._storeHistoryOfNextRender = false;
// history.pushState({}, document.title, this.state.appUrl);
// }
//
// return (
// this._fullScreenPages.includes(this.state.viewingPage) ?
// mainContent :
// <div id="window">
// <Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
// <div id="main-content">
// {mainContent}
// </div>
// <Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
// type="confirm" confirmButtonLabel="Upgrade" abortButtonLabel="Skip"
// onConfirmed={this.handleUpgradeClicked} onAborted={this.handleSkipClicked}>
// Your version of LBRY is out of date and may be unreliable or insecure.
// </Modal>
// <Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom">
// Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
// <Line percent={this.state.downloadProgress} strokeWidth="4"/>
// {this.state.downloadComplete ? (
// <div>
// <br />
// <p>Click "Begin Upgrade" to start the upgrade process.</p>
// <p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
// <p>After the install is complete, please reopen the app.</p>
// </div>
// ) : null }
// <div className="modal__buttons">
// {this.state.downloadComplete
// ? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={this.handleStartUpgradeClicked} />
// : null}
// <Link button="alt" label="Cancel" className="modal__button" onClick={this.cancelUpgrade} />
// </div>
// </Modal>
// <ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
// overlayClassName="error-modal-overlay" onConfirmed={this.closeModal}
// extraContent={this.state.errorInfo}>
// <h3 className="modal__header">Error</h3>
//
// <div className="error-modal__content">
// <div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
// <p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
// </div>
// </ExpandableModal>
// </div>
// );

View file

@ -0,0 +1,37 @@
import React from 'react';
import { connect } from 'react-redux'
import {
selectCurrentPage,
selectCurrentModal,
selectDrawerOpen,
selectHeaderLinks,
selectSearchTerm,
} from 'selectors/app'
import {
doCheckUpgradeAvailable,
doOpenDrawer,
doCloseDrawer,
doOpenModal,
doCloseModal,
doSearch,
} from 'actions/app'
import App from './view'
const select = (state) => ({
currentPage: selectCurrentPage(state),
modal: selectCurrentModal(state),
drawerOpen: selectDrawerOpen(state),
headerLinks: selectHeaderLinks(state),
searchTerm: selectSearchTerm(state)
})
const perform = (dispatch) => ({
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
openDrawer: () => dispatch(doOpenDrawer()),
closeDrawer: () => dispatch(doCloseDrawer()),
openModal: () => dispatch(doOpenModal()),
closeModal: () => dispatch(doCloseModal()),
})
export default connect(select, perform)(App)

View file

@ -0,0 +1,65 @@
import React from 'react'
import lbry from 'lbry.js';
import Router from 'component/router'
import Drawer from 'component/drawer';
import Header from 'component/header.js';
import {Modal, ExpandableModal} from 'component/modal.js';
import ErrorModal from 'component/errorModal'
import DownloadingModal from 'component/downloadingModal'
import UpgradeModal from 'component/upgradeModal'
import Link from 'component/link';
import {Line} from 'rc-progress';
const App = React.createClass({
// Temporary workaround since electron-dl throws errors when you try to get the filename
getViewingPageAndArgs: function(address) {
// For now, routes are in format ?page or ?page=args
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
return {
viewingPage: viewingPage,
pageArgs: pageArgs === undefined ? null : pageArgs
};
},
componentWillMount: function() {
document.addEventListener('unhandledError', (event) => {
this.props.alertError(event.detail);
});
if (!this.props.upgradeSkipped) {
this.props.checkUpgradeAvailable()
}
},
render: function() {
const {
currentPage,
openDrawer,
closeDrawer,
openModal,
closeModal,
modal,
drawerOpen,
headerLinks,
search,
searchTerm,
} = this.props
const searchQuery = (currentPage == 'discover' && searchTerm ? searchTerm : '')
return (
currentPage == 'watch' ?
<Router /> :
<div id="window" className={ drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={closeDrawer} viewingPage={currentPage} />
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
<Header onOpenDrawer={openDrawer} initialQuery={searchQuery} onSearch={search} links={headerLinks} />
<Router />
</div>
{modal == 'upgrade' && <UpgradeModal />}
{modal == 'downloading' && <DownloadingModal />}
{modal == 'error' && <ErrorModal />}
</div>
);
}
});
export default App

View file

@ -0,0 +1,25 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doStartUpgrade,
doCancelUpgrade,
} from 'actions/app'
import {
selectDownloadProgress,
selectDownloadComplete,
} from 'selectors/app'
import DownloadingModal from './view'
const select = (state) => ({
downloadProgress: selectDownloadProgress(state),
downloadComplete: selectDownloadComplete(state),
})
const perform = (dispatch) => ({
startUpgrade: () => dispatch(doStartUpgrade()),
cancelUpgrade: () => dispatch(doCancelUpgrade())
})
export default connect(select, perform)(DownloadingModal)

View file

@ -0,0 +1,40 @@
import React from 'react'
import {
Modal
} from 'component/modal'
import {Line} from 'rc-progress';
import Link from 'component/link'
class DownloadingModal extends React.Component {
render() {
const {
downloadProgress,
downloadComplete,
startUpgrade,
cancelUpgrade,
} = this.props
return (
<Modal isOpen={true} contentLabel="Downloading Update" type="custom">
Downloading Update{downloadProgress ? `: ${downloadProgress}%` : null}
<Line percent={downloadProgress} strokeWidth="4"/>
{downloadComplete ? (
<div>
<br />
<p>Click "Begin Upgrade" to start the upgrade process.</p>
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
<p>After the install is complete, please reopen the app.</p>
</div>
) : null }
<div className="modal__buttons">
{downloadComplete
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={startUpgrade} />
: null}
<Link button="alt" label="Cancel" className="modal__button" onClick={cancelUpgrade} />
</div>
</Modal>
)
}
}
export default DownloadingModal

View file

@ -0,0 +1,29 @@
import React from 'react'
import {
connect
} from 'react-redux'
import Drawer from './view'
import {
doNavigate,
doCloseDrawer,
doLogoClick,
doUpdateBalance,
} from 'actions/app'
import {
selectCurrentPage,
selectBalance,
} from 'selectors/app'
const select = (state) => ({
currentPage: selectCurrentPage(state),
balance: selectBalance(state),
})
const perform = {
linkClick: doNavigate,
logoClick: doLogoClick,
closeDrawerClick: doCloseDrawer,
updateBalance: doUpdateBalance,
}
export default connect(select, perform)(Drawer)

View file

@ -0,0 +1,68 @@
import lbry from 'lbry.js';
import React from 'react';
import Link from 'component/link';
const DrawerItem = (props) => {
const {
currentPage,
href,
subPages,
label,
linkClick,
icon,
} = props
const isSelected = (
currentPage == href.substr(1) ||
(subPages && subPages.indexOf(currentPage) != -1)
)
return <Link icon={icon} label={label} onClick={() => linkClick(href)} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
}
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
height: '36px'
};
class Drawer extends React.Component {
constructor(props) {
super(props)
this._balanceSubscribeId = null
}
componentDidMount() {
const { updateBalance } = this.props
this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
updateBalance(balance)
}.bind(this));
}
componentWillUnmount() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
}
render() {
const {
closeDrawerClick,
logoClick,
currentPage,
balance,
} = this.props
return(<nav id="drawer">
<div id="drawer-handle">
<Link title="Close" onClick={closeDrawerClick} icon="icon-bars" className="close-drawer-link"/>
<a href="discover" onMouseUp={logoClick}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
</div>
<DrawerItem {...this.props} href='discover' label="Discover" icon="icon-search" />
<DrawerItem {...this.props} href='publish' label="Publish" icon="icon-upload" />
<DrawerItem {...this.props} href='downloaded' subPages={['published']} label="My Files" icon='icon-cloud-download' />
<DrawerItem {...this.props} href="wallet" subPages={['send', 'receive', 'claim', 'referral']} label="My Wallet" badge={lbry.formatCredits(balance) } icon="icon-bank" />
<DrawerItem {...this.props} href='settings' label="Settings" icon='icon-gear' />
<DrawerItem {...this.props} href='help' label="Help" icon='icon-question-circle' />
</nav>)
}
}
export default Drawer;

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
selectCurrentModal,
selectError,
} from 'selectors/app'
import {
doCloseModal,
} from 'actions/app'
import ErrorModal from './view'
const select = (state) => ({
modal: selectCurrentModal(state),
error: selectError(state),
})
const perform = (dispatch) => ({
closeModal: () => dispatch(doCloseModal())
})
export default connect(select, perform)(ErrorModal)

View file

@ -0,0 +1,50 @@
import React from 'react'
import lbry from 'lbry'
import {
ExpandableModal
} from 'component/modal'
class ErrorModal extends React.Component {
render() {
const {
modal,
closeModal,
error,
} = this.props
const _error_key_labels = {
connectionString: 'API connection string',
method: 'Method',
params: 'Parameters',
code: 'Error code',
message: 'Error message',
data: 'Error data',
}
const errorInfo = <ul className="error-modal__error-list"></ul>
const errorInfoList = []
for (let key of Object.keys(error)) {
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
let label = this._error_key_labels[key];
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
}
return(
<ExpandableModal
isOpen={modal == 'error'}
contentLabel="Error" className="error-modal"
overlayClassName="error-modal-overlay"
onConfirmed={closeModal}
extraContent={errorInfo}
>
<h3 className="modal__header">Error</h3>
<div className="error-modal__content">
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
</div>
</ExpandableModal>
)
}
}
export default ErrorModal

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {Icon, FilePrice} from '../component/common.js'; import {Icon, FilePrice} from '../component/common.js';
import {Modal} from './modal.js'; import {Modal} from './modal.js';
import {FormField} from './form.js'; import {FormField} from './form.js';
import Link from 'component/link';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js';

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js'; import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js';
import UriIndicator from '../component/channel-indicator.js'; import UriIndicator from '../component/channel-indicator.js';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import {Link} from './link.js'; import Link from 'component/link';
import {Icon, CreditAmount} from './common.js'; import {Icon, CreditAmount} from './common.js';
var Header = React.createClass({ var Header = React.createClass({

View file

@ -0,0 +1,7 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import Link from './view'
export default connect(null, null)(Link)

View file

@ -0,0 +1,100 @@
import React from 'react';
import {Icon} from 'component/common.js';
const Link = (props) => {
const {
href,
title,
onClick,
style,
label,
icon,
badge,
button,
hidden,
disabled,
} = props
const className = (props.className || '') +
(!props.className && !props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
(props.button ? ' button-block button-' + props.button + ' button-set-item' : '') +
(props.disabled ? ' disabled' : '');
let content;
if (props.children) {
content = this.props.children
} else {
content = (
<span {... 'button' in props ? {className: 'button__content'} : {}}>
{'icon' in props ? <Icon icon={icon} fixed={true} /> : null}
{label ? <span className="link-label">{label}</span> : null}
{'badge' in props ? <span className="badge">{badge}</span> : null}
</span>
)
}
return (
<a className={className} href={href || 'javascript:;'} title={title}
onClick={onClick}
{... 'style' in props ? {style: style} : {}}>
{content}
</a>
);
}
export default Link
// export let Link = React.createClass({
// propTypes: {
// label: React.PropTypes.string,
// icon: React.PropTypes.string,
// button: React.PropTypes.string,
// badge: React.PropTypes.string,
// hidden: React.PropTypes.bool,
// },
// getDefaultProps: function() {
// return {
// hidden: false,
// disabled: false,
// };
// },
// handleClick: function(e) {
// if (this.props.onClick) {
// this.props.onClick(e);
// }
// },
// render: function() {
// if (this.props.hidden) {
// return null;
// }
// /* The way the class name is generated here is a mess -- refactor */
// const className = (this.props.className || '') +
// (!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
// (this.props.button ? ' button-block button-' + this.props.button + ' button-set-item' : '') +
// (this.props.disabled ? ' disabled' : '');
// let content;
// if (this.props.children) { // Custom content
// content = this.props.children;
// } else {
// content = (
// <span {... 'button' in this.props ? {className: 'button__content'} : {}}>
// {'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
// {<span className="link-label">{this.props.label}</span>}
// {'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
// </span>
// );
// }
// return (
// <a className={className} href={this.props.href || 'javascript:;'} title={this.props.title}
// onClick={this.handleClick} {... 'style' in this.props ? {style: this.props.style} : {}}>
// {content}
// </a>
// );
// }
// });

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {BusyMessage, Icon} from './common.js'; import {BusyMessage, Icon} from './common.js';
import {Link} from '../component/link.js' import Link from 'component/link'
var LoadScreen = React.createClass({ var LoadScreen = React.createClass({
propTypes: { propTypes: {

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js'; import {Icon} from './common.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
export let DropDownMenuItem = React.createClass({ export let DropDownMenuItem = React.createClass({
propTypes: { propTypes: {

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import {Link} from './link.js'; import Link from 'component/link';
export const Modal = React.createClass({ export const Modal = React.createClass({

View file

@ -0,0 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import Router from './view.jsx';
import {
selectCurrentPage
} from 'selectors/app.js';
const select = (state) => ({
currentPage: selectCurrentPage(state)
})
const perform = {
}
export default connect(select, null)(Router);

View file

@ -0,0 +1,51 @@
import React from 'react';
import SettingsPage from 'page/settings.js';
import HelpPage from 'page/help';
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 WalletPage from 'page/wallet.js';
import DetailPage 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';
const route = (page, routesMap) => {
const component = routesMap[page]
return component
};
const Router = (props) => {
const {
currentPage,
} = props;
return route(currentPage, {
'settings': <SettingsPage {...props} />,
'help': <HelpPage {...props} />,
'watch': <WatchPage {...props} />,
'report': <ReportPage {...props} />,
'downloaded': <FileListDownloaded {...props} />,
'published': <FileListPublished {...props} />,
'start': <StartPage {...props} />,
'claim': <ClaimCodePage {...props} />,
'wallet': <WalletPage {...props} />,
'send': <WalletPage {...props} />,
'receive': <WalletPage {...props} />,
'show': <DetailPage {...props} />,
'publish': <PublishPage {...props} />,
'developer': <DeveloperPage {...props} />,
'discover': <DiscoverPage {...props} />,
})
}
export default Router

View file

@ -0,0 +1,19 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doDownloadUpgrade,
doSkipUpgrade,
} from 'actions/app'
import UpgradeModal from './view'
const select = (state) => ({
})
const perform = (dispatch) => ({
downloadUpgrade: () => dispatch(doDownloadUpgrade()),
skipUpgrade: () => dispatch(doSkipUpgrade()),
})
export default connect(select, perform)(UpgradeModal)

View file

@ -0,0 +1,32 @@
import React from 'react'
import {
Modal
} from 'component/modal'
import {
downloadUpgrade,
skipUpgrade
} from 'actions/app'
class UpgradeModal extends React.Component {
render() {
const {
downloadUpgrade,
skipUpgrade
} = this.props
return (
<Modal
isOpen={true}
contentLabel="Update available"
type="confirm"
confirmButtonLabel="Upgrade"
abortButtonLabel="Skip"
onConfirmed={downloadUpgrade}
onAborted={skipUpgrade}>
Your version of LBRY is out of date and may be unreliable or insecure.
</Modal>
)
}
}
export default UpgradeModal

View file

@ -0,0 +1,2 @@
module.exports = {
}

View file

@ -0,0 +1,21 @@
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
export const NAVIGATE = 'NAVIGATE'
// Upgrades
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED'
export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE'
export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED'
export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED'
export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED'
export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE'
export const UPDATE_VERSION = 'UPDATE_VERSION'
export const SKIP_UPGRADE = 'SKIP_UPGRADE'
export const START_UPGRADE = 'START_UPGRADE'
export const OPEN_MODAL = 'OPEN_MODAL'
export const CLOSE_MODAL = 'CLOSE_MODAL'
export const OPEN_DRAWER = 'OPEN_DRAWER'
export const CLOSE_DRAWER = 'CLOSE_DRAWER'
export const START_SEARCH = 'START_SEARCH'

View file

@ -3,13 +3,16 @@ import ReactDOM from 'react-dom';
import lbry from './lbry.js'; import lbry from './lbry.js';
import lbryio from './lbryio.js'; import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import App from './app.js'; import App from './component/app/index.js';
import SplashScreen from './component/splash.js'; import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js'; import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js'; import {AuthOverlay} from './component/auth.js';
import { Provider } from 'react-redux';
import store from 'store.js';
const {remote} = require('electron'); const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu'); const contextMenu = remote.require('./menu/context-menu');
const app = require('./app')
lbry.showMenuIfNeeded(); lbry.showMenuIfNeeded();
@ -19,7 +22,9 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault(); event.preventDefault();
}); });
let init = function() { const initialState = app.store.getState();
var init = function() {
window.lbry = lbry; window.lbry = lbry;
window.lighthouse = lighthouse; window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas'); let canvas = document.getElementById('canvas');
@ -30,7 +35,7 @@ let init = function() {
function onDaemonReady() { function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again 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) ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas)
} }
if (window.sessionStorage.getItem('loaded') == 'y') { if (window.sessionStorage.getItem('loaded') == 'y') {

View file

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

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import lbryio from '../lbryio.js'; import lbry from '../lbry.js';
import {FileTile, FileTileStream} from '../component/file-tile.js'; import lighthouse from '../lighthouse.js';
import {FileTile} from '../component/file-tile.js';
import Link from 'component/link';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' + const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
import {FormField} from '../component/form.js'; import FormField from '../component/form.js';
import {SubHeader} from '../component/header.js'; import {SubHeader} from '../component/header.js';
import {FileTileStream} from '../component/file-tile.js'; import {FileTileStream} from '../component/file-tile.js';
import rewards from '../rewards.js'; import rewards from '../rewards.js';

View file

@ -0,0 +1,7 @@
import React from 'react'
import {
connect
} from 'react-redux'
import HelpPage from './view'
export default connect(null, null)(HelpPage)

View file

@ -1,10 +1,9 @@
//@TODO: Customize advice based on OS //@TODO: Customize advice based on OS
//@TODO: Customize advice based on OS
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from 'lbry.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
import {SettingsNav} from './settings.js'; import {SettingsNav} from './settings.js';
import {version as uiVersion} from 'json!../../package.json'; import {version as uiVersion} from 'json!../../../package.json';
var HelpPage = React.createClass({ var HelpPage = React.createClass({
getInitialState: function() { getInitialState: function() {

View file

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {FormField, FormRow} from '../component/form.js'; import FormField from '../component/form.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
import rewards from '../rewards.js'; import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
var PublishPage = React.createClass({ var PublishPage = React.createClass({

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {Link} from '../component/link.js'; import Link from 'component/link';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';

View file

@ -5,7 +5,7 @@ import lbryuri from '../lbryuri.js';
import {Video} from '../page/watch.js' import {Video} from '../page/watch.js'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js'; import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js'; import Link from '../component/link';
import UriIndicator from '../component/channel-indicator.js'; import UriIndicator from '../component/channel-indicator.js';
var FormatItem = React.createClass({ var FormatItem = React.createClass({

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import Link from 'component/link';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {SubHeader} from '../component/header.js'; import {SubHeader} from '../component/header.js';
import {FormField, FormRow} from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';

View file

@ -1,215 +0,0 @@
import React from 'react';
import {Icon, Thumbnail, FilePrice} 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,
metadata: React.PropTypes.object,
downloadStarted: React.PropTypes.bool,
onGet: React.PropTypes.func,
},
getInitialState: function() {
affirmedPurchase: false
},
play: function() {
lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
lbryio.call('file', 'view', {
uri: this.props.uri,
outpoint: streamInfo.outpoint,
claimId: streamInfo.claim_id
}).catch(() => {})
});
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.play()
} else {
lbry.file_list({outpoint: this.props.outpoint}).then((fileInfo) => {
if (fileInfo) { // Already downloaded
this.play();
} 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.play} onAborted={this.closeModal}>
Are you sure you'd like to buy <strong>{this.props.metadata.title}</strong> for <strong><FilePrice uri={this.props.uri} metadata={this.props.metadata} label={false} look="plain" /></strong> credits?
</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,
propTypes: {
uri: React.PropTypes.string.isRequired,
metadata: React.PropTypes.object,
outpoint: React.PropTypes.string,
},
getInitialState: function() {
return {
downloadStarted: false,
readyToPlay: false,
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,
};
},
onGet: function() {
lbry.get({uri: this.props.uri}).then((fileInfo) => {
this.updateLoadStatus();
});
this.setState({
isPlaying: true
})
},
componentDidMount: function() {
if (this.props.autoplay) {
this.start()
}
},
handleMouseMove: function() {
if (this._controlsTimeout) {
clearTimeout(this._controlsTimeout);
}
if (!this.state.controlsShown) {
this.setState({
controlsShown: true,
});
}
this._controlsTimeout = setTimeout(() => {
if (!this.isMounted) {
return;
}
this.setState({
controlsShown: false,
});
}, this._controlsHideDelay);
},
handleMouseLeave: function() {
if (this._controlsTimeout) {
clearTimeout(this._controlsTimeout);
}
if (this.state.controlsShown) {
this.setState({
controlsShown: false,
});
}
},
updateLoadStatus: function() {
lbry.file_list({
outpoint: this.props.outpoint,
full_status: true,
}).then(([status]) => {
if (!status || status.written_bytes == 0) {
// Download hasn't started yet, so update status message (if available) then try again
// TODO: Would be nice to check if we have the MOOV before starting playing
if (status) {
this.setState({
loadStatusMessage: status.message
});
}
setTimeout(() => { this.updateLoadStatus() }, 250);
} else {
this.setState({
readyToPlay: true,
mimeType: status.mime_type,
})
const mediaFile = {
createReadStream: function (opts) {
// Return a readable stream that provides the bytes
// between offsets "start" and "end" inclusive
console.log('Stream between ' + opts.start + ' and ' + opts.end + '.');
return fs.createReadStream(status.download_path, opts)
}
};
rewards.claimNextPurchaseReward()
var elem = this.refs.video;
var videostream = VideoStream(mediaFile, elem);
elem.play();
}
});
},
render: function() {
return (
<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 worst 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} metadata={this.props.metadata} outpoint={this.props.outpoint} onGet={this.onGet} icon="icon-play"></WatchLink>
</div>
}</div>
);
}
})

105
ui/js/reducers/app.js Normal file
View file

@ -0,0 +1,105 @@
import * as types from 'constants/action_types'
const reducers = {}
const defaultState = {
isLoaded: false,
currentPage: 'discover',
platform: process.platform,
drawerOpen: sessionStorage.getItem('drawerOpen') || true,
upgradeSkipped: sessionStorage.getItem('upgradeSkipped')
}
reducers[types.UPDATE_BALANCE] = function(state, action) {
return Object.assign({}, state, {
balance: action.data.balance
})
}
reducers[types.NAVIGATE] = function(state, action) {
return Object.assign({}, state, {
currentPage: action.data.path
})
}
reducers[types.UPGRADE_CANCELLED] = function(state, action) {
return Object.assign({}, state, {
downloadProgress: null,
downloadComplete: false,
modal: null,
})
}
reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) {
return Object.assign({}, state, {
downloadDir: action.data.dir,
downloadComplete: true,
})
}
reducers[types.UPGRADE_DOWNLOAD_STARTED] = function(state, action) {
return Object.assign({}, state, {
upgradeDownloading: true
})
}
reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) {
return Object.assign({}, state, {
upgradeDownloading: false,
upgradeDownloadCompleted: true
})
}
reducers[types.SKIP_UPGRADE] = function(state, action) {
sessionStorage.setItem('upgradeSkipped', true);
return Object.assign({}, state, {
upgradeSkipped: true,
modal: null
})
}
reducers[types.UPDATE_VERSION] = function(state, action) {
return Object.assign({}, state, {
version: action.data.version
})
}
reducers[types.OPEN_MODAL] = function(state, action) {
return Object.assign({}, state, {
modal: action.data.modal,
extraContent: action.data.errorList
})
}
reducers[types.CLOSE_MODAL] = function(state, action) {
return Object.assign({}, state, {
modal: undefined,
extraContent: undefined
})
}
reducers[types.OPEN_DRAWER] = function(state, action) {
sessionStorage.setItem('drawerOpen', false)
return Object.assign({}, state, {
drawerOpen: true
})
}
reducers[types.CLOSE_DRAWER] = function(state, action) {
sessionStorage.setItem('drawerOpen', false)
return Object.assign({}, state, {
drawerOpen: false
})
}
reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) {
return Object.assign({}, state, {
downloadProgress: action.data.percent
})
}
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

145
ui/js/selectors/app.js Normal file
View file

@ -0,0 +1,145 @@
import { createSelector } from 'reselect'
export const _selectState = state => state.app || {}
export const selectIsLoaded = createSelector(
_selectState,
(state) => {
return state.isLoaded
}
)
export const selectCurrentPage = createSelector(
_selectState,
(state) => {
return state.currentPage
}
)
export const selectBalance = createSelector(
_selectState,
(state) => {
return state.balance || 0
}
)
export const selectPlatform = createSelector(
_selectState,
(state) => {
return state.platform
}
)
export const selectUpdateUrl = createSelector(
selectPlatform,
(platform) => {
switch (platform) {
case 'darwin':
return 'https://lbry.io/get/lbry.dmg';
case 'linux':
return 'https://lbry.io/get/lbry.deb';
case 'win32':
return 'https://lbry.io/get/lbry.exe';
default:
throw 'Unknown platform';
}
}
)
export const selectVersion = createSelector(
_selectState,
(state) => {
return state.version
}
)
export const selectUpgradeFilename = createSelector(
selectPlatform,
selectVersion,
(platform, version) => {
switch (platform) {
case 'darwin':
return `LBRY-${version}.dmg`;
case 'linux':
return `LBRY_${version}_amd64.deb`;
case 'windows':
return `LBRY.Setup.${version}.exe`;
default:
throw 'Unknown platform';
}
}
)
export const selectCurrentModal = createSelector(
_selectState,
(state) => state.modal
)
export const selectDownloadProgress = createSelector(
_selectState,
(state) => state.downloadProgress
)
export const selectDownloadComplete = createSelector(
_selectState,
(state) => state.upgradeDownloadCompleted
)
export const selectDrawerOpen = createSelector(
_selectState,
(state) => state.drawerOpen
)
export const selectHeaderLinks = createSelector(
selectCurrentPage,
(page) => {
switch(page)
{
case 'wallet':
case 'send':
case 'receive':
case 'claim':
case 'referral':
return {
'?wallet' : 'Overview',
'?send' : 'Send',
'?receive' : 'Receive',
'?claim' : 'Claim Beta Code',
'?referral' : 'Check Referral Credit',
};
case 'downloaded':
case 'published':
return {
'?downloaded': 'Downloaded',
'?published': 'Published',
};
default:
return null;
}
}
)
export const selectUpgradeSkipped = createSelector(
_selectState,
(state) => state.upgradeSkipped
)
export const selectUpgradeDownloadDir = createSelector(
_selectState,
(state) => state.downloadDir
)
export const selectUpgradeDownloadItem = createSelector(
_selectState,
(state) => state.downloadItem
)
export const selectSearchTerm = createSelector(
_selectState,
(state) => state.searchTerm
)
export const selectError = createSelector(
_selectState,
(state) => state.error
)

37
ui/js/store.js Normal file
View file

@ -0,0 +1,37 @@
const redux = require('redux');
const thunk = require("redux-thunk").default;
const env = process.env.NODE_ENV || 'development';
import {
createLogger
} from 'redux-logger'
import appReducer from 'reducers/app';
function isFunction(object) {
return typeof object === 'function';
}
function isNotFunction(object) {
return !isFunction(object);
}
const reducers = redux.combineReducers({
app: appReducer,
});
var middleware = [thunk]
if (env === 'development') {
const logger = createLogger({
collapsed: true
});
middleware.push(logger)
}
var createStoreWithMiddleware = redux.compose(
redux.applyMiddleware(...middleware)
)(redux.createStore);
var reduxStore = createStoreWithMiddleware(reducers);
export default reduxStore;

View file

@ -27,6 +27,11 @@
"react": "^15.4.0", "react": "^15.4.0",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-modal": "^1.5.2", "react-modal": "^1.5.2",
"react-redux": "^5.0.3",
"redux": "^3.6.0",
"redux-logger": "^3.0.1",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.0",
"videostream": "^2.4.2" "videostream": "^2.4.2"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,4 +1,5 @@
const path = require('path'); const path = require('path');
const appPath = path.resolve(__dirname, 'js');
const PATHS = { const PATHS = {
app: path.join(__dirname, 'app'), app: path.join(__dirname, 'app'),
@ -13,6 +14,10 @@ module.exports = {
filename: "bundle.js" filename: "bundle.js"
}, },
devtool: 'source-map', devtool: 'source-map',
resolve: {
root: appPath,
extensions: ['', '.js', '.jsx', '.css'],
},
module: { module: {
preLoaders: [ preLoaders: [
{ {

View file

@ -1,4 +1,5 @@
const path = require('path'); const path = require('path');
const appPath = path.resolve(__dirname, 'js');
const PATHS = { const PATHS = {
app: path.join(__dirname, 'app'), app: path.join(__dirname, 'app'),
@ -16,6 +17,10 @@ module.exports = {
debug: true, debug: true,
cache: true, cache: true,
devtool: 'eval', devtool: 'eval',
resolve: {
root: appPath,
extensions: ['', '.js', '.jsx', '.css'],
},
module: { module: {
preLoaders: [ preLoaders: [
{ {