diff --git a/app/main.js b/app/main.js index eb867a2d7..3acdbbab4 100644 --- a/app/main.js +++ b/app/main.js @@ -1,5 +1,8 @@ const {app, BrowserWindow, ipcMain} = require('electron'); const url = require('url'); + +require('electron-debug')({showDevTools: true}); + const path = require('path'); const jayson = require('jayson'); const semver = require('semver'); diff --git a/package.json b/package.json index 45142bbaa..b5a3c3678 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ }, "devDependencies": { "electron": "^1.4.15", - "electron-builder": "^11.7.0" + "electron-builder": "^11.7.0", + "electron-debug": "^1.1.0" }, "dependencies": {} } diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js new file mode 100644 index 000000000..14aff78f7 --- /dev/null +++ b/ui/js/actions/app.js @@ -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 + } + }) + } +} diff --git a/ui/js/app.js b/ui/js/app.js index 85f8891a6..834af6521 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1,321 +1,334 @@ -import React from 'react'; -import {Line} from 'rc-progress'; +import store from 'store.js'; -import lbry from './lbry.js'; -import SettingsPage from './page/settings.js'; -import HelpPage from './page/help.js'; -import WatchPage from './page/watch.js'; -import ReportPage from './page/report.js'; -import StartPage from './page/start.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 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 - // electron-dl throws errors when you try to get the filename - getUpgradeFilename: function() { - switch (process.platform) { - case 'darwin': - return `LBRY-${this._version}.dmg`; - case 'linux': - return `LBRY_${this._version}_amd64.deb`; - case 'windows': - return `LBRY.Setup.${this._version}.exe`; - default: - throw 'Unknown platform'; - } - }, - 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 : decodeURIComponent(pageArgs) - }; - }, - getInitialState: function() { - return Object.assign(this.getViewingPageAndArgs(window.location.search), { - viewingPage: 'discover', - appUrl: null, - errorInfo: null, - modal: null, - downloadProgress: null, - downloadComplete: false, - }); - }, - componentWillMount: function() { - window.addEventListener("popstate", this.onHistoryPop); - - document.addEventListener('unhandledError', (event) => { - this.alertError(event.detail); - }); - - //open links in external browser and skip full redraw on changing page - document.addEventListener('click', (event) => { - var target = event.target; - while (target && target !== document) { - if (target.matches('a[href^="http"]')) { - event.preventDefault(); - shell.openExternal(target.href); - return; - } - if (target.matches('a[href^="?"]')) { - event.preventDefault(); - if (this._isMounted) { - let appUrl = target.getAttribute('href'); - this._storeHistoryOfNextRender = true; - this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl })); - document.body.scrollTop = 0; - } - } - target = target.parentNode; - } - }); - - if (!sessionStorage.getItem('upgradeSkipped')) { - lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => { - if (upgradeAvailable) { - this._version = remoteVersion; - this.setState({ - modal: 'upgrade', - }); - } - }); - } - }, - closeModal: function() { - this.setState({ - modal: null, - }); - }, - componentDidMount: function() { - this._isMounted = true; - }, - componentWillUnmount: function() { - this._isMounted = false; - window.removeEventListener("popstate", this.onHistoryPop); - }, - onHistoryPop: function() { - this.setState(this.getViewingPageAndArgs(location.search)); - }, - onSearch: function(term) { - this._storeHistoryOfNextRender = true; - const isShow = term.startsWith('lbry://'); - this.setState({ - viewingPage: isShow ? "show" : "search", - appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term), - pageArgs: term - }); - }, - onSubmit: function(uri) { - this._storeHistoryOfNextRender = true; - this.setState({ - address: uri, - appUrl: "?show=" + encodeURIComponent(uri), - viewingPage: "show", - pageArgs: uri - }) - }, - 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); - - let options = { - onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}), - directory: dir, - }; - download(remote.getCurrentWindow(), this.getUpdateUrl(), 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. - */ - - this._upgradeDownloadItem = downloadItem; - this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename()); - this.setState({ - downloadComplete: true - }); - }); - this.setState({modal: 'downloading'}); - }, - handleStartUpgradeClicked: function() { - ipcRenderer.send('upgrade', this._upgradeDownloadPath); - }, - cancelUpgrade: function() { - if (this._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 { - this._upgradeDownloadItem.cancel(); - } catch (err) { - // Do nothing - } - } - this.setState({ - downloadProgress: null, - downloadComplete: false, - modal: null, - }); - }, - handleSkipClicked: function() { - sessionStorage.setItem('upgradeSkipped', true); - this.setState({ - modal: null, - }); - }, - alertError: function(error) { - var 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(
  • {label}: {val}
  • ); - } - - this.setState({ - modal: 'error', - errorInfo: , - }); - }, - getContentAndAddress: function() - { - switch(this.state.viewingPage) - { - case 'search': - return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', ]; - case 'settings': - return ["Settings", "icon-gear", ]; - case 'help': - return ["Help", "icon-question", ]; - case 'report': - return ['Report an Issue', 'icon-file', ]; - case 'downloaded': - return ["Downloads & Purchases", "icon-folder", ]; - case 'published': - return ["Publishes", "icon-folder", ]; - case 'start': - return ["Start", "icon-file", ]; - case 'rewards': - return ["Rewards", "icon-bank", ]; - case 'wallet': - case 'send': - case 'receive': - return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", ] - case 'show': - return [lbryuri.normalize(this.state.pageArgs), "icon-file", ]; - case 'publish': - return ["Publish", "icon-upload", ]; - case 'developer': - return ["Developer", "icon-file", ]; - case 'discover': - default: - return ["Home", "icon-home", ]; - } - }, - 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 : -
    -
    -
    - {mainContent} -
    - - Your version of LBRY is out of date and may be unreliable or insecure. - - - Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null} - - {this.state.downloadComplete ? ( -
    -
    -

    Click "Begin Upgrade" to start the upgrade process.

    -

    The app will close, and you will be prompted to install the latest version of LBRY.

    -

    After the install is complete, please reopen the app.

    -
    - ) : null } -
    - {this.state.downloadComplete - ? - : null} - -
    -
    - -

    Error

    - -
    -
    -

    We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.

    -
    -
    -
    - ); +const env = process.env.NODE_ENV || 'development'; +const config = require(`./config/${env}`); +const logs = []; +const app = { + env: env, + config: config, + store: store, + logs: logs, + log: function(message) { + console.log(message); + logs.push(message); } -}); - - -export default App; +} +global.app = app; +module.exports = app; +// +// import React from 'react'; +// import {Line} from 'rc-progress'; +// +// import lbry from './lbry.js'; +// import SettingsPage from './page/settings.js'; +// import HelpPage from './page/help.js'; +// import WatchPage from './page/watch.js'; +// import ReportPage from './page/report.js'; +// import StartPage from './page/start.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 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 +// // electron-dl throws errors when you try to get the filename +// getUpgradeFilename: function() { +// switch (process.platform) { +// case 'darwin': +// return `LBRY-${this._version}.dmg`; +// case 'linux': +// return `LBRY_${this._version}_amd64.deb`; +// case 'windows': +// return `LBRY.Setup.${this._version}.exe`; +// default: +// throw 'Unknown platform'; +// } +// }, +// 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 : decodeURIComponent(pageArgs) +// }; +// }, +// getInitialState: function() { +// return Object.assign(this.getViewingPageAndArgs(window.location.search), { +// viewingPage: 'discover', +// appUrl: null, +// errorInfo: null, +// modal: null, +// downloadProgress: null, +// downloadComplete: false, +// }); +// }, +// componentWillMount: function() { +// window.addEventListener("popstate", this.onHistoryPop); +// +// document.addEventListener('unhandledError', (event) => { +// this.alertError(event.detail); +// }); +// +// //open links in external browser and skip full redraw on changing page +// document.addEventListener('click', (event) => { +// var target = event.target; +// while (target && target !== document) { +// if (target.matches('a[href^="http"]')) { +// event.preventDefault(); +// shell.openExternal(target.href); +// return; +// } +// if (target.matches('a[href^="?"]')) { +// event.preventDefault(); +// if (this._isMounted) { +// let appUrl = target.getAttribute('href'); +// this._storeHistoryOfNextRender = true; +// this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl })); +// document.body.scrollTop = 0; +// } +// } +// target = target.parentNode; +// } +// }); +// +// if (!sessionStorage.getItem('upgradeSkipped')) { +// lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => { +// if (upgradeAvailable) { +// this._version = remoteVersion; +// this.setState({ +// modal: 'upgrade', +// }); +// } +// }); +// } +// }, +// closeModal: function() { +// this.setState({ +// modal: null, +// }); +// }, +// componentDidMount: function() { +// this._isMounted = true; +// }, +// componentWillUnmount: function() { +// this._isMounted = false; +// window.removeEventListener("popstate", this.onHistoryPop); +// }, +// onHistoryPop: function() { +// this.setState(this.getViewingPageAndArgs(location.search)); +// }, +// onSearch: function(term) { +// this._storeHistoryOfNextRender = true; +// const isShow = term.startsWith('lbry://'); +// this.setState({ +// viewingPage: isShow ? "show" : "search", +// appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term), +// pageArgs: term +// }); +// }, +// onSubmit: function(uri) { +// this._storeHistoryOfNextRender = true; +// this.setState({ +// address: uri, +// appUrl: "?show=" + encodeURIComponent(uri), +// viewingPage: "show", +// pageArgs: uri +// }) +// }, +// 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); +// +// let options = { +// onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}), +// directory: dir, +// }; +// download(remote.getCurrentWindow(), this.getUpdateUrl(), 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. +// */ +// +// this._upgradeDownloadItem = downloadItem; +// this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename()); +// this.setState({ +// downloadComplete: true +// }); +// }); +// this.setState({modal: 'downloading'}); +// }, +// handleStartUpgradeClicked: function() { +// ipcRenderer.send('upgrade', this._upgradeDownloadPath); +// }, +// cancelUpgrade: function() { +// if (this._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 { +// this._upgradeDownloadItem.cancel(); +// } catch (err) { +// // Do nothing +// } +// } +// this.setState({ +// downloadProgress: null, +// downloadComplete: false, +// modal: null, +// }); +// }, +// handleSkipClicked: function() { +// sessionStorage.setItem('upgradeSkipped', true); +// this.setState({ +// modal: null, +// }); +// }, +// alertError: function(error) { +// var 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(
  • {label}: {val}
  • ); +// } +// +// this.setState({ +// modal: 'error', +// errorInfo:
      {errorInfoList}
    , +// }); +// }, +// getContentAndAddress: function() +// { +// switch(this.state.viewingPage) +// { +// case 'search': +// return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', ]; +// case 'settings': +// return ["Settings", "icon-gear", ]; +// case 'help': +// return ["Help", "icon-question", ]; +// case 'report': +// return ['Report an Issue', 'icon-file', ]; +// case 'downloaded': +// return ["Downloads & Purchases", "icon-folder", ]; +// case 'published': +// return ["Publishes", "icon-folder", ]; +// case 'start': +// return ["Start", "icon-file", ]; +// case 'rewards': +// return ["Rewards", "icon-bank", ]; +// case 'wallet': +// case 'send': +// case 'receive': +// return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", ] +// case 'show': +// return [lbryuri.normalize(this.state.pageArgs), "icon-file", ]; +// case 'publish': +// return ["Publish", "icon-upload", ]; +// case 'developer': +// return ["Developer", "icon-file", ]; +// case 'discover': +// default: +// return ["Home", "icon-home", ]; +// } +// }, +// 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 : +//
    +//
    +//
    +// {mainContent} +//
    +// +// Your version of LBRY is out of date and may be unreliable or insecure. +// +// +// Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null} +// +// {this.state.downloadComplete ? ( +//
    +//
    +//

    Click "Begin Upgrade" to start the upgrade process.

    +//

    The app will close, and you will be prompted to install the latest version of LBRY.

    +//

    After the install is complete, please reopen the app.

    +//
    +// ) : null } +//
    +// {this.state.downloadComplete +// ? +// : null} +// +//
    +//
    +// +//

    Error

    +// +//
    +//
    +//

    We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.

    +//
    +//
    +//
    +// ); \ No newline at end of file diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js new file mode 100644 index 000000000..0920e51d8 --- /dev/null +++ b/ui/js/component/app/index.js @@ -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) diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx new file mode 100644 index 000000000..336b39150 --- /dev/null +++ b/ui/js/component/app/view.jsx @@ -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' ? + : +
    + +
    +
    + +
    + {modal == 'upgrade' && } + {modal == 'downloading' && } + {modal == 'error' && } +
    + ); + } +}); + +export default App diff --git a/ui/js/component/downloadingModal/index.jsx b/ui/js/component/downloadingModal/index.jsx new file mode 100644 index 000000000..618f335fb --- /dev/null +++ b/ui/js/component/downloadingModal/index.jsx @@ -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) diff --git a/ui/js/component/downloadingModal/view.jsx b/ui/js/component/downloadingModal/view.jsx new file mode 100644 index 000000000..b59605e28 --- /dev/null +++ b/ui/js/component/downloadingModal/view.jsx @@ -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 ( + + Downloading Update{downloadProgress ? `: ${downloadProgress}%` : null} + + {downloadComplete ? ( +
    +
    +

    Click "Begin Upgrade" to start the upgrade process.

    +

    The app will close, and you will be prompted to install the latest version of LBRY.

    +

    After the install is complete, please reopen the app.

    +
    + ) : null } +
    + {downloadComplete + ? + : null} + +
    +
    + ) + } +} + +export default DownloadingModal diff --git a/ui/js/component/drawer/index.jsx b/ui/js/component/drawer/index.jsx new file mode 100644 index 000000000..b5f666c92 --- /dev/null +++ b/ui/js/component/drawer/index.jsx @@ -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) diff --git a/ui/js/component/drawer/view.jsx b/ui/js/component/drawer/view.jsx new file mode 100644 index 000000000..d2a381b88 --- /dev/null +++ b/ui/js/component/drawer/view.jsx @@ -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 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() + } +} + +export default Drawer; diff --git a/ui/js/component/errorModal/index.jsx b/ui/js/component/errorModal/index.jsx new file mode 100644 index 000000000..c7db4cef4 --- /dev/null +++ b/ui/js/component/errorModal/index.jsx @@ -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) diff --git a/ui/js/component/errorModal/view.jsx b/ui/js/component/errorModal/view.jsx new file mode 100644 index 000000000..676a2d52b --- /dev/null +++ b/ui/js/component/errorModal/view.jsx @@ -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 =
      + 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(
    • {label}: {val}
    • ); + } + + return( + +

      Error

      + +
      +
      +

      We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.

      +
      +
      + ) + } +} + +export default ErrorModal diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 21d25eb47..aef2f5380 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -1,10 +1,10 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryuri from '../lbryuri.js'; -import {Link} from '../component/link.js'; import {Icon, FilePrice} from '../component/common.js'; import {Modal} from './modal.js'; import {FormField} from './form.js'; +import Link from 'component/link'; import {ToolTip} from '../component/tooltip.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js'; diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 73adb63b2..ae8e0e3fe 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.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 {BusyMessage, TruncatedText, FilePrice} from '../component/common.js'; import UriIndicator from '../component/channel-indicator.js'; diff --git a/ui/js/component/header.js b/ui/js/component/header.js index 848f076a2..4ab99c061 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -1,6 +1,6 @@ import React from 'react'; import lbryuri from '../lbryuri.js'; -import {Link} from './link.js'; +import Link from 'component/link'; import {Icon, CreditAmount} from './common.js'; var Header = React.createClass({ diff --git a/ui/js/component/link/index.jsx b/ui/js/component/link/index.jsx new file mode 100644 index 000000000..2ad86dc05 --- /dev/null +++ b/ui/js/component/link/index.jsx @@ -0,0 +1,7 @@ +import React from 'react' +import { + connect, +} from 'react-redux' +import Link from './view' + +export default connect(null, null)(Link) diff --git a/ui/js/component/link/view.js b/ui/js/component/link/view.js new file mode 100644 index 000000000..e4b361241 --- /dev/null +++ b/ui/js/component/link/view.js @@ -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 = ( + + {'icon' in props ? : null} + {label ? {label} : null} + {'badge' in props ? {badge} : null} + + ) + } + + return ( + + {content} + + ); +} + +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 = ( +// +// {'icon' in this.props ? : null} +// {{this.props.label}} +// {'badge' in this.props ? {this.props.badge} : null} +// +// ); +// } + +// return ( +// +// {content} +// +// ); +// } +// }); diff --git a/ui/js/component/load_screen.js b/ui/js/component/load_screen.js index 4f191d891..4002d5191 100644 --- a/ui/js/component/load_screen.js +++ b/ui/js/component/load_screen.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.js'; import {BusyMessage, Icon} from './common.js'; -import {Link} from '../component/link.js' +import Link from 'component/link' var LoadScreen = React.createClass({ propTypes: { diff --git a/ui/js/component/menu.js b/ui/js/component/menu.js index c35952426..c43d81f0e 100644 --- a/ui/js/component/menu.js +++ b/ui/js/component/menu.js @@ -1,6 +1,6 @@ import React from 'react'; import {Icon} from './common.js'; -import {Link} from '../component/link.js'; +import Link from 'component/link'; export let DropDownMenuItem = React.createClass({ propTypes: { diff --git a/ui/js/component/modal.js b/ui/js/component/modal.js index dbb8ff646..9dfedba6b 100644 --- a/ui/js/component/modal.js +++ b/ui/js/component/modal.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactModal from 'react-modal'; -import {Link} from './link.js'; +import Link from 'component/link'; export const Modal = React.createClass({ diff --git a/ui/js/component/router/index.jsx b/ui/js/component/router/index.jsx new file mode 100644 index 000000000..c75222949 --- /dev/null +++ b/ui/js/component/router/index.jsx @@ -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); diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx new file mode 100644 index 000000000..c6aad0588 --- /dev/null +++ b/ui/js/component/router/view.jsx @@ -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': , + 'help': , + 'watch': , + 'report': , + 'downloaded': , + 'published': , + 'start': , + 'claim': , + 'wallet': , + 'send': , + 'receive': , + 'show': , + 'publish': , + 'developer': , + 'discover': , + }) +} + +export default Router diff --git a/ui/js/component/upgradeModal/index.jsx b/ui/js/component/upgradeModal/index.jsx new file mode 100644 index 000000000..e2872978c --- /dev/null +++ b/ui/js/component/upgradeModal/index.jsx @@ -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) diff --git a/ui/js/component/upgradeModal/view.jsx b/ui/js/component/upgradeModal/view.jsx new file mode 100644 index 000000000..a2a181c79 --- /dev/null +++ b/ui/js/component/upgradeModal/view.jsx @@ -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 ( + + Your version of LBRY is out of date and may be unreliable or insecure. + + ) + } +} + +export default UpgradeModal diff --git a/ui/js/config/development.js b/ui/js/config/development.js new file mode 100644 index 000000000..631f37565 --- /dev/null +++ b/ui/js/config/development.js @@ -0,0 +1,2 @@ +module.exports = { +} diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js new file mode 100644 index 000000000..17f05441a --- /dev/null +++ b/ui/js/constants/action_types.js @@ -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' diff --git a/ui/js/main.js b/ui/js/main.js index 610ca8594..c346a0e5c 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -3,13 +3,16 @@ 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 App from './component/app/index.js'; import SplashScreen from './component/splash.js'; import SnackBar from './component/snack-bar.js'; import {AuthOverlay} from './component/auth.js'; +import { Provider } from 'react-redux'; +import store from 'store.js'; const {remote} = require('electron'); const contextMenu = remote.require('./menu/context-menu'); +const app = require('./app') lbry.showMenuIfNeeded(); @@ -19,7 +22,9 @@ window.addEventListener('contextmenu', (event) => { event.preventDefault(); }); -let init = function() { +const initialState = app.store.getState(); + +var init = function() { window.lbry = lbry; window.lighthouse = lighthouse; let canvas = document.getElementById('canvas'); @@ -30,7 +35,7 @@ let init = function() { 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(
      { lbryio.enabled ? : '' }
      , canvas) + ReactDOM.render(
      { lbryio.enabled ? : '' }
      , canvas) } if (window.sessionStorage.getItem('loaded') == 'y') { diff --git a/ui/js/page/developer.js b/ui/js/page/developer.js index 377204852..31b3b6634 100644 --- a/ui/js/page/developer.js +++ b/ui/js/page/developer.js @@ -1,7 +1,7 @@ import lbry from '../lbry.js'; import React from 'react'; import {FormField} from '../component/form.js'; -import {Link} from '../component/link.js'; +import Link from '../component/link'; const fs = require('fs'); const {ipcRenderer} = require('electron'); diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index d522a99f8..912a38f8c 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,6 +1,8 @@ import React from 'react'; -import lbryio from '../lbryio.js'; -import {FileTile, FileTileStream} from '../component/file-tile.js'; +import lbry from '../lbry.js'; +import lighthouse from '../lighthouse.js'; +import {FileTile} from '../component/file-tile.js'; +import Link from 'component/link'; import {ToolTip} from '../component/tooltip.js'; const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' + diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 71f8e2fc2..8ae732dfe 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -1,8 +1,8 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryuri from '../lbryuri.js'; -import {Link} from '../component/link.js'; -import {FormField} from '../component/form.js'; +import Link from 'component/link'; +import FormField from '../component/form.js'; import {SubHeader} from '../component/header.js'; import {FileTileStream} from '../component/file-tile.js'; import rewards from '../rewards.js'; diff --git a/ui/js/page/help/index.jsx b/ui/js/page/help/index.jsx new file mode 100644 index 000000000..0663d5ffa --- /dev/null +++ b/ui/js/page/help/index.jsx @@ -0,0 +1,7 @@ +import React from 'react' +import { + connect +} from 'react-redux' +import HelpPage from './view' + +export default connect(null, null)(HelpPage) diff --git a/ui/js/page/help.js b/ui/js/page/help/view.jsx similarity index 95% rename from ui/js/page/help.js rename to ui/js/page/help/view.jsx index d6a28ae99..5db7f6472 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help/view.jsx @@ -1,10 +1,9 @@ //@TODO: Customize advice based on OS -//@TODO: Customize advice based on OS import React from 'react'; -import lbry from '../lbry.js'; -import {Link} from '../component/link.js'; +import lbry from 'lbry.js'; +import Link from 'component/link'; 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({ getInitialState: function() { @@ -119,4 +118,4 @@ var HelpPage = React.createClass({ } }); -export default HelpPage; \ No newline at end of file +export default HelpPage; diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 03583136b..d443d8737 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,9 +1,8 @@ import React from 'react'; import lbry from '../lbry.js'; -import {FormField, FormRow} from '../component/form.js'; -import {Link} from '../component/link.js'; +import FormField from '../component/form.js'; +import Link from 'component/link'; import rewards from '../rewards.js'; -import lbryio from '../lbryio.js'; import Modal from '../component/modal.js'; var PublishPage = React.createClass({ diff --git a/ui/js/page/report.js b/ui/js/page/report.js index e76905d4b..03f0bb915 100644 --- a/ui/js/page/report.js +++ b/ui/js/page/report.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Link} from '../component/link.js'; +import Link from 'component/link'; import Modal from '../component/modal.js'; import lbry from '../lbry.js'; diff --git a/ui/js/page/show.js b/ui/js/page/show.js index c72a8bde1..e9013716b 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -5,7 +5,7 @@ import lbryuri from '../lbryuri.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'; +import Link from '../component/link'; import UriIndicator from '../component/channel-indicator.js'; var FormatItem = React.createClass({ diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index f50f4e1be..a9b3a92a0 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import {Link} from '../component/link.js'; +import Link from 'component/link'; import Modal from '../component/modal.js'; import {SubHeader} from '../component/header.js'; import {FormField, FormRow} from '../component/form.js'; diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js deleted file mode 100644 index 2f01ae93b..000000000 --- a/ui/js/page/watch.js +++ /dev/null @@ -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 (
      - - - You don't have enough LBRY credits to pay for this stream. - - - Are you sure you'd like to buy {this.props.metadata.title} for credits? - -
      ); - } -}); - - -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 ( -
      { - this.state.isPlaying ? - !this.state.readyToPlay ? - this is the world's worst loading screen and we shipped our software with it anyway...

      {this.state.loadStatusMessage}
      : - : -
      - -
      - }
      - ); - } -}) diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js new file mode 100644 index 000000000..6859b60f3 --- /dev/null +++ b/ui/js/reducers/app.js @@ -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; +} diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js new file mode 100644 index 000000000..0bb22541b --- /dev/null +++ b/ui/js/selectors/app.js @@ -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 +) diff --git a/ui/js/store.js b/ui/js/store.js new file mode 100644 index 000000000..9d6ac13c4 --- /dev/null +++ b/ui/js/store.js @@ -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; diff --git a/ui/package.json b/ui/package.json index ead9a1e2b..4c4b9e937 100644 --- a/ui/package.json +++ b/ui/package.json @@ -27,6 +27,11 @@ "react": "^15.4.0", "react-dom": "^15.4.0", "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" }, "devDependencies": { diff --git a/ui/webpack.config.js b/ui/webpack.config.js index be349baa9..9afb1d82f 100644 --- a/ui/webpack.config.js +++ b/ui/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const appPath = path.resolve(__dirname, 'js'); const PATHS = { app: path.join(__dirname, 'app'), @@ -13,6 +14,10 @@ module.exports = { filename: "bundle.js" }, devtool: 'source-map', + resolve: { + root: appPath, + extensions: ['', '.js', '.jsx', '.css'], + }, module: { preLoaders: [ { @@ -25,8 +30,8 @@ module.exports = { loaders: [ { test: /\.css$/, loader: "style!css" }, { - test: /\.jsx?$/, - loader: 'babel', + test: /\.jsx?$/, + loader: 'babel', query: { cacheDirectory: true, presets:[ 'es2015', 'react', 'stage-2' ] diff --git a/ui/webpack.dev.config.js b/ui/webpack.dev.config.js index 358d6c04d..cb284e233 100644 --- a/ui/webpack.dev.config.js +++ b/ui/webpack.dev.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const appPath = path.resolve(__dirname, 'js'); const PATHS = { app: path.join(__dirname, 'app'), @@ -16,6 +17,10 @@ module.exports = { debug: true, cache: true, devtool: 'eval', + resolve: { + root: appPath, + extensions: ['', '.js', '.jsx', '.css'], + }, module: { preLoaders: [ { @@ -28,9 +33,9 @@ module.exports = { loaders: [ { test: /\.css$/, loader: "style!css" }, { - test: /\.jsx?$/, - loader: 'babel', - query: { + test: /\.jsx?$/, + loader: 'babel', + query: { cacheDirectory: true, presets:[ 'es2015', 'react', 'stage-2' ] }