Redux proof of concept #33
103 changed files with 5900 additions and 2192 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,3 +14,5 @@ dist
|
||||||
|
|
||||||
*.pyc
|
*.pyc
|
||||||
.#*
|
.#*
|
||||||
|
|
||||||
|
build/daemon.zip
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const {app, BrowserWindow, ipcMain} = require('electron');
|
const {app, BrowserWindow, ipcMain} = require('electron');
|
||||||
|
|
||||||
|
require('electron-debug')({showDevTools: true});
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const jayson = require('jayson');
|
const jayson = require('jayson');
|
||||||
// tree-kill has better cross-platform handling of
|
// tree-kill has better cross-platform handling of
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
},
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
210
ui/js/actions/app.js
Normal file
210
ui/js/actions/app.js
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectUpdateUrl,
|
||||||
|
selectUpgradeDownloadDir,
|
||||||
|
selectUpgradeDownloadItem,
|
||||||
|
selectUpgradeFilename,
|
||||||
|
selectPageTitle,
|
||||||
|
} 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 function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.NAVIGATE,
|
||||||
|
data: {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const pageTitle = selectPageTitle(state)
|
||||||
|
window.document.title = pageTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doDaemonReady() {
|
||||||
|
return {
|
||||||
|
type: types.DAEMON_READY
|
||||||
|
}
|
||||||
|
}
|
39
ui/js/actions/availability.js
Normal file
39
ui/js/actions/availability.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export function doFetchUriAvailability(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_AVAILABILITY_STARTED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
|
||||||
|
const successCallback = (availability) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_AVAILABILITY_COMPLETED,
|
||||||
|
data: {
|
||||||
|
availability,
|
||||||
|
uri,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCallback = () => {
|
||||||
|
console.debug('error')
|
||||||
|
}
|
||||||
|
|
||||||
|
lbry.get_availability({ uri }, successCallback, errorCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchCurrentUriAvailability() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const uri = selectCurrentUri(state)
|
||||||
|
|
||||||
|
dispatch(doFetchUriAvailability(uri))
|
||||||
|
}
|
||||||
|
}
|
291
ui/js/actions/content.js
Normal file
291
ui/js/actions/content.js
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import lbryio from 'lbryio'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import rewards from 'rewards'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import {
|
||||||
|
selectSearchTerm,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
selectDownloadingByUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectCurrentUriCostInfo,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import {
|
||||||
|
selectCurrentResolvedUriClaimOutpoint,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectClaimsByUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
doOpenModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doFetchCostInfoForUri,
|
||||||
|
} from 'actions/cost_info'
|
||||||
|
import batchActions from 'util/batchActions'
|
||||||
|
|
||||||
|
export function doResolveUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.RESOLVE_URI_STARTED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.resolve({ uri }).then((resolutionInfo) => {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
certificate,
|
||||||
|
} = resolutionInfo
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.RESOLVE_URI_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
claim,
|
||||||
|
certificate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(doFetchCostInfoForUri(uri))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchDownloadedContent() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_DOWNLOADED_CONTENT_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.claim_list_mine().then((myClaimInfos) => {
|
||||||
|
lbry.file_list().then((fileInfos) => {
|
||||||
|
const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
channelName: fileInfo.channel_name,
|
||||||
|
contentName: fileInfo.name,
|
||||||
|
})
|
||||||
|
const claim = selectClaimsByUri(state)[uri]
|
||||||
|
if (!claim) dispatch(doResolveUri(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_DOWNLOADED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchPublishedContent() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_PUBLISHED_CONTENT_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.claim_list_mine().then((claimInfos) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_MY_CLAIMS_COMPLETED,
|
||||||
|
data: {
|
||||||
|
claims: claimInfos,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lbry.file_list().then((fileInfos) => {
|
||||||
|
const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout)
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_PUBLISHED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchFeaturedContent() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FEATURED_CONTENT_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
const success = ({ Categories, Uris }) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
categories: Categories,
|
||||||
|
uris: Uris,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(Uris).forEach((category) => {
|
||||||
|
Uris[category].forEach((uri) => {
|
||||||
|
dispatch(doResolveUri(lbryuri.normalize(uri)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const failure = () => {
|
||||||
|
}
|
||||||
|
|
||||||
|
lbryio.call('discover', 'list', { version: "early-access" } )
|
||||||
|
.then(success, failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doUpdateLoadStatus(uri, outpoint) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
lbry.file_list({
|
||||||
|
outpoint: outpoint,
|
||||||
|
full_status: true,
|
||||||
|
}).then(([fileInfo]) => {
|
||||||
|
if(!fileInfo || fileInfo.written_bytes == 0) {
|
||||||
|
// download hasn't started yet
|
||||||
|
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
|
||||||
|
} else if (fileInfo.completed) {
|
||||||
|
// TODO this isn't going to get called if they reload the client before
|
||||||
|
// the download finished
|
||||||
|
rewards.claimNextPurchaseReward()
|
||||||
|
dispatch({
|
||||||
|
type: types.DOWNLOADING_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// ready to play
|
||||||
|
const {
|
||||||
|
total_bytes,
|
||||||
|
written_bytes,
|
||||||
|
} = fileInfo
|
||||||
|
const progress = (written_bytes / total_bytes) * 100
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.DOWNLOADING_PROGRESSED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
progress,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doPlayVideo(uri) {
|
||||||
|
return {
|
||||||
|
type: types.PLAY_VIDEO_STARTED,
|
||||||
|
data: { uri }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doDownloadFile(uri, streamInfo) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
lbry.file_list({ outpoint: streamInfo.outpoint, full_status: true }).then(([fileInfo]) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.DOWNLOADING_STARTED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
lbryio.call('file', 'view', {
|
||||||
|
uri: uri,
|
||||||
|
outpoint: streamInfo.outpoint,
|
||||||
|
claimId: streamInfo.claim_id,
|
||||||
|
}).catch(() => {})
|
||||||
|
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doLoadVideo() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const uri = selectCurrentUri(state)
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.LOADING_VIDEO_STARTED,
|
||||||
|
data: {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.get({ uri }).then(streamInfo => {
|
||||||
|
if (streamInfo === null || typeof streamInfo !== 'object') {
|
||||||
|
dispatch({
|
||||||
|
type: types.LOADING_VIDEO_FAILED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
dispatch(doOpenModal('timedOut'))
|
||||||
|
} else {
|
||||||
|
dispatch(doDownloadFile(uri, streamInfo))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doWatchVideo() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const uri = selectCurrentUri(state)
|
||||||
|
const balance = selectBalance(state)
|
||||||
|
const fileInfo = selectCurrentUriFileInfo(state)
|
||||||
|
const costInfo = selectCurrentUriCostInfo(state)
|
||||||
|
const downloadingByUri = selectDownloadingByUri(state)
|
||||||
|
const alreadyDownloading = !!downloadingByUri[uri]
|
||||||
|
const { cost } = costInfo
|
||||||
|
|
||||||
|
// we already fully downloaded the file
|
||||||
|
if (fileInfo && fileInfo.completed) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are already downloading the file
|
||||||
|
if (alreadyDownloading) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// the file is free or we have partially downloaded it
|
||||||
|
if (cost <= 0.01 || fileInfo.download_directory) {
|
||||||
|
dispatch(doLoadVideo())
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cost > balance) {
|
||||||
|
dispatch(doOpenModal('notEnoughCredits'))
|
||||||
|
} else {
|
||||||
|
dispatch(doOpenModal('affirmPurchase'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
36
ui/js/actions/cost_info.js
Normal file
36
ui/js/actions/cost_info.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
|
||||||
|
export function doFetchCostInfoForUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_COST_INFO_STARTED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.getCostInfo(uri).then(costInfo => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_COST_INFO_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
costInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchCurrentUriCostInfo() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const uri = selectCurrentUri(state)
|
||||||
|
|
||||||
|
dispatch(doFetchCostInfoForUri(uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
102
ui/js/actions/file_info.js
Normal file
102
ui/js/actions/file_info.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectCurrentUriClaimOutpoint,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
|
||||||
|
const {
|
||||||
|
shell,
|
||||||
|
} = require('electron')
|
||||||
|
|
||||||
|
export function doFetchCurrentUriFileInfo() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const uri = selectCurrentUri(state)
|
||||||
|
const outpoint = selectCurrentUriClaimOutpoint(state)
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FILE_INFO_STARTED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
outpoint,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.file_list({ outpoint: outpoint, full_status: true }).then(([fileInfo]) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FILE_INFO_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doOpenFileInShell(fileInfo) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
shell.openItem(fileInfo.download_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doOpenFileInFolder(fileInfo) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
shell.showItemInFolder(fileInfo.download_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doDeleteFile(uri, fileInfo, deleteFromComputer) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.DELETE_FILE_STARTED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
deleteFromComputer,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const successCallback = () => {
|
||||||
|
dispatch({
|
||||||
|
type: types.DELETE_FILE_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dispatch(doCloseModal())
|
||||||
|
}
|
||||||
|
|
||||||
|
lbry.removeFile(fileInfo.outpoint, deleteFromComputer, successCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchDownloadedContent() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_DOWNLOADED_CONTENT_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.claim_list_mine().then((myClaimInfos) => {
|
||||||
|
lbry.file_list().then((fileInfos) => {
|
||||||
|
const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_DOWNLOADED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
36
ui/js/actions/rewards.js
Normal file
36
ui/js/actions/rewards.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import lbryio from 'lbryio';
|
||||||
|
import rewards from 'rewards'
|
||||||
|
|
||||||
|
export function doFetchRewards() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_REWARDS_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
lbryio.call('reward', 'list', {}).then(function(userRewards) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_REWARDS_COMPLETED,
|
||||||
|
data: { userRewards }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doClaimReward(rewardType) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
try {
|
||||||
|
rewards.claimReward(rewards[rewardType])
|
||||||
|
dispatch({
|
||||||
|
type: types.REWARD_CLAIMED,
|
||||||
|
data: {
|
||||||
|
reward: rewards[rewardType]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch(err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
ui/js/actions/search.js
Normal file
77
ui/js/actions/search.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import lbryio from 'lbryio'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import lighthouse from 'lighthouse'
|
||||||
|
import {
|
||||||
|
selectSearchQuery,
|
||||||
|
} from 'selectors/search'
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export function doSearchContent(query) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const page = selectCurrentPage(state)
|
||||||
|
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return dispatch({
|
||||||
|
type: types.SEARCH_CANCELLED,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.SEARCH_STARTED,
|
||||||
|
data: { query }
|
||||||
|
})
|
||||||
|
|
||||||
|
if(page != 'discover' && query != undefined) dispatch(doNavigate('discover'))
|
||||||
|
|
||||||
|
lighthouse.search(query).then(results => {
|
||||||
|
results.forEach(result => {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
channelName: result.channel_name,
|
||||||
|
contentName: result.name,
|
||||||
|
claimId: result.channel_id || result.claim_id,
|
||||||
|
})
|
||||||
|
dispatch(doResolveUri(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.SEARCH_COMPLETED,
|
||||||
|
data: {
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doActivateSearch() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const page = selectCurrentPage(state)
|
||||||
|
const query = selectSearchQuery(state)
|
||||||
|
|
||||||
|
if(page != 'discover' && query != undefined) dispatch(doNavigate('discover'))
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.ACTIVATE_SEARCH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doDeactivateSearch() {
|
||||||
|
return {
|
||||||
|
type: types.DEACTIVATE_SEARCH,
|
||||||
|
}
|
||||||
|
}
|
125
ui/js/actions/wallet.js
Normal file
125
ui/js/actions/wallet.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectDraftTransaction,
|
||||||
|
selectDraftTransactionAmount,
|
||||||
|
selectBalance,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import {
|
||||||
|
doOpenModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
|
||||||
|
export function doUpdateBalance(balance) {
|
||||||
|
return {
|
||||||
|
type: types.UPDATE_BALANCE,
|
||||||
|
data: {
|
||||||
|
balance: balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchTransactions() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_TRANSACTIONS_STARTED
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.call('get_transaction_history', {}, (results) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_TRANSACTIONS_COMPLETED,
|
||||||
|
data: {
|
||||||
|
transactions: results
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doGetNewAddress() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.GET_NEW_ADDRESS_STARTED
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.wallet_new_address().then(function(address) {
|
||||||
|
localStorage.setItem('wallet_address', address);
|
||||||
|
dispatch({
|
||||||
|
type: types.GET_NEW_ADDRESS_COMPLETED,
|
||||||
|
data: { address }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCheckAddressIsMine(address) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.CHECK_ADDRESS_IS_MINE_STARTED
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.checkAddressIsMine(address, (isMine) => {
|
||||||
|
if (!isMine) dispatch(doGetNewAddress())
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.CHECK_ADDRESS_IS_MINE_COMPLETED
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSendDraftTransaction() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const draftTx = selectDraftTransaction(state)
|
||||||
|
const balance = selectBalance(state)
|
||||||
|
const amount = selectDraftTransactionAmount(state)
|
||||||
|
|
||||||
|
if (balance - amount < 1) {
|
||||||
|
return dispatch(doOpenModal('insufficientBalance'))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.SEND_TRANSACTION_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
const successCallback = (results) => {
|
||||||
|
if(results === true) {
|
||||||
|
dispatch({
|
||||||
|
type: types.SEND_TRANSACTION_COMPLETED,
|
||||||
|
})
|
||||||
|
dispatch(doOpenModal('transactionSuccessful'))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dispatch({
|
||||||
|
type: types.SEND_TRANSACTION_FAILED,
|
||||||
|
data: { error: results }
|
||||||
|
})
|
||||||
|
dispatch(doOpenModal('transactionFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCallback = (error) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.SEND_TRANSACTION_FAILED,
|
||||||
|
data: { error: error.message }
|
||||||
|
})
|
||||||
|
dispatch(doOpenModal('transactionFailed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
lbry.sendToAddress(draftTx.amount, draftTx.address, successCallback, errorCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetDraftTransactionAmount(amount) {
|
||||||
|
return {
|
||||||
|
type: types.SET_DRAFT_TRANSACTION_AMOUNT,
|
||||||
|
data: { amount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetDraftTransactionAddress(address) {
|
||||||
|
return {
|
||||||
|
type: types.SET_DRAFT_TRANSACTION_ADDRESS,
|
||||||
|
data: { address }
|
||||||
|
}
|
||||||
|
}
|
37
ui/js/app.js
37
ui/js/app.js
|
@ -1,3 +1,22 @@
|
||||||
|
import store from 'store.js';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.app = app;
|
||||||
|
module.exports = app;
|
||||||
|
|
||||||
|
/*
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Line} from 'rc-progress';
|
import {Line} from 'rc-progress';
|
||||||
|
|
||||||
|
@ -18,7 +37,7 @@ import {FileListDownloaded, FileListPublished} from './page/file-list.js';
|
||||||
import Drawer from './component/drawer.js';
|
import Drawer from './component/drawer.js';
|
||||||
import Header from './component/header.js';
|
import Header from './component/header.js';
|
||||||
import {Modal, ExpandableModal} from './component/modal.js';
|
import {Modal, ExpandableModal} from './component/modal.js';
|
||||||
import {Link} from './component/link.js';
|
import Link from './component/link';
|
||||||
|
|
||||||
|
|
||||||
const {remote, ipcRenderer, shell} = require('electron');
|
const {remote, ipcRenderer, shell} = require('electron');
|
||||||
|
@ -163,11 +182,6 @@ var App = React.createClass({
|
||||||
};
|
};
|
||||||
download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
|
download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
|
||||||
.then(downloadItem => {
|
.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._upgradeDownloadItem = downloadItem;
|
||||||
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
|
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
|
||||||
|
@ -182,11 +196,6 @@ var App = React.createClass({
|
||||||
},
|
},
|
||||||
cancelUpgrade: function() {
|
cancelUpgrade: function() {
|
||||||
if (this._upgradeDownloadItem) {
|
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 {
|
try {
|
||||||
this._upgradeDownloadItem.cancel();
|
this._upgradeDownloadItem.cancel();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -330,8 +339,4 @@ var App = React.createClass({
|
||||||
</ExpandableModal>
|
</ExpandableModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
*/
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export default App;
|
|
37
ui/js/component/app/index.js
Normal file
37
ui/js/component/app/index.js
Normal 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)
|
46
ui/js/component/app/view.jsx
Normal file
46
ui/js/component/app/view.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Router from 'component/router'
|
||||||
|
import Drawer from 'component/drawer';
|
||||||
|
import Header from 'component/header';
|
||||||
|
import ErrorModal from 'component/errorModal'
|
||||||
|
import DownloadingModal from 'component/downloadingModal'
|
||||||
|
import UpgradeModal from 'component/upgradeModal'
|
||||||
|
import {Line} from 'rc-progress';
|
||||||
|
|
||||||
|
const App = React.createClass({
|
||||||
|
componentWillMount: function() {
|
||||||
|
document.addEventListener('unhandledError', (event) => {
|
||||||
|
this.props.alertError(event.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.props.upgradeSkipped) {
|
||||||
|
this.props.checkUpgradeAvailable()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
openDrawer,
|
||||||
|
closeDrawer,
|
||||||
|
modal,
|
||||||
|
drawerOpen,
|
||||||
|
headerLinks,
|
||||||
|
search,
|
||||||
|
searchTerm,
|
||||||
|
} = this.props
|
||||||
|
const searchQuery = (currentPage == 'discover' && searchTerm ? searchTerm : '')
|
||||||
|
|
||||||
|
return <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
|
|
@ -2,7 +2,8 @@ import React from "react";
|
||||||
import lbryio from "../lbryio.js";
|
import lbryio from "../lbryio.js";
|
||||||
import Modal from "./modal.js";
|
import Modal from "./modal.js";
|
||||||
import ModalPage from "./modal-page.js";
|
import ModalPage from "./modal-page.js";
|
||||||
import {Link, RewardLink} from "../component/link.js";
|
import {RewardLink} from "../component/reward-link.js";
|
||||||
|
import Link from "../component/link";
|
||||||
import {FormRow} from "../component/form.js";
|
import {FormRow} from "../component/form.js";
|
||||||
import {CreditAmount, Address} from "../component/common.js";
|
import {CreditAmount, Address} from "../component/common.js";
|
||||||
import {getLocal, getSession, setSession, setLocal} from '../utils.js';
|
import {getLocal, getSession, setSession, setLocal} from '../utils.js';
|
||||||
|
|
|
@ -97,54 +97,6 @@ export let CreditAmount = React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export let FilePrice = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string.isRequired,
|
|
||||||
look: React.PropTypes.oneOf(['indicator', 'plain']),
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
look: 'indicator',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.setState({
|
|
||||||
cost: null,
|
|
||||||
isEstimate: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
lbry.getCostInfo(this.props.uri).then(({cost, includesData}) => {
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState({
|
|
||||||
cost: cost,
|
|
||||||
isEstimate: !includesData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, (err) => {
|
|
||||||
// If we get an error looking up cost information, do nothing
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
if (this.state.cost === null) {
|
|
||||||
return <span className={`credit-amount credit-amount--${this.props.look}`}>???</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CreditAmount label={false} amount={this.state.cost} isEstimate={this.state.isEstimate} showFree={true} />
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var addressStyle = {
|
var addressStyle = {
|
||||||
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
|
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
|
||||||
};
|
};
|
||||||
|
|
25
ui/js/component/downloadingModal/index.jsx
Normal file
25
ui/js/component/downloadingModal/index.jsx
Normal 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)
|
40
ui/js/component/downloadingModal/view.jsx
Normal file
40
ui/js/component/downloadingModal/view.jsx
Normal 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
|
|
@ -1,67 +0,0 @@
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import React from 'react';
|
|
||||||
import {Link} from './link.js';
|
|
||||||
|
|
||||||
var DrawerItem = React.createClass({
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
subPages: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
|
|
||||||
this.props.subPages.indexOf(this.props.viewingPage) != -1);
|
|
||||||
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
|
|
||||||
height: '36px'
|
|
||||||
};
|
|
||||||
|
|
||||||
var Drawer = React.createClass({
|
|
||||||
_balanceSubscribeId: null,
|
|
||||||
|
|
||||||
handleLogoClicked: function(event) {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
|
|
||||||
window.location.href = '?developer'
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
balance: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
|
|
||||||
this.setState({
|
|
||||||
balance: balance
|
|
||||||
});
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._balanceSubscribeId) {
|
|
||||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<nav id="drawer">
|
|
||||||
<div id="drawer-handle">
|
|
||||||
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
|
|
||||||
<a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
|
|
||||||
</div>
|
|
||||||
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
|
|
||||||
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
|
|
||||||
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
|
|
||||||
<DrawerItem href="?wallet" subPages={['send', 'receive', 'rewards']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
|
|
||||||
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
|
|
||||||
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export default Drawer;
|
|
33
ui/js/component/drawer/index.jsx
Normal file
33
ui/js/component/drawer/index.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import Drawer from './view'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
doCloseDrawer,
|
||||||
|
doLogoClick,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doUpdateBalance,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
|
||||||
|
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)
|
68
ui/js/component/drawer/view.jsx
Normal file
68
ui/js/component/drawer/view.jsx
Normal 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,
|
||||||
|
badge,
|
||||||
|
label,
|
||||||
|
linkClick,
|
||||||
|
icon,
|
||||||
|
} = props
|
||||||
|
const isSelected = (
|
||||||
|
currentPage == href.substr(0) ||
|
||||||
|
(subPages && subPages.indexOf(currentPage) != -1)
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Link icon={icon} badge={badge} 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((balance) => {
|
||||||
|
updateBalance(balance)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._balanceSubscribeId) {
|
||||||
|
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
closeDrawerClick,
|
||||||
|
logoClick,
|
||||||
|
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']} 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;
|
23
ui/js/component/errorModal/index.jsx
Normal file
23
ui/js/component/errorModal/index.jsx
Normal 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)
|
50
ui/js/component/errorModal/view.jsx
Normal file
50
ui/js/component/errorModal/view.jsx
Normal 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
|
|
@ -1,270 +0,0 @@
|
||||||
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 {ToolTip} from '../component/tooltip.js';
|
|
||||||
import {DropDownMenu, DropDownMenuItem} from './menu.js';
|
|
||||||
|
|
||||||
const {shell} = require('electron');
|
|
||||||
|
|
||||||
let FileActionsRow = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
_fileInfoSubscribeId: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
outpoint: React.PropTypes.string.isRequired,
|
|
||||||
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
|
|
||||||
contentType: React.PropTypes.string.isRequired,
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
fileInfo: null,
|
|
||||||
modal: null,
|
|
||||||
menuOpen: false,
|
|
||||||
deleteChecked: false,
|
|
||||||
attemptingDownload: false,
|
|
||||||
attemptingRemove: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFileInfoUpdate: function(fileInfo) {
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState({
|
|
||||||
fileInfo: fileInfo ? fileInfo : false,
|
|
||||||
attemptingDownload: fileInfo ? false : this.state.attemptingDownload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tryDownload: function() {
|
|
||||||
this.setState({
|
|
||||||
attemptingDownload: true,
|
|
||||||
attemptingRemove: false
|
|
||||||
});
|
|
||||||
lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
|
||||||
lbry.getBalance((balance) => {
|
|
||||||
if (cost > balance) {
|
|
||||||
this.setState({
|
|
||||||
modal: 'notEnoughCredits',
|
|
||||||
attemptingDownload: false,
|
|
||||||
});
|
|
||||||
} else if (this.state.affirmedPurchase) {
|
|
||||||
lbry.get({uri: this.props.uri}).then((streamInfo) => {
|
|
||||||
if (streamInfo === null || typeof streamInfo !== 'object') {
|
|
||||||
this.setState({
|
|
||||||
modal: 'timedOut',
|
|
||||||
attemptingDownload: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
attemptingDownload: false,
|
|
||||||
modal: 'affirmPurchase'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
closeModal: function() {
|
|
||||||
this.setState({
|
|
||||||
modal: null,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onDownloadClick: function() {
|
|
||||||
if (!this.state.fileInfo && !this.state.attemptingDownload) {
|
|
||||||
this.tryDownload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onOpenClick: function() {
|
|
||||||
if (this.state.fileInfo && this.state.fileInfo.download_path) {
|
|
||||||
shell.openItem(this.state.fileInfo.download_path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDeleteCheckboxClicked: function(event) {
|
|
||||||
this.setState({
|
|
||||||
deleteChecked: event.target.checked,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleRevealClicked: function() {
|
|
||||||
if (this.state.fileInfo && this.state.fileInfo.download_path) {
|
|
||||||
shell.showItemInFolder(this.state.fileInfo.download_path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleRemoveClicked: function() {
|
|
||||||
this.setState({
|
|
||||||
modal: 'confirmRemove',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleRemoveConfirmed: function() {
|
|
||||||
lbry.removeFile(this.props.outpoint, this.state.deleteChecked);
|
|
||||||
this.setState({
|
|
||||||
modal: null,
|
|
||||||
fileInfo: false,
|
|
||||||
attemptingDownload: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onAffirmPurchase: function() {
|
|
||||||
this.setState({
|
|
||||||
affirmedPurchase: true,
|
|
||||||
modal: null
|
|
||||||
});
|
|
||||||
this.tryDownload();
|
|
||||||
},
|
|
||||||
openMenu: function() {
|
|
||||||
this.setState({
|
|
||||||
menuOpen: !this.state.menuOpen,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
if (this._fileInfoSubscribeId) {
|
|
||||||
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (this.state.fileInfo === null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
|
|
||||||
showMenu = !!this.state.fileInfo;
|
|
||||||
|
|
||||||
let linkBlock;
|
|
||||||
if (this.state.fileInfo === false && !this.state.attemptingDownload) {
|
|
||||||
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.onDownloadClick} />;
|
|
||||||
} else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
|
|
||||||
const
|
|
||||||
progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
|
|
||||||
label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
|
|
||||||
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
|
|
||||||
|
|
||||||
linkBlock = (
|
|
||||||
<div className="faux-button-block file-actions__download-status-bar button-set-item">
|
|
||||||
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
|
|
||||||
{labelWithIcon}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={this.onOpenClick} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = lbryuri.normalize(this.props.uri);
|
|
||||||
const title = this.props.metadata ? this.props.metadata.title : uri;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{this.state.fileInfo !== null || this.state.fileInfo.isMine
|
|
||||||
? linkBlock
|
|
||||||
: null}
|
|
||||||
{ showMenu ?
|
|
||||||
<DropDownMenu>
|
|
||||||
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
|
|
||||||
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
|
|
||||||
</DropDownMenu> : '' }
|
|
||||||
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
|
|
||||||
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
|
|
||||||
Are you sure you'd like to buy <strong>{title}</strong> for <strong><FilePrice uri={uri} metadata={this.props.metadata} label={false} look="plain" /></strong> credits?
|
|
||||||
</Modal>
|
|
||||||
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
|
|
||||||
onConfirmed={this.closeModal}>
|
|
||||||
You don't have enough LBRY credits to pay for this stream.
|
|
||||||
</Modal>
|
|
||||||
<Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
|
|
||||||
onConfirmed={this.closeModal}>
|
|
||||||
LBRY was unable to download the stream <strong>{uri}</strong>.
|
|
||||||
</Modal>
|
|
||||||
<Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
|
|
||||||
type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
|
|
||||||
onAborted={this.closeModal}>
|
|
||||||
<p>Are you sure you'd like to remove <cite>{title}</cite> from LBRY?</p>
|
|
||||||
|
|
||||||
<label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let FileActions = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
_fileInfoSubscribeId: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
outpoint: React.PropTypes.string.isRequired,
|
|
||||||
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
|
|
||||||
contentType: React.PropTypes.string,
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
available: true,
|
|
||||||
forceShowActions: false,
|
|
||||||
fileInfo: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onShowFileActionsRowClicked: function() {
|
|
||||||
this.setState({
|
|
||||||
forceShowActions: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onFileInfoUpdate: function(fileInfo) {
|
|
||||||
if (this.isMounted) {
|
|
||||||
this.setState({
|
|
||||||
fileInfo: fileInfo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
|
||||||
|
|
||||||
lbry.get_availability({uri: this.props.uri}, (availability) => {
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState({
|
|
||||||
available: availability > 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
// Take any error to mean the file is unavailable
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState({
|
|
||||||
available: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
if (this._fileInfoSubscribeId) {
|
|
||||||
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
const fileInfo = this.state.fileInfo;
|
|
||||||
if (fileInfo === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<section className="file-actions">
|
|
||||||
{
|
|
||||||
fileInfo || this.state.available || this.state.forceShowActions
|
|
||||||
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
|
|
||||||
contentType={this.props.contentType} />
|
|
||||||
: <div>
|
|
||||||
<div className="button-set-item empty">Content unavailable.</div>
|
|
||||||
<ToolTip label="Why?"
|
|
||||||
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
|
|
||||||
className="button-set-item" />
|
|
||||||
<Link label="Try Anyway" onClick={this.onShowFileActionsRowClicked} className="button-text button-set-item" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</section>);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,276 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import lbryuri from '../lbryuri.js';
|
|
||||||
import {Link} from '../component/link.js';
|
|
||||||
import {FileActions} from '../component/file-actions.js';
|
|
||||||
import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
|
|
||||||
import UriIndicator from '../component/channel-indicator.js';
|
|
||||||
|
|
||||||
/*should be merged into FileTile once FileTile is refactored to take a single id*/
|
|
||||||
export let FileTileStream = React.createClass({
|
|
||||||
_fileInfoSubscribeId: null,
|
|
||||||
_isMounted: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
metadata: React.PropTypes.object,
|
|
||||||
contentType: React.PropTypes.string.isRequired,
|
|
||||||
outpoint: React.PropTypes.string,
|
|
||||||
hasSignature: React.PropTypes.bool,
|
|
||||||
signatureIsValid: React.PropTypes.bool,
|
|
||||||
hideOnRemove: React.PropTypes.bool,
|
|
||||||
hidePrice: React.PropTypes.bool,
|
|
||||||
obscureNsfw: React.PropTypes.bool
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
showNsfwHelp: false,
|
|
||||||
isHidden: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
obscureNsfw: !lbry.getClientSetting('showNsfw'),
|
|
||||||
hidePrice: false,
|
|
||||||
hasSignature: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
if (this.props.hideOnRemove) {
|
|
||||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._fileInfoSubscribeId) {
|
|
||||||
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFileInfoUpdate: function(fileInfo) {
|
|
||||||
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
|
|
||||||
this.setState({
|
|
||||||
isHidden: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMouseOver: function() {
|
|
||||||
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
|
|
||||||
this.setState({
|
|
||||||
showNsfwHelp: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMouseOut: function() {
|
|
||||||
if (this.state.showNsfwHelp) {
|
|
||||||
this.setState({
|
|
||||||
showNsfwHelp: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (this.state.isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = lbryuri.normalize(this.props.uri);
|
|
||||||
const metadata = this.props.metadata;
|
|
||||||
const isConfirmed = !!metadata;
|
|
||||||
const title = isConfirmed ? metadata.title : uri;
|
|
||||||
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
|
||||||
return (
|
|
||||||
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
|
||||||
<div className={"row-fluid card__inner file-tile__row"}>
|
|
||||||
<div className="span3 file-tile__thumbnail-container">
|
|
||||||
<a href={'?show=' + uri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + this.props.uri} /></a>
|
|
||||||
</div>
|
|
||||||
<div className="span9">
|
|
||||||
<div className="card__title-primary">
|
|
||||||
{ !this.props.hidePrice
|
|
||||||
? <FilePrice uri={this.props.uri} />
|
|
||||||
: null}
|
|
||||||
<div className="meta"><a href={'?show=' + this.props.uri}>{uri}</a></div>
|
|
||||||
<h3>
|
|
||||||
<a href={'?show=' + uri} title={title}>
|
|
||||||
<TruncatedText lines={1}>
|
|
||||||
{title}
|
|
||||||
</TruncatedText>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__actions">
|
|
||||||
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<p className="file-tile__description">
|
|
||||||
<TruncatedText lines={2}>
|
|
||||||
{isConfirmed
|
|
||||||
? metadata.description
|
|
||||||
: <span className="empty">This file is pending confirmation.</span>}
|
|
||||||
</TruncatedText>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.showNsfwHelp
|
|
||||||
? <div className='card-overlay'>
|
|
||||||
<p>
|
|
||||||
This content is Not Safe For Work.
|
|
||||||
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let FileCardStream = React.createClass({
|
|
||||||
_fileInfoSubscribeId: null,
|
|
||||||
_isMounted: null,
|
|
||||||
_metadata: null,
|
|
||||||
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
claimInfo: React.PropTypes.object,
|
|
||||||
outpoint: React.PropTypes.string,
|
|
||||||
hideOnRemove: React.PropTypes.bool,
|
|
||||||
hidePrice: React.PropTypes.bool,
|
|
||||||
obscureNsfw: React.PropTypes.bool
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
showNsfwHelp: false,
|
|
||||||
isHidden: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
obscureNsfw: !lbry.getClientSetting('showNsfw'),
|
|
||||||
hidePrice: false,
|
|
||||||
hasSignature: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
if (this.props.hideOnRemove) {
|
|
||||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._fileInfoSubscribeId) {
|
|
||||||
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFileInfoUpdate: function(fileInfo) {
|
|
||||||
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
|
|
||||||
this.setState({
|
|
||||||
isHidden: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMouseOver: function() {
|
|
||||||
this.setState({
|
|
||||||
hovered: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleMouseOut: function() {
|
|
||||||
this.setState({
|
|
||||||
hovered: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (this.state.isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = lbryuri.normalize(this.props.uri);
|
|
||||||
const metadata = this.props.metadata;
|
|
||||||
const isConfirmed = !!metadata;
|
|
||||||
const title = isConfirmed ? metadata.title : uri;
|
|
||||||
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
|
||||||
const primaryUrl = '?show=' + uri;
|
|
||||||
return (
|
|
||||||
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
|
||||||
<div className="card__inner">
|
|
||||||
<a href={primaryUrl} className="card__link">
|
|
||||||
<div className="card__title-identity">
|
|
||||||
<h5 title={title}><TruncatedText lines={1}>{title}</TruncatedText></h5>
|
|
||||||
<div className="card__subtitle">
|
|
||||||
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span> : null}
|
|
||||||
<UriIndicator uri={uri} metadata={metadata} contentType={this.props.contentType}
|
|
||||||
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
|
|
||||||
<div className="card__content card__subtext card__subtext--two-lines">
|
|
||||||
<TruncatedText lines={2}>
|
|
||||||
{isConfirmed
|
|
||||||
? metadata.description
|
|
||||||
: <span className="empty">This file is pending confirmation.</span>}
|
|
||||||
</TruncatedText>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{this.state.showNsfwHelp && this.state.hovered
|
|
||||||
? <div className='card-overlay'>
|
|
||||||
<p>
|
|
||||||
This content is Not Safe For Work.
|
|
||||||
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let FileTile = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
outpoint: null,
|
|
||||||
claimInfo: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
|
|
||||||
lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
|
|
||||||
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
|
|
||||||
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
|
|
||||||
// In case of a failed lookup, metadata will be null, in which case the component will never display
|
|
||||||
this.setState({
|
|
||||||
claimInfo: resolutionInfo.claim,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (!this.state.claimInfo) {
|
|
||||||
if (this.props.displayStyle == 'card') {
|
|
||||||
return <FileCardStream outpoint={null} metadata={{title: this.props.uri, description: "Loading..."}} contentType={null} hidePrice={true}
|
|
||||||
hasSignature={false} signatureIsValid={false} uri={this.props.uri} />
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {txid, nout, has_signature, signature_is_valid,
|
|
||||||
value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo;
|
|
||||||
|
|
||||||
return this.props.displayStyle == 'card' ?
|
|
||||||
<FileCardStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
|
|
||||||
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props}/> :
|
|
||||||
<FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
|
|
||||||
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props} />;
|
|
||||||
}
|
|
||||||
});
|
|
68
ui/js/component/fileActions/index.js
Normal file
68
ui/js/component/fileActions/index.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectObscureNsfw,
|
||||||
|
selectHidePrice,
|
||||||
|
selectHasSignature,
|
||||||
|
selectPlatform,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectDownloadingForUri,
|
||||||
|
makeSelectLoadingForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
makeSelectAvailabilityForUri,
|
||||||
|
} from 'selectors/availability'
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
doOpenModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doOpenFileInShell,
|
||||||
|
doOpenFileInFolder,
|
||||||
|
doDeleteFile,
|
||||||
|
} from 'actions/file_info'
|
||||||
|
import {
|
||||||
|
doWatchVideo,
|
||||||
|
doLoadVideo,
|
||||||
|
} from 'actions/content'
|
||||||
|
import FileActions from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectAvailabilityForUri = makeSelectAvailabilityForUri()
|
||||||
|
const selectDownloadingForUri = makeSelectDownloadingForUri()
|
||||||
|
const selectLoadingForUri = makeSelectLoadingForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
obscureNsfw: selectObscureNsfw(state),
|
||||||
|
hidePrice: selectHidePrice(state),
|
||||||
|
hasSignature: selectHasSignature(state),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
availability: selectAvailabilityForUri(state, props),
|
||||||
|
platform: selectPlatform(state),
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
downloading: selectDownloadingForUri(state, props),
|
||||||
|
loading: selectLoadingForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
openInFolder: (fileInfo) => dispatch(doOpenFileInFolder(fileInfo)),
|
||||||
|
openInShell: (fileInfo) => dispatch(doOpenFileInShell(fileInfo)),
|
||||||
|
deleteFile: (fileInfo, deleteFromComputer) => dispatch(doDeleteFile(fileInfo, deleteFromComputer)),
|
||||||
|
openModal: (modal) => dispatch(doOpenModal(modal)),
|
||||||
|
downloadClick: () => dispatch(doWatchVideo()),
|
||||||
|
loadVideo: () => dispatch(doLoadVideo())
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileActions)
|
345
ui/js/component/fileActions/view.jsx
Normal file
345
ui/js/component/fileActions/view.jsx
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry';
|
||||||
|
import lbryuri from 'lbryuri';
|
||||||
|
import {Icon,} from 'component/common';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import {Modal} from 'component/modal';
|
||||||
|
import {FormField} from 'component/form';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {ToolTip} from 'component/tooltip';
|
||||||
|
import {DropDownMenu, DropDownMenuItem} from 'component/menu';
|
||||||
|
|
||||||
|
class FileActionsRow extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
deleteChecked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteCheckboxClicked(event) {
|
||||||
|
this.setState({
|
||||||
|
deleteChecked: event.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onAffirmPurchase() {
|
||||||
|
this.props.closeModal()
|
||||||
|
this.props.loadVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fileInfo,
|
||||||
|
platform,
|
||||||
|
downloading,
|
||||||
|
loading,
|
||||||
|
uri,
|
||||||
|
deleteFile,
|
||||||
|
openInFolder,
|
||||||
|
openInShell,
|
||||||
|
modal,
|
||||||
|
openModal,
|
||||||
|
affirmPurchase,
|
||||||
|
closeModal,
|
||||||
|
downloadClick,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const {
|
||||||
|
deleteChecked,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const metadata = fileInfo ? fileInfo.metadata : null
|
||||||
|
|
||||||
|
if (!fileInfo)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInFolderMessage = platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
|
||||||
|
showMenu = Object.keys(fileInfo).length != 0;
|
||||||
|
|
||||||
|
let linkBlock;
|
||||||
|
if (Object.keys(fileInfo).length == 0 && !downloading && !loading) {
|
||||||
|
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={downloadClick} />;
|
||||||
|
} else if (downloading || loading) {
|
||||||
|
const
|
||||||
|
progress = (fileInfo && fileInfo.written_bytes) ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
|
||||||
|
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
|
||||||
|
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
|
||||||
|
|
||||||
|
linkBlock = (
|
||||||
|
<div className="faux-button-block file-actions__download-status-bar button-set-item">
|
||||||
|
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
|
||||||
|
{labelWithIcon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={() => openInShell(fileInfo)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = metadata ? metadata.title : uri;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fileInfo !== null || fileInfo.isMine
|
||||||
|
? linkBlock
|
||||||
|
: null}
|
||||||
|
{ showMenu ?
|
||||||
|
<DropDownMenu>
|
||||||
|
<DropDownMenuItem key={0} onClick={() => openInFolder(fileInfo)} label={openInFolderMessage} />
|
||||||
|
<DropDownMenuItem key={1} onClick={() => openModal('confirmRemove')} label="Remove..." />
|
||||||
|
</DropDownMenu> : '' }
|
||||||
|
<Modal type="confirm" isOpen={modal == 'affirmPurchase'}
|
||||||
|
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase.bind(this)} onAborted={closeModal}>
|
||||||
|
Are you sure you'd like to buy <strong>{title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits?
|
||||||
|
</Modal>
|
||||||
|
<Modal isOpen={modal == 'notEnoughCredits'} contentLabel="Not enough credits"
|
||||||
|
onConfirmed={closeModal}>
|
||||||
|
You don't have enough LBRY credits to pay for this stream.
|
||||||
|
</Modal>
|
||||||
|
<Modal isOpen={modal == 'timedOut'} contentLabel="Download failed"
|
||||||
|
onConfirmed={closeModal}>
|
||||||
|
LBRY was unable to download the stream <strong>{uri}</strong>.
|
||||||
|
</Modal>
|
||||||
|
<Modal isOpen={modal == 'confirmRemove'}
|
||||||
|
contentLabel="Not enough credits"
|
||||||
|
type="confirm"
|
||||||
|
confirmButtonLabel="Remove"
|
||||||
|
onConfirmed={() => deleteFile(uri, fileInfo, deleteChecked)}
|
||||||
|
onAborted={closeModal}>
|
||||||
|
<p>Are you sure you'd like to remove <cite>{title}</cite> from LBRY?</p>
|
||||||
|
|
||||||
|
<label><FormField type="checkbox" checked={deleteChecked} onClick={this.handleDeleteCheckboxClicked.bind(this)} /> Delete this file from my computer</label>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const FileActionsRow = React.createClass({
|
||||||
|
// _isMounted: false,
|
||||||
|
// _fileInfoSubscribeId: null,
|
||||||
|
|
||||||
|
// propTypes: {
|
||||||
|
// uri: React.PropTypes.string,
|
||||||
|
// outpoint: React.PropTypes.string.isRequired,
|
||||||
|
// metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
|
||||||
|
// contentType: React.PropTypes.string.isRequired,
|
||||||
|
// },
|
||||||
|
// getInitialState: function() {
|
||||||
|
// return {
|
||||||
|
// fileInfo: null,
|
||||||
|
// modal: null,
|
||||||
|
// menuOpen: false,
|
||||||
|
// deleteChecked: false,
|
||||||
|
// attemptingDownload: false,
|
||||||
|
// attemptingRemove: false,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// onFileInfoUpdate: function(fileInfo) {
|
||||||
|
// if (this._isMounted) {
|
||||||
|
// this.setState({
|
||||||
|
// fileInfo: fileInfo ? fileInfo : false,
|
||||||
|
// attemptingDownload: fileInfo ? false : this.state.attemptingDownload
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// tryDownload: function() {
|
||||||
|
// this.setState({
|
||||||
|
// attemptingDownload: true,
|
||||||
|
// attemptingRemove: false
|
||||||
|
// });
|
||||||
|
// lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
||||||
|
// lbry.getBalance((balance) => {
|
||||||
|
// if (cost > balance) {
|
||||||
|
// this.setState({
|
||||||
|
// modal: 'notEnoughCredits',
|
||||||
|
// attemptingDownload: false,
|
||||||
|
// });
|
||||||
|
// } else if (this.state.affirmedPurchase) {
|
||||||
|
// lbry.get({uri: this.props.uri}).then((streamInfo) => {
|
||||||
|
// if (streamInfo === null || typeof streamInfo !== 'object') {
|
||||||
|
// this.setState({
|
||||||
|
// modal: 'timedOut',
|
||||||
|
// attemptingDownload: false,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// this.setState({
|
||||||
|
// attemptingDownload: false,
|
||||||
|
// modal: 'affirmPurchase'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// closeModal: function() {
|
||||||
|
// this.setState({
|
||||||
|
// modal: null,
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// onDownloadClick: function() {
|
||||||
|
// if (!this.state.fileInfo && !this.state.attemptingDownload) {
|
||||||
|
// this.tryDownload();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// onOpenClick: function() {
|
||||||
|
// if (this.state.fileInfo && this.state.fileInfo.download_path) {
|
||||||
|
// shell.openItem(this.state.fileInfo.download_path);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// handleDeleteCheckboxClicked: function(event) {
|
||||||
|
// this.setState({
|
||||||
|
// deleteChecked: event.target.checked,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// handleRevealClicked: function() {
|
||||||
|
// if (this.state.fileInfo && this.state.fileInfo.download_path) {
|
||||||
|
// shell.showItemInFolder(this.state.fileInfo.download_path);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// handleRemoveClicked: function() {
|
||||||
|
// this.setState({
|
||||||
|
// modal: 'confirmRemove',
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// handleRemoveConfirmed: function() {
|
||||||
|
// lbry.removeFile(this.props.outpoint, this.state.deleteChecked);
|
||||||
|
// this.setState({
|
||||||
|
// modal: null,
|
||||||
|
// fileInfo: false,
|
||||||
|
// attemptingDownload: false
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// onAffirmPurchase: function() {
|
||||||
|
// this.setState({
|
||||||
|
// affirmedPurchase: true,
|
||||||
|
// modal: null
|
||||||
|
// });
|
||||||
|
// this.tryDownload();
|
||||||
|
// },
|
||||||
|
// openMenu: function() {
|
||||||
|
// this.setState({
|
||||||
|
// menuOpen: !this.state.menuOpen,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// componentDidMount: function() {
|
||||||
|
// this._isMounted = true;
|
||||||
|
// this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
||||||
|
// },
|
||||||
|
// componentWillUnmount: function() {
|
||||||
|
// this._isMounted = false;
|
||||||
|
// if (this._fileInfoSubscribeId) {
|
||||||
|
// lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// render: function() {
|
||||||
|
// if (this.state.fileInfo === null)
|
||||||
|
// {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
|
||||||
|
// showMenu = !!this.state.fileInfo;
|
||||||
|
|
||||||
|
// let linkBlock;
|
||||||
|
// if (this.state.fileInfo === false && !this.state.attemptingDownload) {
|
||||||
|
// linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.onDownloadClick} />;
|
||||||
|
// } else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
|
||||||
|
// const
|
||||||
|
// progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
|
||||||
|
// label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
|
||||||
|
// labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
|
||||||
|
|
||||||
|
// linkBlock = (
|
||||||
|
// <div className="faux-button-block file-actions__download-status-bar button-set-item">
|
||||||
|
// <div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
|
||||||
|
// {labelWithIcon}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={this.onOpenClick} />;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const uri = lbryuri.normalize(this.props.uri);
|
||||||
|
// const title = this.props.metadata ? this.props.metadata.title : uri;
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// {this.state.fileInfo !== null || this.state.fileInfo.isMine
|
||||||
|
// ? linkBlock
|
||||||
|
// : null}
|
||||||
|
// { showMenu ?
|
||||||
|
// <DropDownMenu>
|
||||||
|
// <DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
|
||||||
|
// <DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
|
||||||
|
// </DropDownMenu> : '' }
|
||||||
|
// <Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
|
||||||
|
// contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
|
||||||
|
// Are you sure you'd like to buy <strong>{title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits?
|
||||||
|
// </Modal>
|
||||||
|
// <Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
|
||||||
|
// onConfirmed={this.closeModal}>
|
||||||
|
// You don't have enough LBRY credits to pay for this stream.
|
||||||
|
// </Modal>
|
||||||
|
// <Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
|
||||||
|
// onConfirmed={this.closeModal}>
|
||||||
|
// LBRY was unable to download the stream <strong>{uri}</strong>.
|
||||||
|
// </Modal>
|
||||||
|
// <Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
|
||||||
|
// type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
|
||||||
|
// onAborted={this.closeModal}>
|
||||||
|
// <p>Are you sure you'd like to remove <cite>{title}</cite> from LBRY?</p>
|
||||||
|
|
||||||
|
// <label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
|
||||||
|
// </Modal>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
class FileActions extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this._isMounted = false
|
||||||
|
this._fileInfoSubscribeId = null
|
||||||
|
this.state = {
|
||||||
|
available: true,
|
||||||
|
forceShowActions: false,
|
||||||
|
fileInfo: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowFileActionsRowClicked() {
|
||||||
|
this.setState({
|
||||||
|
forceShowActions: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fileInfo,
|
||||||
|
availability,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (fileInfo === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<section className="file-actions">
|
||||||
|
{
|
||||||
|
fileInfo || this.state.available || this.state.forceShowActions
|
||||||
|
? <FileActionsRow {...this.props} />
|
||||||
|
: <div>
|
||||||
|
<div className="button-set-item empty">Content unavailable.</div>
|
||||||
|
<ToolTip label="Why?"
|
||||||
|
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
|
||||||
|
className="button-set-item" />
|
||||||
|
<Link label="Try Anyway" onClick={this.onShowFileActionsRowClicked.bind(this)} className="button-text button-set-item" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileActions
|
44
ui/js/component/fileCardStream/index.js
Normal file
44
ui/js/component/fileCardStream/index.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectHidePrice,
|
||||||
|
selectObscureNsfw,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectSourceForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import FileCardStream from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaimForUri = makeSelectClaimForUri()
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectMetadataForUri = makeSelectMetadataForUri()
|
||||||
|
const selectSourceForUri = makeSelectSourceForUri()
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaimForUri(state, props),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
hidePrice: selectHidePrice(state),
|
||||||
|
obscureNsfw: selectObscureNsfw(state),
|
||||||
|
hasSignature: false,
|
||||||
|
metadata: selectMetadataForUri(state, props),
|
||||||
|
source: selectSourceForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileCardStream)
|
106
ui/js/component/fileCardStream/view.jsx
Normal file
106
ui/js/component/fileCardStream/view.jsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {Thumbnail, TruncatedText,} from 'component/common';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import UriIndicator from 'component/channel-indicator';
|
||||||
|
|
||||||
|
class FileCardStream extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this._fileInfoSubscribeId = null
|
||||||
|
this._isMounted = null
|
||||||
|
this._metadata = null
|
||||||
|
this.state = {
|
||||||
|
showNsfwHelp: false,
|
||||||
|
isHidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._isMounted = true;
|
||||||
|
if (this.props.hideOnRemove) {
|
||||||
|
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._fileInfoSubscribeId) {
|
||||||
|
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileInfoUpdate(fileInfo) {
|
||||||
|
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
|
||||||
|
this.setState({
|
||||||
|
isHidden: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOver() {
|
||||||
|
this.setState({
|
||||||
|
hovered: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOut() {
|
||||||
|
this.setState({
|
||||||
|
hovered: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.isHidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!this.props.metadata) {
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
|
||||||
|
const uri = lbryuri.normalize(this.props.uri);
|
||||||
|
const metadata = this.props.metadata;
|
||||||
|
const isConfirmed = !!metadata;
|
||||||
|
const title = isConfirmed ? metadata.title : uri;
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
||||||
|
const primaryUrl = 'show=' + uri;
|
||||||
|
return (
|
||||||
|
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
|
||||||
|
<div className="card__inner">
|
||||||
|
<a href="#" onClick={() => this.props.navigate(primaryUrl)} className="card__link">
|
||||||
|
<div className="card__title-identity">
|
||||||
|
<h5 title={title}><TruncatedText lines={1}>{title}</TruncatedText></h5>
|
||||||
|
<div className="card__subtitle">
|
||||||
|
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={lbryuri.normalize(this.props.uri)} /></span> : null}
|
||||||
|
<UriIndicator uri={uri} metadata={metadata} contentType={this.props.contentType}
|
||||||
|
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{metadata &&
|
||||||
|
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
|
||||||
|
}
|
||||||
|
<div className="card__content card__subtext card__subtext--two-lines">
|
||||||
|
<TruncatedText lines={2}>
|
||||||
|
{isConfirmed
|
||||||
|
? metadata.description
|
||||||
|
: <span className="empty">This file is pending confirmation.</span>}
|
||||||
|
</TruncatedText>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{this.state.showNsfwHelp && this.state.hovered
|
||||||
|
? <div className='card-overlay'>
|
||||||
|
<p>
|
||||||
|
This content is Not Safe For Work.
|
||||||
|
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileCardStream
|
13
ui/js/component/fileList/index.js
Normal file
13
ui/js/component/fileList/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import FileList from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileList)
|
184
ui/js/component/fileList/view.jsx
Normal file
184
ui/js/component/fileList/view.jsx
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {FormField} from 'component/form.js';
|
||||||
|
import FileTileStream from 'component/fileTileStream';
|
||||||
|
import rewards from 'rewards.js';
|
||||||
|
import lbryio from 'lbryio.js';
|
||||||
|
import {BusyMessage, Thumbnail} from 'component/common.js';
|
||||||
|
|
||||||
|
class FileList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
sortBy: 'date',
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sortFunctions = {
|
||||||
|
date: function(fileInfos) {
|
||||||
|
return fileInfos.slice().reverse();
|
||||||
|
},
|
||||||
|
title: function(fileInfos) {
|
||||||
|
return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
|
||||||
|
const title1 = fileInfo1.metadata ? fileInfo1.metadata.stream.metadata.title.toLowerCase() : fileInfo1.name;
|
||||||
|
const title2 = fileInfo2.metadata ? fileInfo2.metadata.stream.metadata.title.toLowerCase() : fileInfo2.name;
|
||||||
|
if (title1 < title2) {
|
||||||
|
return -1;
|
||||||
|
} else if (title1 > title2) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
filename: function(fileInfos) {
|
||||||
|
return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
|
||||||
|
const fileName1Lower = fileName1.toLowerCase();
|
||||||
|
const fileName2Lower = fileName2.toLowerCase();
|
||||||
|
if (fileName1Lower < fileName2Lower) {
|
||||||
|
return -1;
|
||||||
|
} else if (fileName2Lower > fileName1Lower) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChanged(event) {
|
||||||
|
this.setState({
|
||||||
|
sortBy: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
handleSortChanged,
|
||||||
|
fileInfos,
|
||||||
|
hidePrices,
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
sortBy,
|
||||||
|
} = this.state
|
||||||
|
const content = []
|
||||||
|
|
||||||
|
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
contentName: fileInfo.name,
|
||||||
|
channelName: fileInfo.channel_name,
|
||||||
|
})
|
||||||
|
content.push(<FileTileStream key={uri} uri={uri} hidePrice={hidePrices} />)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<span className='sort-section'>
|
||||||
|
Sort by { ' ' }
|
||||||
|
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="title">Title</option>
|
||||||
|
<option value="filename">File name</option>
|
||||||
|
</FormField>
|
||||||
|
</span>
|
||||||
|
{content}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const FileList = React.createClass({
|
||||||
|
// _sortFunctions: {
|
||||||
|
// date: function(fileInfos) {
|
||||||
|
// return fileInfos.slice().reverse();
|
||||||
|
// },
|
||||||
|
// title: function(fileInfos) {
|
||||||
|
// return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
|
||||||
|
// const title1 = fileInfo1.metadata ? fileInfo1.metadata.title.toLowerCase() : fileInfo1.name;
|
||||||
|
// const title2 = fileInfo2.metadata ? fileInfo2.metadata.title.toLowerCase() : fileInfo2.name;
|
||||||
|
// if (title1 < title2) {
|
||||||
|
// return -1;
|
||||||
|
// } else if (title1 > title2) {
|
||||||
|
// return 1;
|
||||||
|
// } else {
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// filename: function(fileInfos) {
|
||||||
|
// return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
|
||||||
|
// const fileName1Lower = fileName1.toLowerCase();
|
||||||
|
// const fileName2Lower = fileName2.toLowerCase();
|
||||||
|
// if (fileName1Lower < fileName2Lower) {
|
||||||
|
// return -1;
|
||||||
|
// } else if (fileName2Lower > fileName1Lower) {
|
||||||
|
// return 1;
|
||||||
|
// } else {
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// propTypes: {
|
||||||
|
// fileInfos: React.PropTypes.array.isRequired,
|
||||||
|
// hidePrices: React.PropTypes.bool,
|
||||||
|
// },
|
||||||
|
// getDefaultProps: function() {
|
||||||
|
// return {
|
||||||
|
// hidePrices: false,
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// getInitialState: function() {
|
||||||
|
// return {
|
||||||
|
// sortBy: 'date',
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// handleSortChanged: function(event) {
|
||||||
|
// this.setState({
|
||||||
|
// sortBy: event.target.value,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// render: function() {
|
||||||
|
// var content = [],
|
||||||
|
// seenUris = {};
|
||||||
|
|
||||||
|
// console.debug(this.props)
|
||||||
|
|
||||||
|
// const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
|
||||||
|
// for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
|
||||||
|
// if (seenUris[name] || !claim_id) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let streamMetadata;
|
||||||
|
// if (metadata) {
|
||||||
|
// streamMetadata = metadata.stream.metadata;
|
||||||
|
// } else {
|
||||||
|
// streamMetadata = null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// const uri = lbryuri.build({contentName: name, channelName: channel_name});
|
||||||
|
// seenUris[name] = true;
|
||||||
|
// content.push(<FileTileStream key={outpoint} uri={uri} hideOnRemove={true} />)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <section>
|
||||||
|
// <span className='sort-section'>
|
||||||
|
// Sort by { ' ' }
|
||||||
|
// <FormField type="select" onChange={this.handleSortChanged}>
|
||||||
|
// <option value="date">Date</option>
|
||||||
|
// <option value="title">Title</option>
|
||||||
|
// <option value="filename">File name</option>
|
||||||
|
// </FormField>
|
||||||
|
// </span>
|
||||||
|
// {content}
|
||||||
|
// </section>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
export default FileList
|
22
ui/js/component/filePrice/index.js
Normal file
22
ui/js/component/filePrice/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
makeSelectCostInfoForUri,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import FilePrice from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectCostInfoForUri = makeSelectCostInfoForUri()
|
||||||
|
const select = (state, props) => ({
|
||||||
|
costInfo: selectCostInfoForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FilePrice)
|
21
ui/js/component/filePrice/view.jsx
Normal file
21
ui/js/component/filePrice/view.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
CreditAmount,
|
||||||
|
} from 'component/common'
|
||||||
|
|
||||||
|
const FilePrice = (props) => {
|
||||||
|
const {
|
||||||
|
costInfo,
|
||||||
|
look = 'indicator',
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const isEstimate = costInfo ? !costInfo.includesData : null
|
||||||
|
|
||||||
|
if (!costInfo) {
|
||||||
|
return <span className={`credit-amount credit-amount--${look}`}>???</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CreditAmount label={false} amount={costInfo.cost} isEstimate={isEstimate} showFree={true} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilePrice
|
27
ui/js/component/fileTile/index.js
Normal file
27
ui/js/component/fileTile/index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import FileTile from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaimForUri = makeSelectClaimForUri()
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaimForUri(state, props),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileTile)
|
31
ui/js/component/fileTile/view.jsx
Normal file
31
ui/js/component/fileTile/view.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import FileCardStream from 'component/fileCardStream'
|
||||||
|
import FileTileStream from 'component/fileTileStream'
|
||||||
|
import FileActions from 'component/fileActions';
|
||||||
|
|
||||||
|
class FileTile extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
displayStyle,
|
||||||
|
uri,
|
||||||
|
claim,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if(!claim) {
|
||||||
|
if (displayStyle == 'card') {
|
||||||
|
return <FileCardStream uri={uri} />
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayStyle == 'card' ?
|
||||||
|
<FileCardStream uri={uri} />
|
||||||
|
:
|
||||||
|
<FileTileStream uri={uri} key={uri} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileTile
|
50
ui/js/component/fileTileStream/index.js
Normal file
50
ui/js/component/fileTileStream/index.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectSourceForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
makeSelectFetchingAvailabilityForUri,
|
||||||
|
makeSelectAvailabilityForUri,
|
||||||
|
} from 'selectors/availability'
|
||||||
|
import {
|
||||||
|
selectObscureNsfw,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import FileTileStream from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaimForUri = makeSelectClaimForUri()
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectFetchingAvailabilityForUri = makeSelectFetchingAvailabilityForUri()
|
||||||
|
const selectAvailabilityForUri = makeSelectAvailabilityForUri()
|
||||||
|
const selectMetadataForUri = makeSelectMetadataForUri()
|
||||||
|
const selectSourceForUri = makeSelectSourceForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaimForUri(state, props),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
fetchingAvailability: selectFetchingAvailabilityForUri(state, props),
|
||||||
|
selectAvailabilityForUri: selectAvailabilityForUri(state, props),
|
||||||
|
obscureNsfw: selectObscureNsfw(state),
|
||||||
|
metadata: selectMetadataForUri(state, props),
|
||||||
|
source: selectSourceForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileTileStream)
|
126
ui/js/component/fileTileStream/view.jsx
Normal file
126
ui/js/component/fileTileStream/view.jsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import FileActions from 'component/fileActions';
|
||||||
|
import {Thumbnail, TruncatedText,} from 'component/common.js';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import UriIndicator from 'component/channel-indicator.js';
|
||||||
|
|
||||||
|
/*should be merged into FileTile once FileTile is refactored to take a single id*/
|
||||||
|
class FileTileStream extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this._fileInfoSubscribeId = null
|
||||||
|
this._isMounted = null
|
||||||
|
this.state = {
|
||||||
|
showNsfwHelp: false,
|
||||||
|
isHidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._isMounted = true;
|
||||||
|
if (this.props.hideOnRemove) {
|
||||||
|
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._fileInfoSubscribeId) {
|
||||||
|
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileInfoUpdate(fileInfo) {
|
||||||
|
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
|
||||||
|
this.setState({
|
||||||
|
isHidden: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOver() {
|
||||||
|
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
|
||||||
|
this.setState({
|
||||||
|
showNsfwHelp: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOut() {
|
||||||
|
if (this.state.showNsfwHelp) {
|
||||||
|
this.setState({
|
||||||
|
showNsfwHelp: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.isHidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
metadata,
|
||||||
|
navigate,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const uri = lbryuri.normalize(this.props.uri);
|
||||||
|
const isConfirmed = !!metadata;
|
||||||
|
const title = isConfirmed ? metadata.title : uri;
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
|
||||||
|
<div className={"row-fluid card__inner file-tile__row"}>
|
||||||
|
<div className="span3 file-tile__thumbnail-container">
|
||||||
|
<a href="#" onClick={() => navigate(`show=${uri}`)}>
|
||||||
|
{metadata && metadata.thumbnail ?
|
||||||
|
<Thumbnail key={this.props.uri} className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + this.props.uri} />
|
||||||
|
:
|
||||||
|
<Thumbnail className="file-tile__thumbnail" alt={'Photo for ' + this.props.uri} />
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="span9">
|
||||||
|
<div className="card__title-primary">
|
||||||
|
{ !this.props.hidePrice
|
||||||
|
? <FilePrice uri={this.props.uri} />
|
||||||
|
: null}
|
||||||
|
<div className="meta"><a href="#" onClick={() => navigate(`show=${uri}`)}>{uri}</a></div>
|
||||||
|
<h3>
|
||||||
|
<a href="#" onClick={() => navigate(`show=${uri}`)} title={title}>
|
||||||
|
<TruncatedText lines={1}>
|
||||||
|
{title}
|
||||||
|
</TruncatedText>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__actions">
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<p className="file-tile__description">
|
||||||
|
<TruncatedText lines={2}>
|
||||||
|
{isConfirmed
|
||||||
|
? metadata.description
|
||||||
|
: <span className="empty">This file is pending confirmation.</span>}
|
||||||
|
</TruncatedText>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.state.showNsfwHelp
|
||||||
|
? <div className='card-overlay'>
|
||||||
|
<p>
|
||||||
|
This content is Not Safe For Work.
|
||||||
|
To view adult content, please change your <Link className="button-text" href="#" onClick={() => navigate('settings')} label="Settings" />.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileTileStream
|
33
ui/js/component/header/index.js
Normal file
33
ui/js/component/header/index.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
selectHeaderLinks,
|
||||||
|
selectPageTitle,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doSearchContent,
|
||||||
|
doActivateSearch,
|
||||||
|
doDeactivateSearch,
|
||||||
|
} from 'actions/search'
|
||||||
|
import Header from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
currentPage: selectCurrentPage(state),
|
||||||
|
subLinks: selectHeaderLinks(state),
|
||||||
|
pageTitle: selectPageTitle(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
search: (query) => dispatch(doSearchContent(query)),
|
||||||
|
activateSearch: () => dispatch(doActivateSearch()),
|
||||||
|
deactivateSearch: () => setTimeout(() => { dispatch(doDeactivateSearch()) }, 50),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(Header)
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Link} from './link.js';
|
import {Icon} from 'component/common.js';
|
||||||
import {Icon} from './common.js';
|
import Link from 'component/link';
|
||||||
|
|
||||||
var Header = React.createClass({
|
var Header = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -42,7 +42,7 @@ var Header = React.createClass({
|
||||||
//@TODO: Switch to React.js timing
|
//@TODO: Switch to React.js timing
|
||||||
var searchTerm = event.target.value;
|
var searchTerm = event.target.value;
|
||||||
this.userTypingTimer = setTimeout(() => {
|
this.userTypingTimer = setTimeout(() => {
|
||||||
this.props.onSearch(searchTerm);
|
this.props.search(searchTerm);
|
||||||
}, 800); // 800ms delay, tweak for faster/slower
|
}, 800); // 800ms delay, tweak for faster/slower
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -51,16 +51,18 @@ var Header = React.createClass({
|
||||||
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }>
|
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }>
|
||||||
<div className="header-top-bar">
|
<div className="header-top-bar">
|
||||||
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
|
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
|
||||||
<h1>{ this.state.title }</h1>
|
<h1>{ this.props.pageTitle }</h1>
|
||||||
<div className="header-search">
|
<div className="header-search">
|
||||||
<Icon icon="icon-search" />
|
<Icon icon="icon-search" />
|
||||||
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
|
<input type="search" onChange={this.onQueryChange}
|
||||||
|
onFocus={() => this.props.activateSearch()}
|
||||||
|
onBlur={() => this.props.deactivateSearch()}
|
||||||
placeholder="Find movies, music, games, and more" />
|
placeholder="Find movies, music, games, and more" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
this.props.links ?
|
this.props.links ?
|
||||||
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
|
<SubHeader {...this.props} /> :
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
@ -68,24 +70,28 @@ var Header = React.createClass({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var SubHeader = React.createClass({
|
const SubHeader = (props) => {
|
||||||
render: function() {
|
const {
|
||||||
var links = [],
|
subLinks,
|
||||||
viewingUrl = '?' + this.props.viewingPage;
|
currentPage,
|
||||||
|
navigate,
|
||||||
|
} = props
|
||||||
|
|
||||||
for (let link of Object.keys(this.props.links)) {
|
const links = []
|
||||||
|
|
||||||
|
for(let link of Object.keys(subLinks)) {
|
||||||
links.push(
|
links.push(
|
||||||
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>
|
<a href="#" onClick={() => navigate(link)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
|
||||||
{this.props.links[link]}
|
{subLinks[link]}
|
||||||
</a>
|
</a>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="sub-header">
|
<nav className="sub-header">
|
||||||
{links}
|
{links}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
7
ui/js/component/link/index.jsx
Normal file
7
ui/js/component/link/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import Link from './view'
|
||||||
|
|
||||||
|
export default connect(null, null)(Link)
|
99
ui/js/component/link/view.js
Normal file
99
ui/js/component/link/view.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
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}
|
||||||
|
{<span className="link-label">{label}</span>}
|
||||||
|
{ badge ? <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>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
|
@ -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: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -3,59 +3,6 @@ import {Icon} from './common.js';
|
||||||
import Modal from '../component/modal.js';
|
import Modal from '../component/modal.js';
|
||||||
import rewards from '../rewards.js';
|
import rewards from '../rewards.js';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let RewardLink = React.createClass({
|
export let RewardLink = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
type: React.PropTypes.string.isRequired,
|
type: React.PropTypes.string.isRequired,
|
15
ui/js/component/router/index.jsx
Normal file
15
ui/js/component/router/index.jsx
Normal 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);
|
46
ui/js/component/router/view.jsx
Normal file
46
ui/js/component/router/view.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SettingsPage from 'page/settings.js';
|
||||||
|
import HelpPage from 'page/help';
|
||||||
|
import ReportPage from 'page/report.js';
|
||||||
|
import StartPage from 'page/start.js';
|
||||||
|
import WalletPage from 'page/wallet';
|
||||||
|
import ShowPage from 'page/showPage';
|
||||||
|
import PublishPage from 'page/publish.js';
|
||||||
|
import DiscoverPage from 'page/discover';
|
||||||
|
import SplashScreen from 'component/splash.js';
|
||||||
|
import RewardsPage from 'page/rewards.js';
|
||||||
|
import DeveloperPage from 'page/developer.js';
|
||||||
|
import FileListDownloaded from 'page/fileListDownloaded'
|
||||||
|
import FileListPublished from 'page/fileListPublished'
|
||||||
|
|
||||||
|
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} />,
|
||||||
|
'report': <ReportPage {...props} />,
|
||||||
|
'downloaded': <FileListDownloaded {...props} />,
|
||||||
|
'published': <FileListPublished {...props} />,
|
||||||
|
'rewards' : <RewardsPage {...props} />,
|
||||||
|
'start': <StartPage {...props} />,
|
||||||
|
'wallet': <WalletPage {...props} />,
|
||||||
|
'send': <WalletPage {...props} />,
|
||||||
|
'receive': <WalletPage {...props} />,
|
||||||
|
'show': <ShowPage {...props} />,
|
||||||
|
'publish': <PublishPage {...props} />,
|
||||||
|
'developer': <DeveloperPage {...props} />,
|
||||||
|
'discover': <DiscoverPage {...props} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Router
|
19
ui/js/component/upgradeModal/index.jsx
Normal file
19
ui/js/component/upgradeModal/index.jsx
Normal 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)
|
32
ui/js/component/upgradeModal/view.jsx
Normal file
32
ui/js/component/upgradeModal/view.jsx
Normal 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
|
2
ui/js/config/development.js
Normal file
2
ui/js/config/development.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports = {
|
||||||
|
}
|
66
ui/js/constants/action_types.js
Normal file
66
ui/js/constants/action_types.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
export const NAVIGATE = 'NAVIGATE'
|
||||||
|
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 DAEMON_READY = 'DAEMON_READY'
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
// Wallet
|
||||||
|
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'
|
||||||
|
export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED'
|
||||||
|
export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED'
|
||||||
|
export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'
|
||||||
|
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
|
||||||
|
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'
|
||||||
|
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'
|
||||||
|
export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'
|
||||||
|
export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'
|
||||||
|
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'
|
||||||
|
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'
|
||||||
|
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'
|
||||||
|
|
||||||
|
// Content
|
||||||
|
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'
|
||||||
|
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'
|
||||||
|
export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED'
|
||||||
|
export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED'
|
||||||
|
export const FETCH_DOWNLOADED_CONTENT_STARTED = 'FETCH_DOWNLOADED_CONTENT_STARTED'
|
||||||
|
export const FETCH_DOWNLOADED_CONTENT_COMPLETED = 'FETCH_DOWNLOADED_CONTENT_COMPLETED'
|
||||||
|
export const FETCH_PUBLISHED_CONTENT_STARTED = 'FETCH_PUBLISHED_CONTENT_STARTED'
|
||||||
|
export const FETCH_PUBLISHED_CONTENT_COMPLETED = 'FETCH_PUBLISHED_CONTENT_COMPLETED'
|
||||||
|
export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED'
|
||||||
|
export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED'
|
||||||
|
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'
|
||||||
|
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'
|
||||||
|
export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED'
|
||||||
|
export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED'
|
||||||
|
export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED'
|
||||||
|
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED'
|
||||||
|
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED'
|
||||||
|
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED'
|
||||||
|
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED'
|
||||||
|
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'
|
||||||
|
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'
|
||||||
|
export const DELETE_FILE_STARTED = 'DELETE_FILE_STARTED'
|
||||||
|
export const DELETE_FILE_COMPLETED = 'DELETE_FILE_COMPLETED'
|
||||||
|
export const FETCH_MY_CLAIMS_COMPLETED = 'FETCH_MY_CLAIMS_COMPLETED'
|
||||||
|
|
||||||
|
// Search
|
||||||
|
export const SEARCH_STARTED = 'SEARCH_STARTED'
|
||||||
|
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'
|
||||||
|
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'
|
||||||
|
export const ACTIVATE_SEARCH = 'ACTIVATE_SEARCH'
|
||||||
|
export const DEACTIVATE_SEARCH = 'DEACTIVATE_SEARCH'
|
|
@ -287,7 +287,7 @@ lbry.getMyClaims = function(callback) {
|
||||||
|
|
||||||
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
|
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
|
||||||
this._removedFiles.push(outpoint);
|
this._removedFiles.push(outpoint);
|
||||||
this._updateFileInfoSubscribers(outpoint);
|
// this._updateFileInfoSubscribers(outpoint);
|
||||||
|
|
||||||
lbry.file_delete({
|
lbry.file_delete({
|
||||||
outpoint: outpoint,
|
outpoint: outpoint,
|
||||||
|
|
|
@ -5,6 +5,8 @@ const queryTimeout = 3000;
|
||||||
const maxQueryTries = 2;
|
const maxQueryTries = 2;
|
||||||
const defaultServers = [
|
const defaultServers = [
|
||||||
'http://lighthouse7.lbry.io:50005',
|
'http://lighthouse7.lbry.io:50005',
|
||||||
|
'http://lighthouse8.lbry.io:50005',
|
||||||
|
'http://lighthouse9.lbry.io:50005',
|
||||||
];
|
];
|
||||||
const path = '/';
|
const path = '/';
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,20 @@ 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';
|
||||||
|
import { runTriggers } from 'triggers'
|
||||||
|
import {
|
||||||
|
doDaemonReady
|
||||||
|
} from 'actions/app'
|
||||||
|
|
||||||
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 +26,11 @@ window.addEventListener('contextmenu', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
let init = function() {
|
const initialState = app.store.getState();
|
||||||
|
app.store.subscribe(runTriggers);
|
||||||
|
runTriggers();
|
||||||
|
|
||||||
|
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');
|
||||||
|
@ -29,8 +40,8 @@ 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
|
app.store.dispatch(doDaemonReady())
|
||||||
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') {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import lbryio from '../lbryio.js';
|
|
||||||
import lbryuri from '../lbryuri.js';
|
|
||||||
import lighthouse from '../lighthouse.js';
|
|
||||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
|
||||||
import {Link} from '../component/link.js';
|
|
||||||
import {ToolTip} from '../component/tooltip.js';
|
|
||||||
import {BusyMessage} from '../component/common.js';
|
|
||||||
|
|
||||||
var fetchResultsStyle = {
|
|
||||||
color: '#888',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '1.2em'
|
|
||||||
};
|
|
||||||
|
|
||||||
var SearchActive = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<div style={fetchResultsStyle}>
|
|
||||||
<BusyMessage message="Looking up the Dewey Decimals" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var searchNoResultsStyle = {
|
|
||||||
textAlign: 'center'
|
|
||||||
}, searchNoResultsMessageStyle = {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
marginRight: '5px'
|
|
||||||
};
|
|
||||||
|
|
||||||
var SearchNoResults = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<section style={searchNoResultsStyle}>
|
|
||||||
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {this.props.query} yet.</span>
|
|
||||||
<Link label="Be the first" href="?publish" />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var SearchResults = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
var rows = [],
|
|
||||||
seenNames = {}; //fix this when the search API returns claim IDs
|
|
||||||
|
|
||||||
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
|
|
||||||
const uri = lbryuri.build({
|
|
||||||
channelName: channel_name,
|
|
||||||
contentName: name,
|
|
||||||
claimId: channel_id || claim_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.push(
|
|
||||||
<FileTileStream key={name} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>{rows}</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
|
|
||||||
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
|
|
||||||
'"five" to put your content here!');
|
|
||||||
|
|
||||||
var FeaturedCategory = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
return (<div className="card-row card-row--small">
|
|
||||||
{ this.props.category ?
|
|
||||||
<h3 className="card-row__header">{this.props.category}
|
|
||||||
{ this.props.category == "community" ?
|
|
||||||
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
|
|
||||||
: '' }</h3>
|
|
||||||
: '' }
|
|
||||||
{ this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" uri={name} /> }) }
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var FeaturedContent = React.createClass({
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
featuredUris: {},
|
|
||||||
failed: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
|
|
||||||
let featuredUris = {}
|
|
||||||
Categories.forEach((category) => {
|
|
||||||
if (Uris[category] && Uris[category].length) {
|
|
||||||
featuredUris[category] = Uris[category]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.setState({ featuredUris: featuredUris });
|
|
||||||
}, () => {
|
|
||||||
this.setState({
|
|
||||||
failed: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
this.state.failed ?
|
|
||||||
<div className="empty">Failed to load landing content.</div> :
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
Object.keys(this.state.featuredUris).map(function(category) {
|
|
||||||
return this.state.featuredUris[category].length ?
|
|
||||||
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
|
|
||||||
'';
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var DiscoverPage = React.createClass({
|
|
||||||
userTypingTimer: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
showWelcome: React.PropTypes.bool.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
|
||||||
if (this.props.query != this.state.query)
|
|
||||||
{
|
|
||||||
this.handleSearchChanged(this.props.query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
showWelcome: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps, nextState) {
|
|
||||||
if (nextProps.query != nextState.query)
|
|
||||||
{
|
|
||||||
this.handleSearchChanged(nextProps.query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSearchChanged: function(query) {
|
|
||||||
this.setState({
|
|
||||||
searching: true,
|
|
||||||
query: query,
|
|
||||||
});
|
|
||||||
|
|
||||||
lighthouse.search(query).then(this.searchCallback);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleWelcomeDone: function() {
|
|
||||||
this.setState({
|
|
||||||
welcomeComplete: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
document.title = "Discover";
|
|
||||||
|
|
||||||
if (this.props.query) {
|
|
||||||
// Rendering with a query already typed
|
|
||||||
this.handleSearchChanged(this.props.query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
welcomeComplete: false,
|
|
||||||
results: [],
|
|
||||||
query: this.props.query,
|
|
||||||
searching: ('query' in this.props) && (this.props.query.length > 0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
searchCallback: function(results) {
|
|
||||||
if (this.state.searching) //could have canceled while results were pending, in which case nothing to do
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
results: results,
|
|
||||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
{ this.state.searching ? <SearchActive /> : null }
|
|
||||||
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null }
|
|
||||||
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null }
|
|
||||||
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DiscoverPage;
|
|
30
ui/js/page/discover/index.js
Normal file
30
ui/js/page/discover/index.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectFeaturedContentByCategory
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
doSearchContent,
|
||||||
|
} from 'actions/search'
|
||||||
|
import {
|
||||||
|
selectIsSearching,
|
||||||
|
selectSearchQuery,
|
||||||
|
selectCurrentSearchResults,
|
||||||
|
selectSearchActivated,
|
||||||
|
} from 'selectors/search'
|
||||||
|
import DiscoverPage from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
featuredContentByCategory: selectFeaturedContentByCategory(state),
|
||||||
|
isSearching: selectIsSearching(state),
|
||||||
|
query: selectSearchQuery(state),
|
||||||
|
results: selectCurrentSearchResults(state),
|
||||||
|
searchActive: selectSearchActivated(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(DiscoverPage)
|
126
ui/js/page/discover/view.jsx
Normal file
126
ui/js/page/discover/view.jsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryio from 'lbryio.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import lighthouse from 'lighthouse.js';
|
||||||
|
import FileTile from 'component/fileTile';
|
||||||
|
import FileTileStream from 'component/fileTileStream'
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {ToolTip} from 'component/tooltip.js';
|
||||||
|
import {BusyMessage} from 'component/common.js';
|
||||||
|
|
||||||
|
const fetchResultsStyle = {
|
||||||
|
color: '#888',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '1.2em'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchActive = (props) => {
|
||||||
|
return (
|
||||||
|
<div style={fetchResultsStyle}>
|
||||||
|
<BusyMessage message="Looking up the Dewey Decimals" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchNoResultsStyle = {
|
||||||
|
textAlign: 'center'
|
||||||
|
}, searchNoResultsMessageStyle = {
|
||||||
|
fontStyle: 'italic',
|
||||||
|
marginRight: '5px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchNoResults = (props) => {
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={searchNoResultsStyle}>
|
||||||
|
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {query} yet.</span>
|
||||||
|
<Link label="Be the first" href="?publish" />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchResults = (props) => {
|
||||||
|
const {
|
||||||
|
results
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const rows = [],
|
||||||
|
seenNames = {}; //fix this when the search API returns claim IDs
|
||||||
|
|
||||||
|
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of results) {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
channelName: channel_name,
|
||||||
|
contentName: name,
|
||||||
|
claimId: channel_id || claim_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<FileTileStream key={uri} uri={uri} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>{rows}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
|
||||||
|
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
|
||||||
|
'"five" to put your content here!');
|
||||||
|
|
||||||
|
const FeaturedCategory = (props) => {
|
||||||
|
const {
|
||||||
|
category,
|
||||||
|
names,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-row card-row--small">
|
||||||
|
<h3 className="card-row__header">{category}
|
||||||
|
{category &&
|
||||||
|
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header" />}
|
||||||
|
</h3>
|
||||||
|
{names && names.map(name => <FileTile key={name} displayStyle="card" uri={name} />)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeaturedContent = (props) => {
|
||||||
|
const {
|
||||||
|
featuredContentByCategory,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const categories = Object.keys(featuredContentByCategory)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{categories.map(category =>
|
||||||
|
<FeaturedCategory key={category} category={category} names={featuredContentByCategory[category]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DiscoverPage = (props) => {
|
||||||
|
const {
|
||||||
|
isSearching,
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
searchActive,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
{ (!searchActive || (!isSearching && !query)) && <FeaturedContent {...props} /> }
|
||||||
|
{ searchActive && isSearching ? <SearchActive /> : null }
|
||||||
|
{ searchActive && !isSearching && query && results.length ? <SearchResults results={results} /> : null }
|
||||||
|
{ searchActive && !isSearching && query && !results.length ? <SearchNoResults query={query} /> : null }
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiscoverPage;
|
|
@ -1,220 +0,0 @@
|
||||||
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 {FileTileStream} from '../component/file-tile.js';
|
|
||||||
import rewards from '../rewards.js';
|
|
||||||
import lbryio from '../lbryio.js';
|
|
||||||
import {BusyMessage, Thumbnail} from '../component/common.js';
|
|
||||||
|
|
||||||
|
|
||||||
export let FileListDownloaded = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
fileInfos: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
document.title = "Downloaded Files";
|
|
||||||
|
|
||||||
lbry.claim_list_mine().then((myClaimInfos) => {
|
|
||||||
if (!this._isMounted) { return; }
|
|
||||||
|
|
||||||
lbry.file_list().then((fileInfos) => {
|
|
||||||
if (!this._isMounted) { return; }
|
|
||||||
|
|
||||||
const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
|
|
||||||
this.setState({
|
|
||||||
fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (this.state.fileInfos === null) {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<BusyMessage message="Loading" />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
} else if (!this.state.fileInfos.length) {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<FileList fileInfos={this.state.fileInfos} hidePrices={true} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let FileListPublished = React.createClass({
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
fileInfos: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
_requestPublishReward: function() {
|
|
||||||
lbryio.call('reward', 'list', {}).then(function(userRewards) {
|
|
||||||
//already rewarded
|
|
||||||
if (userRewards.filter(function (reward) {
|
|
||||||
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
|
|
||||||
}).length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
this._isMounted = true;
|
|
||||||
this._requestPublishReward();
|
|
||||||
document.title = "Published Files";
|
|
||||||
|
|
||||||
lbry.claim_list_mine().then((claimInfos) => {
|
|
||||||
if (!this._isMounted) { return; }
|
|
||||||
|
|
||||||
lbry.file_list().then((fileInfos) => {
|
|
||||||
if (!this._isMounted) { return; }
|
|
||||||
|
|
||||||
const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout);
|
|
||||||
this.setState({
|
|
||||||
fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
if (this.state.fileInfos === null) {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<BusyMessage message="Loading" />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (!this.state.fileInfos.length) {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<FileList fileInfos={this.state.fileInfos} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export let FileList = React.createClass({
|
|
||||||
_sortFunctions: {
|
|
||||||
date: function(fileInfos) {
|
|
||||||
return fileInfos.slice().reverse();
|
|
||||||
},
|
|
||||||
title: function(fileInfos) {
|
|
||||||
return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
|
|
||||||
const title1 = fileInfo1.metadata ? fileInfo1.metadata.title.toLowerCase() : fileInfo1.name;
|
|
||||||
const title2 = fileInfo2.metadata ? fileInfo2.metadata.title.toLowerCase() : fileInfo2.name;
|
|
||||||
if (title1 < title2) {
|
|
||||||
return -1;
|
|
||||||
} else if (title1 > title2) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
filename: function(fileInfos) {
|
|
||||||
return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
|
|
||||||
const fileName1Lower = fileName1.toLowerCase();
|
|
||||||
const fileName2Lower = fileName2.toLowerCase();
|
|
||||||
if (fileName1Lower < fileName2Lower) {
|
|
||||||
return -1;
|
|
||||||
} else if (fileName2Lower > fileName1Lower) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propTypes: {
|
|
||||||
fileInfos: React.PropTypes.array.isRequired,
|
|
||||||
hidePrices: React.PropTypes.bool,
|
|
||||||
},
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
hidePrices: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
sortBy: 'date',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
handleSortChanged: function(event) {
|
|
||||||
this.setState({
|
|
||||||
sortBy: event.target.value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
var content = [],
|
|
||||||
seenUris = {};
|
|
||||||
|
|
||||||
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
|
|
||||||
for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
|
|
||||||
if (seenUris[name] || !claim_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let streamMetadata;
|
|
||||||
if (metadata) {
|
|
||||||
streamMetadata = metadata.stream.metadata;
|
|
||||||
} else {
|
|
||||||
streamMetadata = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const uri = lbryuri.build({contentName: name, channelName: channel_name});
|
|
||||||
seenUris[name] = true;
|
|
||||||
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true}
|
|
||||||
hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
|
|
||||||
hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<span className='sort-section'>
|
|
||||||
Sort by { ' ' }
|
|
||||||
<FormField type="select" onChange={this.handleSortChanged}>
|
|
||||||
<option value="date">Date</option>
|
|
||||||
<option value="title">Title</option>
|
|
||||||
<option value="filename">File name</option>
|
|
||||||
</FormField>
|
|
||||||
</span>
|
|
||||||
{content}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
25
ui/js/page/fileListDownloaded/index.js
Normal file
25
ui/js/page/fileListDownloaded/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectFetchingDownloadedContent,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectDownloadedFileInfo,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import FileListDownloaded from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
downloadedContent: selectDownloadedFileInfo(state),
|
||||||
|
fetching: selectFetchingDownloadedContent(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileListDownloaded)
|
42
ui/js/page/fileListDownloaded/view.jsx
Normal file
42
ui/js/page/fileListDownloaded/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {FormField} from 'component/form.js';
|
||||||
|
import {FileTileStream} from 'component/fileTile';
|
||||||
|
import rewards from 'rewards.js';
|
||||||
|
import lbryio from 'lbryio.js';
|
||||||
|
import {BusyMessage, Thumbnail} from 'component/common.js';
|
||||||
|
import FileList from 'component/fileList'
|
||||||
|
|
||||||
|
class FileListDownloaded extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
downloadedContent,
|
||||||
|
fetching,
|
||||||
|
navigate,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<BusyMessage message="Loading" />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
} else if (!downloadedContent.length) {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<span>You haven't downloaded anything from LBRY yet. Go <Link href="#" onClick={() => navigate('discover')} label="search for your first download" />!</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<FileList fileInfos={downloadedContent} hidePrices={true} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileListDownloaded
|
25
ui/js/page/fileListPublished/index.js
Normal file
25
ui/js/page/fileListPublished/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectFetchingPublishedContent,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectPublishedFileInfo,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import FileListPublished from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
publishedContent: selectPublishedFileInfo(state),
|
||||||
|
fetching: selectFetchingPublishedContent(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileListPublished)
|
126
ui/js/page/fileListPublished/view.jsx
Normal file
126
ui/js/page/fileListPublished/view.jsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {FormField} from 'component/form.js';
|
||||||
|
import {FileTileStream} from 'component/fileTile';
|
||||||
|
import rewards from 'rewards.js';
|
||||||
|
import lbryio from 'lbryio.js';
|
||||||
|
import {BusyMessage, Thumbnail} from 'component/common.js';
|
||||||
|
import FileList from 'component/fileList'
|
||||||
|
|
||||||
|
class FileListPublished extends React.Component {
|
||||||
|
componentDidUpdate() {
|
||||||
|
if(this.props.publishedContent.length > 0) this._requestPublishReward()
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestPublishReward() {
|
||||||
|
lbryio.call('reward', 'list', {}).then(function(userRewards) {
|
||||||
|
//already rewarded
|
||||||
|
if (userRewards.filter(function (reward) {
|
||||||
|
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID
|
||||||
|
}).length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
publishedContent,
|
||||||
|
fetching,
|
||||||
|
navigate,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<BusyMessage message="Loading" />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
} else if (!publishedContent.length) {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<span>You haven't downloaded anything from LBRY yet. Go <Link href="#" onClick={() => navigate('discover')} label="search for your first download" />!</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<FileList fileInfos={publishedContent} hidePrices={true} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const FileListPublished = React.createClass({
|
||||||
|
// _isMounted: false,
|
||||||
|
|
||||||
|
// getInitialState: function () {
|
||||||
|
// return {
|
||||||
|
// fileInfos: null,
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// _requestPublishReward: function() {
|
||||||
|
// lbryio.call('reward', 'list', {}).then(function(userRewards) {
|
||||||
|
// //already rewarded
|
||||||
|
// if (userRewards.filter(function (reward) {
|
||||||
|
// return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
|
||||||
|
// }).length) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// componentDidMount: function () {
|
||||||
|
// this._isMounted = true;
|
||||||
|
// this._requestPublishReward();
|
||||||
|
// document.title = "Published Files";
|
||||||
|
|
||||||
|
// lbry.claim_list_mine().then((claimInfos) => {
|
||||||
|
// if (!this._isMounted) { return; }
|
||||||
|
|
||||||
|
// lbry.file_list().then((fileInfos) => {
|
||||||
|
// if (!this._isMounted) { return; }
|
||||||
|
|
||||||
|
// const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout);
|
||||||
|
// this.setState({
|
||||||
|
// fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// componentWillUnmount: function() {
|
||||||
|
// this._isMounted = false;
|
||||||
|
// },
|
||||||
|
// render: function () {
|
||||||
|
// if (this.state.fileInfos === null) {
|
||||||
|
// return (
|
||||||
|
// <main className="page">
|
||||||
|
// <BusyMessage message="Loading" />
|
||||||
|
// </main>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// else if (!this.state.fileInfos.length) {
|
||||||
|
// return (
|
||||||
|
// <main className="page">
|
||||||
|
// <span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
|
||||||
|
// </main>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// return (
|
||||||
|
// <main className="page">
|
||||||
|
// <FileList fileInfos={this.state.fileInfos} />
|
||||||
|
// </main>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
export default FileListPublished
|
7
ui/js/page/help/index.jsx
Normal file
7
ui/js/page/help/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import HelpPage from './view'
|
||||||
|
|
||||||
|
export default connect(null, null)(HelpPage)
|
|
@ -1,9 +1,9 @@
|
||||||
//@TODO: Customize advice based on OS
|
//@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 {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() {
|
|
@ -1,7 +1,7 @@
|
||||||
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, FormRow} 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 lbryio from '../lbryio.js';
|
||||||
import Modal from '../component/modal.js';
|
import Modal from '../component/modal.js';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbryio from '../lbryio.js';
|
import lbryio from '../lbryio.js';
|
||||||
import {Link} from '../component/link.js';
|
import {Link} from '../component/link';
|
||||||
import Notice from '../component/notice.js';
|
import Notice from '../component/notice.js';
|
||||||
import {CreditAmount} from '../component/common.js';
|
import {CreditAmount} from '../component/common.js';
|
||||||
//
|
//
|
||||||
|
|
|
@ -4,7 +4,7 @@ import lbryio from '../lbryio.js';
|
||||||
import {CreditAmount, Icon} from '../component/common.js';
|
import {CreditAmount, Icon} from '../component/common.js';
|
||||||
import rewards from '../rewards.js';
|
import rewards from '../rewards.js';
|
||||||
import Modal from '../component/modal.js';
|
import Modal from '../component/modal.js';
|
||||||
import {RewardLink} from '../component/link.js';
|
import {RewardLink} from '../component/reward-link.js';
|
||||||
|
|
||||||
const RewardTile = React.createClass({
|
const RewardTile = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import lighthouse from '../lighthouse.js';
|
|
||||||
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 UriIndicator from '../component/channel-indicator.js';
|
|
||||||
|
|
||||||
var FormatItem = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
metadata: React.PropTypes.object,
|
|
||||||
contentType: React.PropTypes.string,
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
outpoint: React.PropTypes.string,
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
const {thumbnail, author, title, description, language, license} = this.props.metadata;
|
|
||||||
const mediaType = lbry.getMediaType(this.props.contentType);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="table-standard">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Content-Type</td><td>{this.props.contentType}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Author</td><td>{author}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Language</td><td>{language}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>License</td><td>{license}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let ShowPage = React.createClass({
|
|
||||||
_uri: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string,
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
metadata: null,
|
|
||||||
contentType: null,
|
|
||||||
hasSignature: false,
|
|
||||||
signatureIsValid: false,
|
|
||||||
cost: null,
|
|
||||||
costIncludesData: null,
|
|
||||||
uriLookupComplete: null,
|
|
||||||
isDownloaded: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
this._uri = lbryuri.normalize(this.props.uri);
|
|
||||||
document.title = this._uri;
|
|
||||||
|
|
||||||
lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
|
|
||||||
const outpoint = txid + ':' + nout;
|
|
||||||
|
|
||||||
lbry.file_list({outpoint}).then((fileInfo) => {
|
|
||||||
this.setState({
|
|
||||||
isDownloaded: fileInfo.length > 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
outpoint: outpoint,
|
|
||||||
metadata: metadata,
|
|
||||||
hasSignature: has_signature,
|
|
||||||
signatureIsValid: signature_is_valid,
|
|
||||||
contentType: contentType,
|
|
||||||
uriLookupComplete: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
|
|
||||||
this.setState({
|
|
||||||
cost: cost,
|
|
||||||
costIncludesData: includesData,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
const metadata = this.state.metadata;
|
|
||||||
const title = metadata ? this.state.metadata.title : this._uri;
|
|
||||||
return (
|
|
||||||
<main className="constrained-page">
|
|
||||||
<section className="show-page-media">
|
|
||||||
{ this.state.contentType && this.state.contentType.startsWith('video/') ?
|
|
||||||
<Video className="video-embedded" uri={this._uri} metadata={metadata} outpoint={this.state.outpoint} /> :
|
|
||||||
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
|
|
||||||
</section>
|
|
||||||
<section className="card">
|
|
||||||
<div className="card__inner">
|
|
||||||
<div className="card__title-identity">
|
|
||||||
{this.state.isDownloaded === false
|
|
||||||
? <span style={{float: "right"}}><FilePrice uri={this._uri} metadata={this.state.metadata} /></span>
|
|
||||||
: null}
|
|
||||||
<h1>{title}</h1>
|
|
||||||
{ this.state.uriLookupComplete ?
|
|
||||||
<div>
|
|
||||||
<div className="card__subtitle">
|
|
||||||
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />
|
|
||||||
</div>
|
|
||||||
<div className="card__actions">
|
|
||||||
<FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />
|
|
||||||
</div>
|
|
||||||
</div> : '' }
|
|
||||||
</div>
|
|
||||||
{ this.state.uriLookupComplete ?
|
|
||||||
<div>
|
|
||||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
|
||||||
{metadata.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
|
|
||||||
</div>
|
|
||||||
{ metadata ?
|
|
||||||
<div className="card__content">
|
|
||||||
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData} />
|
|
||||||
</div> : '' }
|
|
||||||
<div className="card__content">
|
|
||||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ShowPage;
|
|
33
ui/js/page/showPage/index.js
Normal file
33
ui/js/page/showPage/index.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectCurrentUriIsDownloaded,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectCurrentUriClaim,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectCurrentUriCostInfo,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import ShowPage from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
claim: selectCurrentUriClaim(state),
|
||||||
|
uri: selectCurrentUri(state),
|
||||||
|
isDownloaded: selectCurrentUriIsDownloaded(state),
|
||||||
|
fileInfo: selectCurrentUriFileInfo(state),
|
||||||
|
costInfo: selectCurrentUriCostInfo(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(ShowPage)
|
135
ui/js/page/showPage/view.jsx
Normal file
135
ui/js/page/showPage/view.jsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lighthouse from 'lighthouse.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Video from 'page/video'
|
||||||
|
import {
|
||||||
|
TruncatedText,
|
||||||
|
Thumbnail,
|
||||||
|
BusyMessage,
|
||||||
|
} from 'component/common';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import FileActions from 'component/fileActions';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import UriIndicator from 'component/channel-indicator.js';
|
||||||
|
|
||||||
|
const FormatItem = (props) => {
|
||||||
|
const {
|
||||||
|
contentType,
|
||||||
|
metadata,
|
||||||
|
metadata: {
|
||||||
|
thumbnail,
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
language,
|
||||||
|
license,
|
||||||
|
},
|
||||||
|
cost,
|
||||||
|
uri,
|
||||||
|
outpoint,
|
||||||
|
costIncludesData,
|
||||||
|
} = props
|
||||||
|
const mediaType = lbry.getMediaType(contentType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="table-standard">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Content-Type</td><td>{contentType}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Author</td><td>{author}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Language</td><td>{language}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>License</td><td>{license}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowPage = (props) => {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
claim: {
|
||||||
|
txid,
|
||||||
|
nout,
|
||||||
|
has_signature: hasSignature,
|
||||||
|
signature_is_valid: signatureIsValid,
|
||||||
|
value,
|
||||||
|
value: {
|
||||||
|
stream,
|
||||||
|
stream: {
|
||||||
|
metadata,
|
||||||
|
source,
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
} = {},
|
||||||
|
source: {
|
||||||
|
contentType,
|
||||||
|
} = {},
|
||||||
|
} = {},
|
||||||
|
} = {},
|
||||||
|
},
|
||||||
|
uri,
|
||||||
|
isDownloaded,
|
||||||
|
fileInfo,
|
||||||
|
costInfo,
|
||||||
|
costInfo: {
|
||||||
|
cost,
|
||||||
|
includesData: costIncludesData,
|
||||||
|
} = {},
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const outpoint = txid + ':' + nout;
|
||||||
|
const uriLookupComplete = !!claim && Object.keys(claim).length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="constrained-page">
|
||||||
|
<section className="show-page-media">
|
||||||
|
{ contentType && contentType.startsWith('video/') ?
|
||||||
|
<Video className="video-embedded" uri={uri} metadata={metadata} outpoint={outpoint} /> :
|
||||||
|
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
|
||||||
|
</section>
|
||||||
|
<section className="card">
|
||||||
|
<div className="card__inner">
|
||||||
|
<div className="card__title-identity">
|
||||||
|
{isDownloaded === false
|
||||||
|
? <span style={{float: "right"}}><FilePrice uri={lbryuri.normalize(uri)} /></span>
|
||||||
|
: null}
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{ uriLookupComplete ?
|
||||||
|
<div>
|
||||||
|
<div className="card__subtitle">
|
||||||
|
<UriIndicator uri={uri} hasSignature={hasSignature} signatureIsValid={signatureIsValid} />
|
||||||
|
</div>
|
||||||
|
<div className="card__actions">
|
||||||
|
<FileActions uri={uri} outpoint={outpoint} metadata={metadata} contentType={contentType} />
|
||||||
|
</div>
|
||||||
|
</div> : '' }
|
||||||
|
</div>
|
||||||
|
{ uriLookupComplete ?
|
||||||
|
<div>
|
||||||
|
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||||
|
{metadata.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
|
||||||
|
</div>
|
||||||
|
{ metadata ?
|
||||||
|
<div className="card__content">
|
||||||
|
<FormatItem metadata={metadata} contentType={contentType} cost={cost} uri={uri} outpoint={outpoint} costIncludesData={costIncludesData} />
|
||||||
|
</div> : '' }
|
||||||
|
<div className="card__content">
|
||||||
|
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShowPage;
|
42
ui/js/page/video/index.js
Normal file
42
ui/js/page/video/index.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doWatchVideo,
|
||||||
|
doLoadVideo,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
selectLoadingCurrentUri,
|
||||||
|
selectCurrentUriFileReadyToPlay,
|
||||||
|
selectCurrentUriIsPlaying,
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
selectDownloadingCurrentUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectCurrentUriCostInfo,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import Video from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
costInfo: selectCurrentUriCostInfo(state),
|
||||||
|
fileInfo: selectCurrentUriFileInfo(state),
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
isLoading: selectLoadingCurrentUri(state),
|
||||||
|
readyToPlay: selectCurrentUriFileReadyToPlay(state),
|
||||||
|
isDownloading: selectDownloadingCurrentUri(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
loadVideo: () => dispatch(doLoadVideo()),
|
||||||
|
watchVideo: (elem) => dispatch(doWatchVideo()),
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(Video)
|
183
ui/js/page/video/view.jsx
Normal file
183
ui/js/page/video/view.jsx
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Thumbnail,
|
||||||
|
} from 'component/common';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import Link from 'component/link';
|
||||||
|
import lbry from 'lbry';
|
||||||
|
import Modal from 'component/modal';
|
||||||
|
import lbryio from 'lbryio';
|
||||||
|
import rewards from 'rewards';
|
||||||
|
import LoadScreen from 'component/load_screen'
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const VideoStream = require('videostream');
|
||||||
|
|
||||||
|
class WatchLink extends React.Component {
|
||||||
|
confirmPurchaseClick() {
|
||||||
|
this.props.closeModal()
|
||||||
|
this.props.startPlaying()
|
||||||
|
this.props.loadVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
button,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
onWatchClick,
|
||||||
|
metadata,
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
uri,
|
||||||
|
modal,
|
||||||
|
closeModal,
|
||||||
|
isLoading,
|
||||||
|
costInfo,
|
||||||
|
fileInfo,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<Link button={ button ? button : null }
|
||||||
|
disabled={isLoading || costInfo.cost == undefined || fileInfo === undefined}
|
||||||
|
label={label ? label : ""}
|
||||||
|
className="video__play-button"
|
||||||
|
icon="icon-play"
|
||||||
|
onClick={onWatchClick} />
|
||||||
|
{modal}
|
||||||
|
<Modal contentLabel="Not enough credits" isOpen={modal == 'notEnoughCredits'} onConfirmed={closeModal}>
|
||||||
|
You don't have enough LBRY credits to pay for this stream.
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
type="confirm"
|
||||||
|
isOpen={modal == 'affirmPurchase'}
|
||||||
|
contentLabel="Confirm Purchase"
|
||||||
|
onConfirmed={this.confirmPurchaseClick.bind(this)}
|
||||||
|
onAborted={closeModal}>
|
||||||
|
Are you sure you'd like to buy <strong>{this.props.metadata.title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits?
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={modal == 'timedOut'} onConfirmed={closeModal} contentLabel="Timed Out">
|
||||||
|
Sorry, your download timed out :(
|
||||||
|
</Modal>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Video extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
// TODO none of this mouse handling stuff seems to actually do anything?
|
||||||
|
this._controlsHideDelay = 3000 // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
|
||||||
|
this._controlsHideTimeout = null
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
handleMouseMove() {
|
||||||
|
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() {
|
||||||
|
if (this._controlsTimeout) {
|
||||||
|
clearTimeout(this._controlsTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.controlsShown) {
|
||||||
|
this.setState({
|
||||||
|
controlsShown: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWatchClick() {
|
||||||
|
this.props.watchVideo().then(() => {
|
||||||
|
if (!this.props.modal) {
|
||||||
|
this.setState({
|
||||||
|
isPlaying: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startPlaying() {
|
||||||
|
this.setState({
|
||||||
|
isPlaying: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
readyToPlay = false,
|
||||||
|
thumbnail,
|
||||||
|
metadata,
|
||||||
|
isLoading,
|
||||||
|
isDownloading,
|
||||||
|
fileInfo,
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
isPlaying = false,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
let loadStatusMessage = ''
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
loadStatusMessage = "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it"
|
||||||
|
} else if (isDownloading) {
|
||||||
|
loadStatusMessage = "Downloading stream... not long left now!"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onMouseMove={this.handleMouseMove.bind(this)} onMouseLeave={this.handleMouseLeave.bind(this)} className={"video " + this.props.className + (isPlaying && readyToPlay ? " video--active" : " video--hidden")}>{
|
||||||
|
isPlaying ?
|
||||||
|
!readyToPlay ?
|
||||||
|
<span>this is the world's worst loading screen and we shipped our software with it anyway... <br /><br />{loadStatusMessage}</span> :
|
||||||
|
<VideoPlayer downloadPath={fileInfo.download_path} /> :
|
||||||
|
<div className="video__cover" style={{backgroundImage: 'url("' + metadata.thumbnail + '")'}}>
|
||||||
|
<WatchLink icon="icon-play" onWatchClick={this.onWatchClick.bind(this)} startPlaying={this.startPlaying.bind(this)} {...this.props}></WatchLink>
|
||||||
|
</div>
|
||||||
|
}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoPlayer extends React.PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
const elem = this.refs.video
|
||||||
|
const {
|
||||||
|
downloadPath,
|
||||||
|
} = this.props
|
||||||
|
const mediaFile = {
|
||||||
|
createReadStream: (opts) =>
|
||||||
|
fs.createReadStream(downloadPath, opts)
|
||||||
|
}
|
||||||
|
const videostream = VideoStream(mediaFile, elem)
|
||||||
|
elem.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<video controls id="video" ref="video"></video>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Video
|
|
@ -1,317 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import {Link} from '../component/link.js';
|
|
||||||
import Modal from '../component/modal.js';
|
|
||||||
import {FormField, FormRow} from '../component/form.js';
|
|
||||||
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
|
|
||||||
|
|
||||||
var AddressSection = React.createClass({
|
|
||||||
_refreshAddress: function(event) {
|
|
||||||
if (typeof event !== 'undefined') {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getUnusedAddress((address) => {
|
|
||||||
window.localStorage.setItem('wallet_address', address);
|
|
||||||
this.setState({
|
|
||||||
address: address,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_getNewAddress: function(event) {
|
|
||||||
if (typeof event !== 'undefined') {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.wallet_new_address().then(function(address) {
|
|
||||||
window.localStorage.setItem('wallet_address', address);
|
|
||||||
this.setState({
|
|
||||||
address: address,
|
|
||||||
});
|
|
||||||
}.bind(this))
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
address: null,
|
|
||||||
modal: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
var address = window.localStorage.getItem('wallet_address');
|
|
||||||
if (address === null) {
|
|
||||||
this._refreshAddress();
|
|
||||||
} else {
|
|
||||||
lbry.checkAddressIsMine(address, (isMine) => {
|
|
||||||
if (isMine) {
|
|
||||||
this.setState({
|
|
||||||
address: address,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._refreshAddress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<section className="card">
|
|
||||||
<div className="card__title-primary">
|
|
||||||
<h3>Wallet Address</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<Address address={this.state.address} />
|
|
||||||
</div>
|
|
||||||
<div className="card__actions">
|
|
||||||
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<div className="help">
|
|
||||||
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
|
|
||||||
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var SendToAddressSection = React.createClass({
|
|
||||||
handleSubmit: function(event) {
|
|
||||||
if (typeof event !== 'undefined') {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((this.state.balance - this.state.amount) < 1)
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
modal: 'insufficientBalance',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
results: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
|
|
||||||
if(results === true)
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
results: "Your transaction was successfully placed in the queue.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
results: "Something went wrong: " + results
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, (error) => {
|
|
||||||
this.setState({
|
|
||||||
results: "Something went wrong: " + error.message
|
|
||||||
})
|
|
||||||
});
|
|
||||||
},
|
|
||||||
closeModal: function() {
|
|
||||||
this.setState({
|
|
||||||
modal: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
address: "",
|
|
||||||
amount: 0.0,
|
|
||||||
balance: <BusyMessage message="Checking balance" />,
|
|
||||||
results: "",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
lbry.getBalance((results) => {
|
|
||||||
this.setState({
|
|
||||||
balance: results,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setAmount: function(event) {
|
|
||||||
this.setState({
|
|
||||||
amount: parseFloat(event.target.value),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setAddress: function(event) {
|
|
||||||
this.setState({
|
|
||||||
address: event.target.value,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<section className="card">
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<div className="card__title-primary">
|
|
||||||
<h3>Send Credits</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
|
|
||||||
</div>
|
|
||||||
<div className="card__actions card__actions--form-submit">
|
|
||||||
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
|
|
||||||
<input type='submit' className='hidden' />
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
this.state.results ?
|
|
||||||
<div className="card__content">
|
|
||||||
<h4>Results</h4>
|
|
||||||
{this.state.results}
|
|
||||||
</div> : ''
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
|
|
||||||
onConfirmed={this.closeModal}>
|
|
||||||
Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
|
|
||||||
</Modal>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var TransactionList = React.createClass({
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
transactionItems: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
lbry.call('get_transaction_history', {}, (results) => {
|
|
||||||
if (results.length == 0) {
|
|
||||||
this.setState({ transactionItems: [] })
|
|
||||||
} else {
|
|
||||||
var transactionItems = [],
|
|
||||||
condensedTransactions = {};
|
|
||||||
results.forEach(function(tx) {
|
|
||||||
var txid = tx["txid"];
|
|
||||||
if (!(txid in condensedTransactions)) {
|
|
||||||
condensedTransactions[txid] = 0;
|
|
||||||
}
|
|
||||||
condensedTransactions[txid] += parseFloat(tx["value"]);
|
|
||||||
});
|
|
||||||
results.reverse().forEach(function(tx) {
|
|
||||||
var txid = tx["txid"];
|
|
||||||
if (condensedTransactions[txid] && condensedTransactions[txid] != 0)
|
|
||||||
{
|
|
||||||
transactionItems.push({
|
|
||||||
id: txid,
|
|
||||||
date: tx["timestamp"] ? (new Date(parseInt(tx["timestamp"]) * 1000)) : null,
|
|
||||||
amount: condensedTransactions[txid]
|
|
||||||
});
|
|
||||||
delete condensedTransactions[txid];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ transactionItems: transactionItems });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
var rows = [];
|
|
||||||
if (this.state.transactionItems && this.state.transactionItems.length > 0)
|
|
||||||
{
|
|
||||||
this.state.transactionItems.forEach(function(item) {
|
|
||||||
rows.push(
|
|
||||||
<tr key={item.id}>
|
|
||||||
<td>{ (item.amount > 0 ? '+' : '' ) + item.amount }</td>
|
|
||||||
<td>{ item.date ? item.date.toLocaleDateString() : <span className="empty">(Transaction pending)</span> }</td>
|
|
||||||
<td>{ item.date ? item.date.toLocaleTimeString() : <span className="empty">(Transaction pending)</span> }</td>
|
|
||||||
<td>
|
|
||||||
<a className="button-text" href={"https://explorer.lbry.io/tx/"+item.id} target="_blank">{item.id.substr(0, 7)}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<section className="card">
|
|
||||||
<div className="card__title-primary">
|
|
||||||
<h3>Transaction History</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
|
|
||||||
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
|
|
||||||
{ this.state.transactionItems && rows.length > 0 ?
|
|
||||||
<table className="table-standard table-stretch">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Transaction</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var WalletPage = React.createClass({
|
|
||||||
_balanceSubscribeId: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
viewingPage: React.PropTypes.string,
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
document.title = "My Wallet";
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
Below should be refactored so that balance is shared all of wallet page. Or even broader?
|
|
||||||
What is the proper React pattern for sharing a global state like balance?
|
|
||||||
*/
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
balance: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
this._balanceSubscribeId = lbry.balanceSubscribe((results) => {
|
|
||||||
this.setState({
|
|
||||||
balance: results,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._balanceSubscribeId) {
|
|
||||||
lbry.balanceUnsubscribe(this._balanceSubscribeId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<main className="page">
|
|
||||||
<section className="card">
|
|
||||||
<div className="card__title-primary">
|
|
||||||
<h3>Balance</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
|
|
||||||
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
|
|
||||||
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }
|
|
||||||
{ this.props.viewingPage === 'receive' ? <AddressSection /> : '' }
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default WalletPage;
|
|
53
ui/js/page/wallet/index.js
Normal file
53
ui/js/page/wallet/index.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doGetNewAddress,
|
||||||
|
doCheckAddressIsMine,
|
||||||
|
doSendDraftTransaction,
|
||||||
|
doSetDraftTransactionAmount,
|
||||||
|
doSetDraftTransactionAddress,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
selectTransactions,
|
||||||
|
selectTransactionItems,
|
||||||
|
selectIsFetchingTransactions,
|
||||||
|
selectReceiveAddress,
|
||||||
|
selectGettingNewAddress,
|
||||||
|
selectDraftTransactionAmount,
|
||||||
|
selectDraftTransactionAddress,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import WalletPage from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
currentPage: selectCurrentPage(state),
|
||||||
|
balance: selectBalance(state),
|
||||||
|
transactions: selectTransactions(state),
|
||||||
|
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||||
|
transactionItems: selectTransactionItems(state),
|
||||||
|
receiveAddress: selectReceiveAddress(state),
|
||||||
|
gettingNewAddress: selectGettingNewAddress(state),
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
address: selectDraftTransactionAddress(state),
|
||||||
|
amount: selectDraftTransactionAmount(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||||
|
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
|
||||||
|
sendToAddress: () => dispatch(doSendDraftTransaction()),
|
||||||
|
setAmount: (event) => dispatch(doSetDraftTransactionAmount(event.target.value)),
|
||||||
|
setAddress: (event) => dispatch(doSetDraftTransactionAddress(event.target.value)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(WalletPage)
|
265
ui/js/page/wallet/view.jsx
Normal file
265
ui/js/page/wallet/view.jsx
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import Modal from 'component/modal';
|
||||||
|
import {
|
||||||
|
FormField,
|
||||||
|
FormRow
|
||||||
|
} from 'component/form';
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
BusyMessage,
|
||||||
|
CreditAmount
|
||||||
|
} from 'component/common';
|
||||||
|
|
||||||
|
class AddressSection extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.checkAddressIsMine(this.props.receiveAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
receiveAddress,
|
||||||
|
getNewAddress,
|
||||||
|
gettingNewAddress,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card">
|
||||||
|
<div className="card__title-primary">
|
||||||
|
<h3>Wallet Address</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<Address address={receiveAddress} />
|
||||||
|
</div>
|
||||||
|
<div className="card__actions">
|
||||||
|
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={getNewAddress} disabled={gettingNewAddress} />
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<div className="help">
|
||||||
|
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
|
||||||
|
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendToAddressSection = (props) => {
|
||||||
|
const {
|
||||||
|
sendToAddress,
|
||||||
|
closeModal,
|
||||||
|
modal,
|
||||||
|
setAmount,
|
||||||
|
setAddress,
|
||||||
|
amount,
|
||||||
|
address,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card">
|
||||||
|
<form onSubmit={sendToAddress}>
|
||||||
|
<div className="card__title-primary">
|
||||||
|
<h3>Send Credits</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={setAmount} value={amount} />
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={setAddress} value={address} />
|
||||||
|
</div>
|
||||||
|
<div className="card__actions card__actions--form-submit">
|
||||||
|
<Link button="primary" label="Send" onClick={sendToAddress} disabled={!(parseFloat(amount) > 0.0) || !address} />
|
||||||
|
<input type='submit' className='hidden' />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{modal == 'insufficientBalance' && <Modal isOpen={true} contentLabel="Insufficient balance" onConfirmed={closeModal}>
|
||||||
|
Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
|
||||||
|
</Modal>}
|
||||||
|
{modal == 'transactionSuccessful' && <Modal isOpen={true} contentLabel="Transaction successful" onConfirmed={closeModal}>
|
||||||
|
Your transaction was successfully placed in the queue.
|
||||||
|
</Modal>}
|
||||||
|
{modal == 'transactionFailed' && <Modal isOpen={true} contentLabel="Transaction failed" onConfirmed={closeModal}>
|
||||||
|
Something went wrong:
|
||||||
|
</Modal>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// var SendToAddressSection = React.createClass({
|
||||||
|
// handleSubmit: function(event) {
|
||||||
|
// if (typeof event !== 'undefined') {
|
||||||
|
// event.preventDefault();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if ((this.state.balance - this.state.amount) < 1)
|
||||||
|
// {
|
||||||
|
// this.setState({
|
||||||
|
// modal: 'insufficientBalance',
|
||||||
|
// });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.setState({
|
||||||
|
// results: "",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
|
||||||
|
// if(results === true)
|
||||||
|
// {
|
||||||
|
// this.setState({
|
||||||
|
// results: "Your transaction was successfully placed in the queue.",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// this.setState({
|
||||||
|
// results: "Something went wrong: " + results
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }, (error) => {
|
||||||
|
// this.setState({
|
||||||
|
// results: "Something went wrong: " + error.message
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// closeModal: function() {
|
||||||
|
// this.setState({
|
||||||
|
// modal: null,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// getInitialState: function() {
|
||||||
|
// return {
|
||||||
|
// address: "",
|
||||||
|
// amount: 0.0,
|
||||||
|
// balance: <BusyMessage message="Checking balance" />,
|
||||||
|
// results: "",
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// componentWillMount: function() {
|
||||||
|
// lbry.getBalance((results) => {
|
||||||
|
// this.setState({
|
||||||
|
// balance: results,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// setAmount: function(event) {
|
||||||
|
// this.setState({
|
||||||
|
// amount: parseFloat(event.target.value),
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// setAddress: function(event) {
|
||||||
|
// this.setState({
|
||||||
|
// address: event.target.value,
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// render: function() {
|
||||||
|
// return (
|
||||||
|
// <section className="card">
|
||||||
|
// <form onSubmit={this.handleSubmit}>
|
||||||
|
// <div className="card__title-primary">
|
||||||
|
// <h3>Send Credits</h3>
|
||||||
|
// </div>
|
||||||
|
// <div className="card__content">
|
||||||
|
// <FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
|
||||||
|
// </div>
|
||||||
|
// <div className="card__content">
|
||||||
|
// <FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
|
||||||
|
// </div>
|
||||||
|
// <div className="card__actions card__actions--form-submit">
|
||||||
|
// <Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
|
||||||
|
// <input type='submit' className='hidden' />
|
||||||
|
// </div>
|
||||||
|
// {
|
||||||
|
// this.state.results ?
|
||||||
|
// <div className="card__content">
|
||||||
|
// <h4>Results</h4>
|
||||||
|
// {this.state.results}
|
||||||
|
// </div> : ''
|
||||||
|
// }
|
||||||
|
// </form>
|
||||||
|
// <Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
|
||||||
|
// onConfirmed={this.closeModal}>
|
||||||
|
// Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
|
||||||
|
// </Modal>
|
||||||
|
// </section>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const TransactionList = (props) => {
|
||||||
|
const {
|
||||||
|
fetchingTransactions,
|
||||||
|
transactionItems,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
if (transactionItems.length > 0) {
|
||||||
|
transactionItems.forEach(function(item) {
|
||||||
|
rows.push(
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{ (item.amount > 0 ? '+' : '' ) + item.amount }</td>
|
||||||
|
<td>{ item.date ? item.date.toLocaleDateString() : <span className="empty">(Transaction pending)</span> }</td>
|
||||||
|
<td>{ item.date ? item.date.toLocaleTimeString() : <span className="empty">(Transaction pending)</span> }</td>
|
||||||
|
<td>
|
||||||
|
<a className="button-text" href={"https://explorer.lbry.io/tx/"+item.id} target="_blank">{item.id.substr(0, 7)}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card">
|
||||||
|
<div className="card__title-primary">
|
||||||
|
<h3>Transaction History</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
{ fetchingTransactions ? <BusyMessage message="Loading transactions" /> : '' }
|
||||||
|
{ !fetchingTransactions && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
|
||||||
|
{ rows.length > 0 ?
|
||||||
|
<table className="table-standard table-stretch">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Transaction</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const WalletPage = (props) => {
|
||||||
|
const {
|
||||||
|
balance,
|
||||||
|
currentPage
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="page">
|
||||||
|
<section className="card">
|
||||||
|
<div className="card__title-primary">
|
||||||
|
<h3>Balance</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<CreditAmount amount={balance} precision={8} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{ currentPage === 'wallet' ? <TransactionList {...props} /> : '' }
|
||||||
|
{ currentPage === 'send' ? <SendToAddressSection {...props} /> : '' }
|
||||||
|
{ currentPage === 'receive' ? <AddressSection {...props} /> : '' }
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WalletPage;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
112
ui/js/reducers/app.js
Normal file
112
ui/js/reducers/app.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
isLoaded: false,
|
||||||
|
currentPath: 'discover',
|
||||||
|
platform: process.platform,
|
||||||
|
drawerOpen: sessionStorage.getItem('drawerOpen') || true,
|
||||||
|
upgradeSkipped: sessionStorage.getItem('upgradeSkipped'),
|
||||||
|
daemonReady: false,
|
||||||
|
platform: window.navigator.platform,
|
||||||
|
obscureNsfw: !lbry.getClientSetting('showNsfw'),
|
||||||
|
hidePrice: false,
|
||||||
|
hasSignature: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.NAVIGATE] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
currentPath: 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DAEMON_READY] = function(state, action) {
|
||||||
|
window.sessionStorage.setItem('loaded', 'y')
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
daemonReady: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
45
ui/js/reducers/availability.js
Normal file
45
ui/js/reducers/availability.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_AVAILABILITY_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
const newByUri = Object.assign({}, newFetching.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = true
|
||||||
|
newFetching.byUri = newByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetching: newFetching,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_AVAILABILITY_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
availability,
|
||||||
|
} = action.data
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
const newFetchingByUri = Object.assign({}, newFetching.byUri)
|
||||||
|
const newAvailabilityByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
delete newFetchingByUri[uri]
|
||||||
|
newFetching.byUri = newFetchingByUri
|
||||||
|
newAvailabilityByUri[uri] = availability
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetching: newFetching,
|
||||||
|
byUri: newAvailabilityByUri
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
27
ui/js/reducers/certificates.js
Normal file
27
ui/js/reducers/certificates.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
certificate,
|
||||||
|
} = action.data
|
||||||
|
if (!certificate) return state
|
||||||
|
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = certificate
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
42
ui/js/reducers/claims.js
Normal file
42
ui/js/reducers/claims.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
claim,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = claim
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_MY_CLAIMS_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
claims,
|
||||||
|
} = action.data
|
||||||
|
const newMine = Object.assign({}, state.mine)
|
||||||
|
const newById = Object.assign({}, newMine.byId)
|
||||||
|
|
||||||
|
claims.forEach(claim => {
|
||||||
|
newById[claim.claim_id] = claim
|
||||||
|
})
|
||||||
|
newMine.byId = newById
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
mine: newMine,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
103
ui/js/reducers/content.js
Normal file
103
ui/js/reducers/content.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_FEATURED_CONTENT_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingFeaturedContent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_FEATURED_CONTENT_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uris
|
||||||
|
} = action.data
|
||||||
|
const newFeaturedContent = Object.assign({}, state.featuredContent, {
|
||||||
|
byCategory: uris,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingFeaturedContent: false,
|
||||||
|
featuredContent: newFeaturedContent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.RESOLVE_URI_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri
|
||||||
|
} = action.data
|
||||||
|
|
||||||
|
const oldResolving = state.resolvingUris || []
|
||||||
|
const newResolving = Object.assign([], oldResolving)
|
||||||
|
if (newResolving.indexOf(uri) == -1) newResolving.push(uri)
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
resolvingUris: newResolving
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const resolvingUris = state.resolvingUris
|
||||||
|
const index = state.resolvingUris.indexOf(uri)
|
||||||
|
const newResolvingUris = [
|
||||||
|
...resolvingUris.slice(0, index),
|
||||||
|
...resolvingUris.slice(index + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
const newState = Object.assign({}, state, {
|
||||||
|
resolvingUris: newResolvingUris,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_DOWNLOADED_CONTENT_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingDownloadedContent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_DOWNLOADED_CONTENT_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
fileInfos
|
||||||
|
} = action.data
|
||||||
|
const newDownloadedContent = Object.assign({}, state.downloadedContent, {
|
||||||
|
fileInfos
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
downloadedContent: newDownloadedContent,
|
||||||
|
fetchingDownloadedContent: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_PUBLISHED_CONTENT_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingPublishedContent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_PUBLISHED_CONTENT_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
fileInfos
|
||||||
|
} = action.data
|
||||||
|
const newPublishedContent = Object.assign({}, state.publishedContent, {
|
||||||
|
fileInfos
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
publishedContent: newPublishedContent,
|
||||||
|
fetchingPublishedContent: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
40
ui/js/reducers/cost_info.js
Normal file
40
ui/js/reducers/cost_info.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_COST_INFO_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
newFetching[uri] = true
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetching: newFetching,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_COST_INFO_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
costInfo,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
|
||||||
|
newByUri[uri] = costInfo
|
||||||
|
delete newFetching[uri]
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
fetching: newFetching,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
206
ui/js/reducers/file_info.js
Normal file
206
ui/js/reducers/file_info.js
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_FILE_INFO_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
|
||||||
|
newFetching[uri] = true
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetching: newFetching,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_FILE_INFO_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
const newFetching = Object.assign({}, state.fetching)
|
||||||
|
|
||||||
|
newByUri[uri] = fileInfo || {}
|
||||||
|
delete newFetching[uri]
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
fetching: newFetching,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DOWNLOADING_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
const newDownloading = Object.assign({}, state.downloading)
|
||||||
|
const newDownloadingByUri = Object.assign({}, newDownloading.byUri)
|
||||||
|
const newLoading = Object.assign({}, state.loading)
|
||||||
|
const newLoadingByUri = Object.assign({}, newLoading)
|
||||||
|
|
||||||
|
newDownloadingByUri[uri] = true
|
||||||
|
newDownloading.byUri = newDownloadingByUri
|
||||||
|
newByUri[uri] = fileInfo
|
||||||
|
delete newLoadingByUri[uri]
|
||||||
|
newLoading.byUri = newLoadingByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
downloading: newDownloading,
|
||||||
|
byUri: newByUri,
|
||||||
|
loading: newLoading,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DOWNLOADING_PROGRESSED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
const newDownloading = Object.assign({}, state.downloading)
|
||||||
|
|
||||||
|
newByUri[uri] = fileInfo
|
||||||
|
newDownloading[uri] = true
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
downloading: newDownloading
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DOWNLOADING_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
fileInfo,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
const newDownloading = Object.assign({}, state.downloading)
|
||||||
|
const newDownloadingByUri = Object.assign({}, newDownloading.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = fileInfo
|
||||||
|
delete newDownloadingByUri[uri]
|
||||||
|
newDownloading.byUri = newDownloadingByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri,
|
||||||
|
downloading: newDownloading,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DELETE_FILE_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newDeleting = Object.assign({}, state.deleting)
|
||||||
|
const newByUri = Object.assign({}, newDeleting.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = true
|
||||||
|
newDeleting.byUri = newByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
deleting: newDeleting,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DELETE_FILE_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newDeleting = Object.assign({}, state.deleting)
|
||||||
|
const newDeletingByUri = Object.assign({}, newDeleting.byUri)
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
delete newDeletingByUri[uri]
|
||||||
|
newDeleting.byUri = newDeletingByUri
|
||||||
|
delete newByUri[uri]
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
deleting: newDeleting,
|
||||||
|
byUri: newByUri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.LOADING_VIDEO_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newLoading = Object.assign({}, state.loading)
|
||||||
|
const newByUri = Object.assign({}, newLoading.byUri)
|
||||||
|
|
||||||
|
newByUri[uri] = true
|
||||||
|
newLoading.byUri = newByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: newLoading,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.LOADING_VIDEO_FAILED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
} = action.data
|
||||||
|
const newLoading = Object.assign({}, state.loading)
|
||||||
|
const newByUri = Object.assign({}, newLoading.byUri)
|
||||||
|
|
||||||
|
delete newByUri[uri]
|
||||||
|
newLoading.byUri = newByUri
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: newLoading,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_DOWNLOADED_CONTENT_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
fileInfos,
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
channelName: fileInfo.channel_name,
|
||||||
|
contentName: fileInfo.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
newByUri[uri] = fileInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_PUBLISHED_CONTENT_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
fileInfos
|
||||||
|
} = action.data
|
||||||
|
const newByUri = Object.assign({}, state.byUri)
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
const uri = lbryuri.build({
|
||||||
|
channelName: fileInfo.channel_name,
|
||||||
|
contentName: fileInfo.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
newByUri[uri] = fileInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byUri: newByUri
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
11
ui/js/reducers/rewards.js
Normal file
11
ui/js/reducers/rewards.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
60
ui/js/reducers/search.js
Normal file
60
ui/js/reducers/search.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const defaultState = {
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEARCH_STARTED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
} = action.data
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
searching: true,
|
||||||
|
query: query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEARCH_COMPLETED] = function(state, action) {
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
} = action.data
|
||||||
|
const oldResults = Object.assign({}, state.results)
|
||||||
|
const newByQuery = Object.assign({}, oldResults.byQuery)
|
||||||
|
newByQuery[query] = results
|
||||||
|
const newResults = Object.assign({}, oldResults, {
|
||||||
|
byQuery: newByQuery
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
searching: false,
|
||||||
|
results: newResults,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEARCH_CANCELLED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
searching: false,
|
||||||
|
query: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.ACTIVATE_SEARCH] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
activated: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.DEACTIVATE_SEARCH] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
activated: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
131
ui/js/reducers/wallet.js
Normal file
131
ui/js/reducers/wallet.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
|
||||||
|
const reducers = {}
|
||||||
|
const address = sessionStorage.getItem('receiveAddress')
|
||||||
|
const buildDraftTransaction = () => ({
|
||||||
|
amount: undefined,
|
||||||
|
address: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
balance: 0,
|
||||||
|
transactions: [],
|
||||||
|
fetchingTransactions: false,
|
||||||
|
receiveAddress: address,
|
||||||
|
gettingNewAddress: false,
|
||||||
|
draftTransaction: buildDraftTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingTransactions: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
|
||||||
|
const oldTransactions = Object.assign({}, state.transactions)
|
||||||
|
const byId = Object.assign({}, oldTransactions.byId)
|
||||||
|
const { transactions } = action.data
|
||||||
|
|
||||||
|
transactions.forEach((transaction) => {
|
||||||
|
byId[transaction.txid] = transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
const newTransactions = Object.assign({}, oldTransactions, {
|
||||||
|
byId: byId
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
transactions: newTransactions,
|
||||||
|
fetchingTransactions: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.GET_NEW_ADDRESS_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
gettingNewAddress: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.GET_NEW_ADDRESS_COMPLETED] = function(state, action) {
|
||||||
|
const { address } = action.data
|
||||||
|
|
||||||
|
sessionStorage.setItem('receiveAddress', address)
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
gettingNewAddress: false,
|
||||||
|
receiveAddress: address
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.UPDATE_BALANCE] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
balance: action.data.balance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.CHECK_ADDRESS_IS_MINE_STARTED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
checkingAddressOwnership: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.CHECK_ADDRESS_IS_MINE_COMPLETED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
checkingAddressOwnership: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SET_DRAFT_TRANSACTION_AMOUNT] = function(state, action) {
|
||||||
|
const oldDraft = state.draftTransaction
|
||||||
|
const newDraft = Object.assign({}, oldDraft, {
|
||||||
|
amount: parseFloat(action.data.amount)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraft
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SET_DRAFT_TRANSACTION_ADDRESS] = function(state, action) {
|
||||||
|
const oldDraft = state.draftTransaction
|
||||||
|
const newDraft = Object.assign({}, oldDraft, {
|
||||||
|
address: action.data.address
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraft
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEND_TRANSACTION_STARTED] = function(state, action) {
|
||||||
|
const newDraftTransaction = Object.assign({}, state.draftTransaction, {
|
||||||
|
sending: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraftTransaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEND_TRANSACTION_COMPLETED] = function(state, action) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: buildDraftTransaction()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[types.SEND_TRANSACTION_FAILED] = function(state, action) {
|
||||||
|
const newDraftTransaction = Object.assign({}, state.draftTransaction, {
|
||||||
|
sending: false,
|
||||||
|
error: action.data.error
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraftTransaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function reducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
196
ui/js/selectors/app.js
Normal file
196
ui/js/selectors/app.js
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
|
export const _selectState = state => state.app || {}
|
||||||
|
|
||||||
|
export const selectIsLoaded = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.isLoaded
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentPath = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.currentPath
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentPage = createSelector(
|
||||||
|
selectCurrentPath,
|
||||||
|
(path) => path.split('=')[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUri = createSelector(
|
||||||
|
selectCurrentPath,
|
||||||
|
(path) => {
|
||||||
|
if (path.match(/=/)) {
|
||||||
|
return path.split('=')[1]
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectPageTitle = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentUri,
|
||||||
|
(page, uri) => {
|
||||||
|
switch(page)
|
||||||
|
{
|
||||||
|
case 'discover':
|
||||||
|
return 'Discover'
|
||||||
|
case 'wallet':
|
||||||
|
case 'send':
|
||||||
|
case 'receive':
|
||||||
|
case 'claim':
|
||||||
|
case 'referral':
|
||||||
|
return 'Wallet'
|
||||||
|
case 'downloaded':
|
||||||
|
return 'My Files'
|
||||||
|
case 'published':
|
||||||
|
return 'My Files'
|
||||||
|
case 'publish':
|
||||||
|
return 'Publish'
|
||||||
|
case 'help':
|
||||||
|
return 'Help'
|
||||||
|
default:
|
||||||
|
return 'LBRY';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectPlatform = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => 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
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDaemonReady = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.daemonReady
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectObscureNsfw = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.obscureNsfw
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectHidePrice = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.hidePrice
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectHasSignature = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.hasSignature
|
||||||
|
)
|
74
ui/js/selectors/availability.js
Normal file
74
ui/js/selectors/availability.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import {
|
||||||
|
createSelector,
|
||||||
|
} from 'reselect'
|
||||||
|
import {
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
const _selectState = state => state.availability
|
||||||
|
|
||||||
|
export const selectAvailabilityByUri = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingAvailability = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.fetching || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingAvailabilityByUri = createSelector(
|
||||||
|
selectFetchingAvailability,
|
||||||
|
(fetching) => fetching.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectAvailabilityForUri = (state, props) => {
|
||||||
|
return selectAvailabilityByUri(state)[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectAvailabilityForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectAvailabilityForUri,
|
||||||
|
(availability) => availability
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFetchingAvailabilityForUri = (state, props) => {
|
||||||
|
return selectFetchingAvailabilityByUri(state)[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectFetchingAvailabilityForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectFetchingAvailabilityForUri,
|
||||||
|
(fetching) => fetching
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectFetchingAvailabilityForCurrentUri = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectFetchingAvailabilityByUri,
|
||||||
|
(uri, byUri) => byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectAvailabilityForCurrentUri = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectAvailabilityByUri,
|
||||||
|
(uri, byUri) => byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchCurrentUriAvailability = createSelector(
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectFetchingAvailabilityForCurrentUri,
|
||||||
|
selectAvailabilityForCurrentUri,
|
||||||
|
(daemonReady, page, fetching, availability) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (page != 'show') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (availability) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
89
ui/js/selectors/claims.js
Normal file
89
ui/js/selectors/claims.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import {
|
||||||
|
createSelector,
|
||||||
|
} from 'reselect'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export const _selectState = state => state.claims || {}
|
||||||
|
|
||||||
|
export const selectClaimsByUri = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriClaim = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectClaimsByUri,
|
||||||
|
(uri, byUri) => byUri[uri] || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriClaimOutpoint = createSelector(
|
||||||
|
selectCurrentUriClaim,
|
||||||
|
(claim) => `${claim.txid}:${claim.nout}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectClaimForUri = (state, props) => {
|
||||||
|
const uri = lbryuri.normalize(props.uri)
|
||||||
|
return selectClaimsByUri(state)[uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectClaimForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectClaimForUri,
|
||||||
|
(claim) => claim
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectMetadataForUri = (state, props) => {
|
||||||
|
const claim = selectClaimForUri(state, props)
|
||||||
|
const metadata = claim && claim.value && claim.value.stream && claim.value.stream.metadata
|
||||||
|
|
||||||
|
return metadata ? metadata : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectMetadataForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectMetadataForUri,
|
||||||
|
(metadata) => metadata
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectSourceForUri = (state, props) => {
|
||||||
|
const claim = selectClaimForUri(state, props)
|
||||||
|
const source = claim && claim.value && claim.value.stream && claim.value.stream.source
|
||||||
|
|
||||||
|
return source ? source : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectSourceForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectSourceForUri,
|
||||||
|
(source) => source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectMyClaims = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.mine || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectMyClaimsById = createSelector(
|
||||||
|
selectMyClaims,
|
||||||
|
(mine) => mine.byId || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectMyClaimsOutpoints = createSelector(
|
||||||
|
selectMyClaimsById,
|
||||||
|
(byId) => {
|
||||||
|
const outpoints = []
|
||||||
|
Object.keys(byId).forEach(key => {
|
||||||
|
const claim = byId[key]
|
||||||
|
const outpoint = `${claim.txid}:${claim.nout}`
|
||||||
|
outpoints.push(outpoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
return outpoints
|
||||||
|
}
|
||||||
|
)
|
98
ui/js/selectors/content.js
Normal file
98
ui/js/selectors/content.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
import {
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentUri,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export const _selectState = state => state.content || {}
|
||||||
|
|
||||||
|
export const selectFeaturedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.featuredContent || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFeaturedContentByCategory = createSelector(
|
||||||
|
selectFeaturedContent,
|
||||||
|
(featuredContent) => featuredContent.byCategory || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingFeaturedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.fetchingFeaturedContent
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchFeaturedContent = createSelector(
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectFetchingFeaturedContent,
|
||||||
|
selectFeaturedContentByCategory,
|
||||||
|
(daemonReady, page, fetching, byCategory) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (page != 'discover') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (Object.keys(byCategory).length != 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingFileInfos = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.fetchingFileInfos || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingDownloadedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.fetchingDownloadedContent
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDownloadedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.downloadedContent || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDownloadedContentFileInfos = createSelector(
|
||||||
|
selectDownloadedContent,
|
||||||
|
(downloadedContent) => downloadedContent.fileInfos || []
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchDownloadedContent = createSelector(
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectFetchingDownloadedContent,
|
||||||
|
selectDownloadedContent,
|
||||||
|
(daemonReady, page, fetching, content) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (page != 'downloaded') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (Object.keys(content).length != 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingPublishedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.fetchingPublishedContent
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectPublishedContent = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.publishedContent || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchPublishedContent = createSelector(
|
||||||
|
selectDaemonReady,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectFetchingPublishedContent,
|
||||||
|
selectPublishedContent,
|
||||||
|
(daemonReady, page, fetching, content) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (page != 'published') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (Object.keys(content).length != 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
54
ui/js/selectors/cost_info.js
Normal file
54
ui/js/selectors/cost_info.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
selectCurrentPage,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export const _selectState = state => state.costInfo || {}
|
||||||
|
|
||||||
|
export const selectAllCostInfoByUri = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriCostInfo = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectAllCostInfoByUri,
|
||||||
|
(uri, byUri) => byUri[uri] || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingCostInfo = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.fetching || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingCurrentUriCostInfo = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectFetchingCostInfo,
|
||||||
|
(uri, byUri) => !!byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchCurrentUriCostInfo = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentUri,
|
||||||
|
selectFetchingCurrentUriCostInfo,
|
||||||
|
selectCurrentUriCostInfo,
|
||||||
|
(page, uri, fetching, costInfo) => {
|
||||||
|
if (page != 'show') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (Object.keys(costInfo).length != 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectCostInfoForUri = (state, props) => {
|
||||||
|
return selectAllCostInfoByUri(state)[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectCostInfoForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectCostInfoForUri,
|
||||||
|
(costInfo) => costInfo
|
||||||
|
)
|
||||||
|
}
|
172
ui/js/selectors/file_info.js
Normal file
172
ui/js/selectors/file_info.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import {
|
||||||
|
createSelector,
|
||||||
|
} from 'reselect'
|
||||||
|
import {
|
||||||
|
selectCurrentUri,
|
||||||
|
selectCurrentPage,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectMyClaimsOutpoints,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
|
||||||
|
export const _selectState = state => state.fileInfo || {}
|
||||||
|
|
||||||
|
export const selectAllFileInfoByUri = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriRawFileInfo = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectAllFileInfoByUri,
|
||||||
|
(uri, byUri) => byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriFileInfo = createSelector(
|
||||||
|
selectCurrentUriRawFileInfo,
|
||||||
|
(fileInfo) => fileInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingFileInfo = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.fetching || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectFetchingCurrentUriFileInfo = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectFetchingFileInfo,
|
||||||
|
(uri, byUri) => !!byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDownloading = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.downloading || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDownloadingByUri = createSelector(
|
||||||
|
selectDownloading,
|
||||||
|
(downloading) => downloading.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDownloadingCurrentUri = createSelector(
|
||||||
|
selectCurrentUri,
|
||||||
|
selectDownloadingByUri,
|
||||||
|
(uri, byUri) => !!byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentUriIsDownloaded = createSelector(
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
(fileInfo) => {
|
||||||
|
if (!fileInfo) return false
|
||||||
|
if (!fileInfo.completed) return false
|
||||||
|
if (!fileInfo.written_bytes > 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchCurrentUriFileInfo = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentUri,
|
||||||
|
selectFetchingCurrentUriFileInfo,
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
(page, uri, fetching, fileInfo) => {
|
||||||
|
if (page != 'show') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (fileInfo != undefined) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectFileInfoForUri = (state, props) => {
|
||||||
|
return selectAllFileInfoByUri(state)[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectFileInfoForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectFileInfoForUri,
|
||||||
|
(fileInfo) => fileInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDownloadingForUri = (state, props) => {
|
||||||
|
const byUri = selectDownloadingByUri(state)
|
||||||
|
return byUri[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectDownloadingForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectDownloadingForUri,
|
||||||
|
(downloadingForUri) => !!downloadingForUri
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectLoading = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.loading || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectLoadingByUri = createSelector(
|
||||||
|
selectLoading,
|
||||||
|
(loading) => loading.byUri || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectLoadingCurrentUri = createSelector(
|
||||||
|
selectLoadingByUri,
|
||||||
|
selectCurrentUri,
|
||||||
|
(byUri, uri) => !!byUri[uri]
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO make this smarter so it doesn't start playing and immediately freeze
|
||||||
|
// while downloading more.
|
||||||
|
export const selectCurrentUriFileReadyToPlay = createSelector(
|
||||||
|
selectCurrentUriFileInfo,
|
||||||
|
(fileInfo) => (fileInfo || {}).written_bytes > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectLoadingForUri = (state, props) => {
|
||||||
|
const byUri = selectLoadingByUri(state)
|
||||||
|
return byUri[props.uri]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSelectLoadingForUri = () => {
|
||||||
|
return createSelector(
|
||||||
|
selectLoadingForUri,
|
||||||
|
(loading) => !!loading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectDownloadedFileInfo = createSelector(
|
||||||
|
selectAllFileInfoByUri,
|
||||||
|
(byUri) => {
|
||||||
|
const fileInfoList = []
|
||||||
|
Object.keys(byUri).forEach(key => {
|
||||||
|
const fileInfo = byUri[key]
|
||||||
|
|
||||||
|
if (fileInfo.completed || fileInfo.written_bytes) {
|
||||||
|
fileInfoList.push(fileInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return fileInfoList
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectPublishedFileInfo = createSelector(
|
||||||
|
selectAllFileInfoByUri,
|
||||||
|
selectMyClaimsOutpoints,
|
||||||
|
(byUri, outpoints) => {
|
||||||
|
const fileInfos = []
|
||||||
|
outpoints.forEach(outpoint => {
|
||||||
|
Object.keys(byUri).forEach(key => {
|
||||||
|
const fileInfo = byUri[key]
|
||||||
|
if (fileInfo.outpoint == outpoint) {
|
||||||
|
fileInfos.push(fileInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return fileInfos
|
||||||
|
}
|
||||||
|
)
|
3
ui/js/selectors/rewards.js
Normal file
3
ui/js/selectors/rewards.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
|
export const _selectState = state => state.rewards || {}
|
34
ui/js/selectors/search.js
Normal file
34
ui/js/selectors/search.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
|
export const _selectState = state => state.search || {}
|
||||||
|
|
||||||
|
export const selectSearchQuery = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.query
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectIsSearching = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.searching
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectSearchResults = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.results || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectSearchResultsByQuery = createSelector(
|
||||||
|
selectSearchResults,
|
||||||
|
(results) => results.byQuery || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectCurrentSearchResults = createSelector(
|
||||||
|
selectSearchQuery,
|
||||||
|
selectSearchResultsByQuery,
|
||||||
|
(query, byQuery) => byQuery[query] || []
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectSearchActivated = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => !!state.activated
|
||||||
|
)
|
109
ui/js/selectors/wallet.js
Normal file
109
ui/js/selectors/wallet.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
selectDaemonReady,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export const _selectState = state => state.wallet || {}
|
||||||
|
|
||||||
|
export const selectBalance = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.balance || 0
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectTransactions = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.transactions || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectTransactionsById = createSelector(
|
||||||
|
selectTransactions,
|
||||||
|
(transactions) => transactions.byId || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectTransactionItems = createSelector(
|
||||||
|
selectTransactionsById,
|
||||||
|
(byId) => {
|
||||||
|
const transactionItems = []
|
||||||
|
const txids = Object.keys(byId)
|
||||||
|
txids.forEach((txid) => {
|
||||||
|
const tx = byId[txid]
|
||||||
|
transactionItems.push({
|
||||||
|
id: txid,
|
||||||
|
date: tx.timestamp ? (new Date(parseInt(tx.timestamp) * 1000)) : null,
|
||||||
|
amount: parseFloat(tx.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return transactionItems.reverse()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectIsFetchingTransactions = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.fetchingTransactions
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldFetchTransactions = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectTransactions,
|
||||||
|
selectIsFetchingTransactions,
|
||||||
|
(page, transactions, fetching) => {
|
||||||
|
if (page != 'wallet') return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (transactions.length != 0) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectReceiveAddress = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.receiveAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectGettingNewAddress = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.gettingNewAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldGetReceiveAddress = createSelector(
|
||||||
|
selectReceiveAddress,
|
||||||
|
selectGettingNewAddress,
|
||||||
|
selectDaemonReady,
|
||||||
|
(address, fetching, daemonReady) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (fetching) return false
|
||||||
|
if (address) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const shouldCheckAddressIsMine = createSelector(
|
||||||
|
_selectState,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectReceiveAddress,
|
||||||
|
selectDaemonReady,
|
||||||
|
(state, page, address, daemonReady) => {
|
||||||
|
if (!daemonReady) return false
|
||||||
|
if (address === undefined) return false
|
||||||
|
if (state.addressOwnershipChecked) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDraftTransaction = createSelector(
|
||||||
|
_selectState,
|
||||||
|
(state) => state.draftTransaction || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDraftTransactionAmount = createSelector(
|
||||||
|
selectDraftTransaction,
|
||||||
|
(draft) => draft.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectDraftTransactionAddress = createSelector(
|
||||||
|
selectDraftTransaction,
|
||||||
|
(draft) => draft.address
|
||||||
|
)
|
78
ui/js/store.js
Normal file
78
ui/js/store.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
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';
|
||||||
|
import availabilityReducer from 'reducers/availability'
|
||||||
|
import certificatesReducer from 'reducers/certificates'
|
||||||
|
import claimsReducer from 'reducers/claims'
|
||||||
|
import contentReducer from 'reducers/content';
|
||||||
|
import costInfoReducer from 'reducers/cost_info'
|
||||||
|
import fileInfoReducer from 'reducers/file_info'
|
||||||
|
import rewardsReducer from 'reducers/rewards'
|
||||||
|
import searchReducer from 'reducers/search'
|
||||||
|
import walletReducer from 'reducers/wallet'
|
||||||
|
|
||||||
|
function isFunction(object) {
|
||||||
|
return typeof object === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotFunction(object) {
|
||||||
|
return !isFunction(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBulkThunkMiddleware() {
|
||||||
|
return ({ dispatch, getState }) => next => (action) => {
|
||||||
|
if (action.type === 'BATCH_ACTIONS') {
|
||||||
|
action.actions.filter(isFunction).map(actionFn =>
|
||||||
|
actionFn(dispatch, getState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return next(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableBatching(reducer) {
|
||||||
|
return function batchingReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'BATCH_ACTIONS':
|
||||||
|
return action.actions.filter(isNotFunction).reduce(batchingReducer, state)
|
||||||
|
default:
|
||||||
|
return reducer(state, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducers = redux.combineReducers({
|
||||||
|
app: appReducer,
|
||||||
|
availability: availabilityReducer,
|
||||||
|
certificates: certificatesReducer,
|
||||||
|
claims: claimsReducer,
|
||||||
|
fileInfo: fileInfoReducer,
|
||||||
|
content: contentReducer,
|
||||||
|
costInfo: costInfoReducer,
|
||||||
|
rewards: rewardsReducer,
|
||||||
|
search: searchReducer,
|
||||||
|
wallet: walletReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bulkThunk = createBulkThunkMiddleware()
|
||||||
|
const middleware = [thunk, bulkThunk]
|
||||||
|
|
||||||
|
if (env === 'development') {
|
||||||
|
const logger = createLogger({
|
||||||
|
collapsed: true
|
||||||
|
});
|
||||||
|
middleware.push(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createStoreWithMiddleware = redux.compose(
|
||||||
|
redux.applyMiddleware(...middleware)
|
||||||
|
)(redux.createStore);
|
||||||
|
|
||||||
|
const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
|
||||||
|
|
||||||
|
export default reduxStore;
|
91
ui/js/triggers.js
Normal file
91
ui/js/triggers.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import {
|
||||||
|
shouldFetchTransactions,
|
||||||
|
shouldGetReceiveAddress,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import {
|
||||||
|
shouldFetchFeaturedContent,
|
||||||
|
shouldFetchDownloadedContent,
|
||||||
|
shouldFetchPublishedContent,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
shouldFetchCurrentUriFileInfo,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
shouldFetchCurrentUriCostInfo,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import {
|
||||||
|
shouldFetchCurrentUriAvailability,
|
||||||
|
} from 'selectors/availability'
|
||||||
|
import {
|
||||||
|
doFetchTransactions,
|
||||||
|
doGetNewAddress,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
doFetchFeaturedContent,
|
||||||
|
doFetchDownloadedContent,
|
||||||
|
doFetchPublishedContent,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
doFetchCurrentUriFileInfo,
|
||||||
|
} from 'actions/file_info'
|
||||||
|
import {
|
||||||
|
doFetchCurrentUriCostInfo,
|
||||||
|
} from 'actions/cost_info'
|
||||||
|
import {
|
||||||
|
doFetchCurrentUriAvailability,
|
||||||
|
} from 'actions/availability'
|
||||||
|
|
||||||
|
const triggers = []
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchTransactions,
|
||||||
|
action: doFetchTransactions,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldGetReceiveAddress,
|
||||||
|
action: doGetNewAddress
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchFeaturedContent,
|
||||||
|
action: doFetchFeaturedContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchDownloadedContent,
|
||||||
|
action: doFetchDownloadedContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchPublishedContent,
|
||||||
|
action: doFetchPublishedContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchCurrentUriFileInfo,
|
||||||
|
action: doFetchCurrentUriFileInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchCurrentUriCostInfo,
|
||||||
|
action: doFetchCurrentUriCostInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
triggers.push({
|
||||||
|
selector: shouldFetchCurrentUriAvailability,
|
||||||
|
action: doFetchCurrentUriAvailability,
|
||||||
|
})
|
||||||
|
|
||||||
|
const runTriggers = function() {
|
||||||
|
triggers.forEach(function(trigger) {
|
||||||
|
const state = app.store.getState();
|
||||||
|
const should = trigger.selector(state)
|
||||||
|
if (trigger.selector(state)) app.store.dispatch(trigger.action())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
triggers: triggers,
|
||||||
|
runTriggers: runTriggers
|
||||||
|
}
|
9
ui/js/util/batchActions.js
Normal file
9
ui/js/util/batchActions.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// https://github.com/reactjs/redux/issues/911
|
||||||
|
function batchActions(...actions) {
|
||||||
|
return {
|
||||||
|
type: 'BATCH_ACTIONS',
|
||||||
|
actions: actions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default batchActions
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue