Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
0bf24f3110
164 changed files with 7018 additions and 4002 deletions
|
@ -9,6 +9,10 @@ environment:
|
||||||
secure: 1mwqyRy7hDqDjDK+TIAoaXyXzpNgwruFNA6TPkinUcVM7A+NLD33RQLnfnwVy+R5ovD2pUfhQ6+N0Fqebv6tZh436LIEsock+6IOdpgFwrg=
|
secure: 1mwqyRy7hDqDjDK+TIAoaXyXzpNgwruFNA6TPkinUcVM7A+NLD33RQLnfnwVy+R5ovD2pUfhQ6+N0Fqebv6tZh436LIEsock+6IOdpgFwrg=
|
||||||
# find with: Get-Childitem –Path "C:\Program Files (x86)\Microsoft SDKs\Windows\" -Include *signtool* -File -Recurse -ErrorAction SilentlyContinue
|
# find with: Get-Childitem –Path "C:\Program Files (x86)\Microsoft SDKs\Windows\" -Include *signtool* -File -Recurse -ErrorAction SilentlyContinue
|
||||||
SIGNTOOL_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe
|
SIGNTOOL_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
secure: iVGwoJ7ogspjSmuqr+haVPLglSgQsp6tUZx6mIlKH7Q=
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
secure: zKaqdZGPl0exDL5YhJkb33prSemC9Rzg9S7Lw2wFy1WnJ6ffgl6mQH7jqJDUTqsY
|
||||||
|
|
||||||
skip_branch_with_pr: true
|
skip_branch_with_pr: true
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.10.0
|
current_version = 0.11.3
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,3 +14,5 @@ dist
|
||||||
|
|
||||||
*.pyc
|
*.pyc
|
||||||
.#*
|
.#*
|
||||||
|
|
||||||
|
build/daemon.zip
|
||||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -16,7 +16,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
||||||
*
|
*
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Error modals now display full screen properly
|
*
|
||||||
*
|
*
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
@ -27,6 +27,38 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|
||||||
|
## [0.11.3] - 2017-05-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed always showing welcome message on run
|
||||||
|
* "Fixed" upgrade process
|
||||||
|
* Version info now shows properly on Help page
|
||||||
|
* Claim info is properly accessed on Publish page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.11.0] - 2017-05-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Entire app re-written to use Redux as state store. Far saner and faster. Will also increase productivity moving forward.
|
||||||
|
* Channel page shows content published in channel.
|
||||||
|
* URI handling. Clicking lbry:// links should open the app and appropriate URI on all OSes.
|
||||||
|
* File cards have an icon indicating you posses that file.
|
||||||
|
* Download directory setting now uses a proper dialog.
|
||||||
|
* Movie player automatically shows if the file has already been downloaded.
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Plyr replaces mediaelement as the movie player.
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Publisher indicator on show pages and file cards/tiles will now always show the proper channel name.
|
||||||
|
* Performance improvements related to avoiding duplicate fetches.
|
||||||
|
* Fix incorrect prompt on empty published page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.10.0] - 2017-05-04
|
## [0.10.0] - 2017-05-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
17
app/main.js
17
app/main.js
|
@ -1,5 +1,11 @@
|
||||||
const {app, BrowserWindow, ipcMain} = require('electron');
|
const {app, BrowserWindow, ipcMain} = require('electron');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
const isDebug = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
require('electron-debug')({showDevTools: true});
|
||||||
|
}
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const jayson = require('jayson');
|
const jayson = require('jayson');
|
||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
|
@ -62,23 +68,20 @@ function checkForNewVersion(callback) {
|
||||||
'User-Agent': `LBRY/${localVersion}`,
|
'User-Agent': `LBRY/${localVersion}`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = https.get(Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), (res) => {
|
const req = https.get(Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), (res) => {
|
||||||
res.on('data', (data) => {
|
res.on('data', (data) => {
|
||||||
result += data;
|
result += data;
|
||||||
});
|
});
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
console.log('Local version:', localVersion);
|
|
||||||
const tagName = JSON.parse(result).tag_name;
|
const tagName = JSON.parse(result).tag_name;
|
||||||
const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/);
|
const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/);
|
||||||
if (!remoteVersion) {
|
if (!remoteVersion) {
|
||||||
console.log('Malformed remote version string:', tagName);
|
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send('version-info-received', null);
|
win.webContents.send('version-info-received', null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Remote version:', remoteVersion);
|
|
||||||
const upgradeAvailable = semver.gt(formatRc(remoteVersion), formatRc(localVersion));
|
const upgradeAvailable = semver.gt(formatRc(remoteVersion), formatRc(localVersion));
|
||||||
console.log(upgradeAvailable ? 'Upgrade available' : 'No upgrade available');
|
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable});
|
win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable});
|
||||||
}
|
}
|
||||||
|
@ -138,7 +141,9 @@ function createWindow () {
|
||||||
win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
|
win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
|
||||||
|
|
||||||
win.maximize()
|
win.maximize()
|
||||||
// win.webContents.openDevTools();
|
if (isDebug) {
|
||||||
|
win.webContents.openDevTools();
|
||||||
|
}
|
||||||
win.loadURL(`file://${__dirname}/dist/index.html`)
|
win.loadURL(`file://${__dirname}/dist/index.html`)
|
||||||
if (openUri) { // We stored and received a URI that an external app requested before we had a window object
|
if (openUri) { // We stored and received a URI that an external app requested before we had a window object
|
||||||
win.webContents.on('did-finish-load', () => {
|
win.webContents.on('did-finish-load', () => {
|
||||||
|
@ -312,7 +317,7 @@ app.on('activate', () => {
|
||||||
// then calls quitNow() to quit for real.
|
// then calls quitNow() to quit for real.
|
||||||
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
|
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
|
||||||
function doShutdown() {
|
function doShutdown() {
|
||||||
console.log('Asking daemon to shut down down');
|
console.log('Shutting down daemon');
|
||||||
daemonStopRequested = true;
|
daemonStopRequested = true;
|
||||||
client.request('daemon_stop', [], (err, res) => {
|
client.request('daemon_stop', [], (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "LBRY",
|
"name": "LBRY",
|
||||||
"version": "0.10.0",
|
"version": "0.11.3",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
|
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
|
||||||
"author": {
|
"author": {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
https://github.com/lbryio/lbry/releases/download/v0.10.3rc1/lbrynet-daemon-v0.10.3rc1-OSNAME.zip
|
https://github.com/lbryio/lbry/releases/download/v0.10.3/lbrynet-daemon-v0.10.3-OSNAME.zip
|
||||||
|
|
|
@ -37,4 +37,4 @@ nuget install secure-file -ExcludeVersion
|
||||||
secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key"
|
secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key"
|
||||||
& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe
|
& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe
|
||||||
|
|
||||||
python build\release_on_tag.py
|
python build\upload_assets.py
|
|
@ -107,7 +107,7 @@ if [ "$FULL_BUILD" == "true" ]; then
|
||||||
# electron-build has a publish feature, but I had a hard time getting
|
# electron-build has a publish feature, but I had a hard time getting
|
||||||
# it to reliably work and it also seemed difficult to configure. Not proud of
|
# it to reliably work and it also seemed difficult to configure. Not proud of
|
||||||
# this, but it seemed better to write my own.
|
# this, but it seemed better to write my own.
|
||||||
python "$BUILD_DIR/release_on_tag.py"
|
python "$BUILD_DIR/upload_assets.py"
|
||||||
|
|
||||||
deactivate
|
deactivate
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,7 @@ def check_url(url):
|
||||||
if new_location == url:
|
if new_location == url:
|
||||||
# self-loop
|
# self-loop
|
||||||
return False
|
return False
|
||||||
if "github-cloud.s3.amazonaws.com/releases" in new_location:
|
if "amazonaws.com" in new_location:
|
||||||
# HEAD doesnt work on s3 links, so assume its good
|
# HEAD doesnt work on s3 links, so assume its good
|
||||||
return True
|
return True
|
||||||
return check_url(new_location)
|
return check_url(new_location)
|
||||||
|
|
|
@ -4,3 +4,4 @@ requests[security]==2.13.0
|
||||||
PyInstaller==3.2.1
|
PyInstaller==3.2.1
|
||||||
uritemplate==3.0.0
|
uritemplate==3.0.0
|
||||||
git+https://github.com/lbryio/bumpversion.git
|
git+https://github.com/lbryio/bumpversion.git
|
||||||
|
boto3==1.4.4
|
|
@ -6,52 +6,16 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import github
|
import github
|
||||||
import requests
|
|
||||||
import uritemplate
|
import uritemplate
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
upload_to_github_if_tagged('lbryio/lbry-app')
|
||||||
current_tag = subprocess.check_output(
|
upload_to_s3('app')
|
||||||
['git', 'describe', '--exact-match', 'HEAD']).strip()
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print 'Stopping as we are not currently on a tag'
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'GH_TOKEN' not in os.environ:
|
|
||||||
print 'Must set GH_TOKEN in order to publish assets to a release'
|
|
||||||
return
|
|
||||||
|
|
||||||
gh_token = os.environ['GH_TOKEN']
|
|
||||||
auth = github.Github(gh_token)
|
|
||||||
repo = auth.get_repo('lbryio/lbry-app')
|
|
||||||
|
|
||||||
if not check_repo_has_tag(repo, current_tag):
|
|
||||||
print 'Tag {} is not in repo {}'.format(current_tag, repo)
|
|
||||||
# TODO: maybe this should be an error
|
|
||||||
return
|
|
||||||
|
|
||||||
app = get_app_artifact()
|
|
||||||
release = get_release(repo, current_tag)
|
|
||||||
upload_asset(release, app, gh_token)
|
|
||||||
|
|
||||||
|
|
||||||
def check_repo_has_tag(repo, target_tag):
|
def get_asset_filename():
|
||||||
tags = repo.get_tags().get_page(0)
|
|
||||||
for tag in tags:
|
|
||||||
if tag.name == target_tag:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_release(current_repo, current_tag):
|
|
||||||
for release in current_repo.get_releases():
|
|
||||||
if release.tag_name == current_tag:
|
|
||||||
return release
|
|
||||||
raise Exception('No release for {} was found'.format(current_tag))
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_artifact():
|
|
||||||
this_dir = os.path.dirname(os.path.realpath(__file__))
|
this_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == 'Darwin':
|
if system == 'Darwin':
|
||||||
|
@ -64,38 +28,93 @@ def get_app_artifact():
|
||||||
raise Exception("I don't know about any artifact on {}".format(system))
|
raise Exception("I don't know about any artifact on {}".format(system))
|
||||||
|
|
||||||
|
|
||||||
def upload_asset(release, asset_to_upload, token):
|
def upload_to_s3(folder):
|
||||||
|
tag = subprocess.check_output(['git', 'describe', '--always', 'HEAD']).strip()
|
||||||
|
commit_date = subprocess.check_output([
|
||||||
|
'git', 'show', '-s', '--format=%cd', '--date=format:%Y%m%d-%H%I%S', 'HEAD']).strip()
|
||||||
|
|
||||||
|
asset_path = get_asset_filename()
|
||||||
|
bucket = 'releases.lbry.io'
|
||||||
|
key = folder + '/' + commit_date + '-' + tag + '/' + os.path.basename(asset_path)
|
||||||
|
|
||||||
|
print "Uploading " + asset_path + " to s3://" + bucket + '/' + key + ''
|
||||||
|
|
||||||
|
if 'AWS_ACCESS_KEY_ID' not in os.environ or 'AWS_SECRET_ACCESS_KEY' not in os.environ:
|
||||||
|
print 'Must set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to publish assets to s3'
|
||||||
|
return 1
|
||||||
|
|
||||||
|
s3 = boto3.resource(
|
||||||
|
's3',
|
||||||
|
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
|
||||||
|
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
|
||||||
|
config=boto3.session.Config(signature_version='s3v4')
|
||||||
|
)
|
||||||
|
s3.meta.client.upload_file(asset_path, bucket, key)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_github_if_tagged(repo_name):
|
||||||
|
try:
|
||||||
|
current_tag = subprocess.check_output(
|
||||||
|
['git', 'describe', '--exact-match', 'HEAD']).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print 'Not uploading to GitHub as we are not currently on a tag'
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print "Current tag: " + current_tag
|
||||||
|
|
||||||
|
if 'GH_TOKEN' not in os.environ:
|
||||||
|
print 'Must set GH_TOKEN in order to publish assets to a release'
|
||||||
|
return 1
|
||||||
|
|
||||||
|
gh_token = os.environ['GH_TOKEN']
|
||||||
|
auth = github.Github(gh_token)
|
||||||
|
repo = auth.get_repo(repo_name)
|
||||||
|
|
||||||
|
if not check_repo_has_tag(repo, current_tag):
|
||||||
|
print 'Tag {} is not in repo {}'.format(current_tag, repo)
|
||||||
|
# TODO: maybe this should be an error
|
||||||
|
return 1
|
||||||
|
|
||||||
|
asset_path = get_asset_filename()
|
||||||
|
print "Uploading " + asset_path + " to Github tag " + current_tag
|
||||||
|
release = get_github_release(repo, current_tag)
|
||||||
|
upload_asset_to_github(release, asset_path, gh_token)
|
||||||
|
|
||||||
|
|
||||||
|
def check_repo_has_tag(repo, target_tag):
|
||||||
|
tags = repo.get_tags().get_page(0)
|
||||||
|
for tag in tags:
|
||||||
|
if tag.name == target_tag:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_release(repo, current_tag):
|
||||||
|
for release in repo.get_releases():
|
||||||
|
if release.tag_name == current_tag:
|
||||||
|
return release
|
||||||
|
raise Exception('No release for {} was found'.format(current_tag))
|
||||||
|
|
||||||
|
|
||||||
|
def upload_asset_to_github(release, asset_to_upload, token):
|
||||||
basename = os.path.basename(asset_to_upload)
|
basename = os.path.basename(asset_to_upload)
|
||||||
if is_asset_already_uploaded(release, basename):
|
for asset in release.raw_data['assets']:
|
||||||
|
if asset['name'] == basename:
|
||||||
|
print 'File {} has already been uploaded to {}'.format(basename, release.tag_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
upload_uri = uritemplate.expand(release.upload_url, {'name': basename})
|
||||||
count = 0
|
count = 0
|
||||||
while count < 10:
|
while count < 10:
|
||||||
try:
|
try:
|
||||||
return _upload_asset(release, asset_to_upload, token, _curl_uploader)
|
output = _curl_uploader(upload_uri, asset_to_upload, token)
|
||||||
except Exception:
|
|
||||||
print 'Failed uploading on attempt {}'.format(count + 1)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
|
|
||||||
def _upload_asset(release, asset_to_upload, token, uploader):
|
|
||||||
basename = os.path.basename(asset_to_upload)
|
|
||||||
upload_uri = uritemplate.expand(release.upload_url, {'name': basename})
|
|
||||||
output = uploader(upload_uri, asset_to_upload, token)
|
|
||||||
if 'errors' in output:
|
if 'errors' in output:
|
||||||
raise Exception(output)
|
raise Exception(output)
|
||||||
else:
|
else:
|
||||||
print 'Successfully uploaded to {}'.format(output['browser_download_url'])
|
print 'Successfully uploaded to {}'.format(output['browser_download_url'])
|
||||||
|
except Exception:
|
||||||
|
print 'Failed uploading on attempt {}'.format(count + 1)
|
||||||
# requests doesn't work on windows / linux / osx.
|
count += 1
|
||||||
def _requests_uploader(upload_uri, asset_to_upload, token):
|
|
||||||
print 'Using requests to upload {} to {}'.format(asset_to_upload, upload_uri)
|
|
||||||
with open(asset_to_upload, 'rb') as f:
|
|
||||||
response = requests.post(upload_uri, data=f, auth=('', token))
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
# curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login
|
|
||||||
|
|
||||||
|
|
||||||
def _curl_uploader(upload_uri, asset_to_upload, token):
|
def _curl_uploader(upload_uri, asset_to_upload, token):
|
||||||
|
@ -129,13 +148,5 @@ def _curl_uploader(upload_uri, asset_to_upload, token):
|
||||||
return json.loads(stdout)
|
return json.loads(stdout)
|
||||||
|
|
||||||
|
|
||||||
def is_asset_already_uploaded(release, basename):
|
|
||||||
for asset in release.raw_data['assets']:
|
|
||||||
if asset['name'] == basename:
|
|
||||||
print 'File {} has already been uploaded to {}'.format(basename, release.tag_name)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
1
lbry
1
lbry
|
@ -1 +0,0 @@
|
||||||
Subproject commit d99fc519b56ee910a44ef4af668b0770e9430d12
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 5c2441fa13e39ba7280292519041e14ec696d753
|
|
1
lbryum
1
lbryum
|
@ -1 +0,0 @@
|
||||||
Subproject commit 950b95aa7e45a2c15b269d807f6ff8e16bae4304
|
|
|
@ -53,7 +53,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"
|
||||||
"dependencies": {}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
ui/dist/index.html
vendored
2
ui/dist/index.html
vendored
|
@ -7,7 +7,7 @@
|
||||||
<link href='https://fonts.googleapis.com/css?family=Raleway:600,300' rel='stylesheet' type='text/css'>
|
<link href='https://fonts.googleapis.com/css?family=Raleway:600,300' rel='stylesheet' type='text/css'>
|
||||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600italic,600' rel='stylesheet' type='text/css'>
|
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600italic,600' rel='stylesheet' type='text/css'>
|
||||||
<link href="./css/all.css" rel="stylesheet" type="text/css" media="screen,print" />
|
<link href="./css/all.css" rel="stylesheet" type="text/css" media="screen,print" />
|
||||||
<link href="./js/mediaelement/mediaelementplayer.css" rel="stylesheet" type="text/css" />
|
<link rel="stylesheet" href="https://cdn.plyr.io/2.0.12/plyr.css">
|
||||||
<link rel="icon" type="image/png" href="./img/fav/favicon-32x32.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="./img/fav/favicon-32x32.png" sizes="32x32">
|
||||||
<link rel="icon" type="image/png" href="./img/fav/favicon-194x194.png" sizes="194x194">
|
<link rel="icon" type="image/png" href="./img/fav/favicon-194x194.png" sizes="194x194">
|
||||||
<link rel="icon" type="image/png" href="./img/fav/favicon-96x96.png" sizes="96x96">
|
<link rel="icon" type="image/png" href="./img/fav/favicon-96x96.png" sizes="96x96">
|
||||||
|
|
240
ui/js/actions/app.js
Normal file
240
ui/js/actions/app.js
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectUpdateUrl,
|
||||||
|
selectUpgradeDownloadDir,
|
||||||
|
selectUpgradeDownloadItem,
|
||||||
|
selectUpgradeFilename,
|
||||||
|
selectPageTitle,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentParams,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doSearch,
|
||||||
|
} from 'actions/search'
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
const queryStringFromParams = (params) => {
|
||||||
|
return Object
|
||||||
|
.keys(params)
|
||||||
|
.map(key => `${key}=${params[key]}`)
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doNavigate(path, params = {}) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
let url = path
|
||||||
|
if (params)
|
||||||
|
url = `${url}?${queryStringFromParams(params)}`
|
||||||
|
|
||||||
|
dispatch(doChangePath(url))
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const pageTitle = selectPageTitle(state)
|
||||||
|
dispatch(doHistoryPush(params, pageTitle, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doChangePath(path) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.CHANGE_PATH,
|
||||||
|
data: {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const pageTitle = selectPageTitle(state)
|
||||||
|
window.document.title = pageTitle
|
||||||
|
|
||||||
|
const currentPage = selectCurrentPage(state)
|
||||||
|
if (currentPage === 'search') {
|
||||||
|
const params = selectCurrentParams(state)
|
||||||
|
dispatch(doSearch(params.query))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doHistoryBack() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doHistoryPush(params, title, relativeUrl) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
let pathParts = window.location.pathname.split('/')
|
||||||
|
pathParts[pathParts.length - 1] = relativeUrl.replace(/^\//, '')
|
||||||
|
const url = pathParts.join('/')
|
||||||
|
title += " - LBRY"
|
||||||
|
history.pushState(params, title, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getAppVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
|
||||||
|
if (upgradeAvailable) {
|
||||||
|
dispatch({
|
||||||
|
type: types.UPDATE_VERSION,
|
||||||
|
data: {
|
||||||
|
version: remoteVersion,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dispatch({
|
||||||
|
type: types.OPEN_MODAL,
|
||||||
|
data: {
|
||||||
|
modal: 'upgrade'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doAlertError(errorList) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
console.log('do alert error')
|
||||||
|
console.log(errorList)
|
||||||
|
dispatch({
|
||||||
|
type: types.OPEN_MODAL,
|
||||||
|
data: {
|
||||||
|
modal: 'error',
|
||||||
|
extraContent: errorList
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doDaemonReady() {
|
||||||
|
return {
|
||||||
|
type: types.DAEMON_READY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doShowSnackBar(data) {
|
||||||
|
return {
|
||||||
|
type: types.SHOW_SNACKBAR,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doRemoveSnackBarSnack() {
|
||||||
|
return {
|
||||||
|
type: types.REMOVE_SNACKBAR_SNACK,
|
||||||
|
}
|
||||||
|
}
|
29
ui/js/actions/availability.js
Normal file
29
ui/js/actions/availability.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
selectFetchingAvailability
|
||||||
|
} from 'selectors/availability'
|
||||||
|
|
||||||
|
export function doFetchAvailability(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const alreadyFetching = !!selectFetchingAvailability(state)[uri]
|
||||||
|
|
||||||
|
if (!alreadyFetching) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_AVAILABILITY_STARTED,
|
||||||
|
data: {uri}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.get_availability({uri}).then((availability) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_AVAILABILITY_COMPLETED,
|
||||||
|
data: {
|
||||||
|
availability,
|
||||||
|
uri,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
294
ui/js/actions/content.js
Normal file
294
ui/js/actions/content.js
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import lbryio from 'lbryio'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import rewards from 'rewards'
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import {
|
||||||
|
selectFileInfoForUri,
|
||||||
|
selectUrisDownloading,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectResolvingUris
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectCostInfoForUri,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import {
|
||||||
|
selectClaimsByUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
doOpenModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
|
||||||
|
export function doResolveUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
|
||||||
|
uri = lbryuri.normalize(uri)
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1
|
||||||
|
|
||||||
|
if (!alreadyResolving) {
|
||||||
|
dispatch({
|
||||||
|
type: types.RESOLVE_URI_STARTED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.resolve({ uri }).then((resolutionInfo) => {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
certificate,
|
||||||
|
} = resolutionInfo ? resolutionInfo : { claim : null, certificate: null }
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.RESOLVE_URI_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
claim,
|
||||||
|
certificate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCancelResolveUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
lbry.cancelResolve({ uri })
|
||||||
|
dispatch({
|
||||||
|
type: types.RESOLVE_URI_CANCELED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchFeaturedUris() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FEATURED_CONTENT_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
const success = ({ Categories, Uris }) => {
|
||||||
|
|
||||||
|
let featuredUris = {}
|
||||||
|
|
||||||
|
Categories.forEach((category) => {
|
||||||
|
if (Uris[category] && Uris[category].length) {
|
||||||
|
featuredUris[category] = Uris[category]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
categories: Categories,
|
||||||
|
uris: featuredUris,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const failure = () => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
categories: [],
|
||||||
|
uris: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
outpoint,
|
||||||
|
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,
|
||||||
|
outpoint,
|
||||||
|
fileInfo,
|
||||||
|
progress,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
outpoint: streamInfo.outpoint,
|
||||||
|
fileInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
lbryio.call('file', 'view', {
|
||||||
|
uri: uri,
|
||||||
|
outpoint: streamInfo.outpoint,
|
||||||
|
claimId: streamInfo.claim_id,
|
||||||
|
}).catch(() => {})
|
||||||
|
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doLoadVideo(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.LOADING_VIDEO_STARTED,
|
||||||
|
data: {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.get({ uri }).then(streamInfo => {
|
||||||
|
const timeout = streamInfo === null ||
|
||||||
|
typeof streamInfo !== 'object' ||
|
||||||
|
streamInfo.error == 'Timeout'
|
||||||
|
|
||||||
|
if(timeout) {
|
||||||
|
dispatch({
|
||||||
|
type: types.LOADING_VIDEO_FAILED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
dispatch(doOpenModal('timedOut'))
|
||||||
|
} else {
|
||||||
|
dispatch(doDownloadFile(uri, streamInfo))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doPurchaseUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const balance = selectBalance(state)
|
||||||
|
const fileInfo = selectFileInfoForUri(state, { uri })
|
||||||
|
const costInfo = selectCostInfoForUri(state, { uri })
|
||||||
|
const downloadingByUri = selectUrisDownloading(state)
|
||||||
|
const alreadyDownloading = !!downloadingByUri[uri]
|
||||||
|
const { cost } = costInfo
|
||||||
|
|
||||||
|
// BUG if you delete a file from the file system system you're going to be
|
||||||
|
// asked to pay for it again. We need to check if the file is in the blobs
|
||||||
|
// here and then dispatch doLoadVideo() which will reconstruct it again from
|
||||||
|
// the blobs. Or perhaps there's another way to see if a file was already
|
||||||
|
// purchased?
|
||||||
|
|
||||||
|
// we already fully downloaded the file. If completed is true but
|
||||||
|
// writtenBytes is false then we downloaded it before but deleted it again,
|
||||||
|
// which means it needs to be reconstructed from the blobs by dispatching
|
||||||
|
// doLoadVideo.
|
||||||
|
if (fileInfo && fileInfo.completed && !!fileInfo.writtenBytes) {
|
||||||
|
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 && fileInfo.download_directory)) {
|
||||||
|
dispatch(doLoadVideo(uri))
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cost > balance) {
|
||||||
|
dispatch(doOpenModal('notEnoughCredits'))
|
||||||
|
} else {
|
||||||
|
dispatch(doOpenModal('affirmPurchase'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchClaimsByChannel(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_CHANNEL_CLAIMS_STARTED,
|
||||||
|
data: { uri }
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.resolve({ uri }).then((resolutionInfo) => {
|
||||||
|
const {
|
||||||
|
claims_in_channel,
|
||||||
|
} = resolutionInfo ? resolutionInfo : { claims_in_channel: [] }
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
claims: claims_in_channel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doClaimListMine() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch({
|
||||||
|
type: types.CLAIM_LIST_MINE_STARTED
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
lbry.claim_list_mine().then((claims) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.CLAIM_LIST_MINE_COMPLETED,
|
||||||
|
data: {
|
||||||
|
claims
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
77
ui/js/actions/cost_info.js
Normal file
77
ui/js/actions/cost_info.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import lbryio from 'lbryio'
|
||||||
|
import {
|
||||||
|
doResolveUri
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
selectResolvingUris,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
selectClaimsByUri
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
selectSettingsIsGenerous
|
||||||
|
} from 'selectors/settings'
|
||||||
|
|
||||||
|
export function doFetchCostInfoForUri(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState(),
|
||||||
|
claim = selectClaimsByUri(state)[uri],
|
||||||
|
isResolving = selectResolvingUris(state).indexOf(uri) !== -1,
|
||||||
|
isGenerous = selectSettingsIsGenerous(state)
|
||||||
|
|
||||||
|
if (claim === null) { //claim doesn't exist, nothing to fetch a cost for
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(doFetchCostInfoForUri(uri))
|
||||||
|
}, 1000)
|
||||||
|
if (!isResolving) {
|
||||||
|
dispatch(doResolveUri(uri))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function begin() {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_COST_INFO_STARTED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(costInfo) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_COST_INFO_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uri,
|
||||||
|
costInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGenerous && claim) {
|
||||||
|
let cost
|
||||||
|
const fee = claim.value.stream.metadata.fee;
|
||||||
|
if (fee === undefined ) {
|
||||||
|
resolve({ cost: 0, includesData: true })
|
||||||
|
} else if (fee.currency == 'LBC') {
|
||||||
|
resolve({ cost: fee.amount, includesData: true })
|
||||||
|
} else {
|
||||||
|
begin()
|
||||||
|
lbryio.getExchangeRates().then(({lbc_usd}) => {
|
||||||
|
resolve({ cost: fee.amount / lbc_usd, includesData: true })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
begin()
|
||||||
|
lbry.getCostInfo(uri).then(resolve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
121
ui/js/actions/file_info.js
Normal file
121
ui/js/actions/file_info.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
doClaimListMine
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectClaimListMineIsPending,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
selectFileListIsPending,
|
||||||
|
selectAllFileInfos,
|
||||||
|
selectUrisLoading,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
|
||||||
|
const {
|
||||||
|
shell,
|
||||||
|
} = require('electron')
|
||||||
|
|
||||||
|
export function doFetchFileInfo(uri) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const claim = selectClaimsByUri(state)[uri]
|
||||||
|
const outpoint = claim ? `${claim.txid}:${claim.nout}` : null
|
||||||
|
const alreadyFetching = !!selectUrisLoading(state)[uri]
|
||||||
|
|
||||||
|
if (!alreadyFetching) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FILE_INFO_STARTED,
|
||||||
|
data: {
|
||||||
|
outpoint,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.file_list({outpoint: outpoint, full_status: true}).then(fileInfos => {
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FETCH_FILE_INFO_COMPLETED,
|
||||||
|
data: {
|
||||||
|
outpoint,
|
||||||
|
fileInfo: fileInfos && fileInfos.length ? fileInfos[0] : null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFileList() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const isPending = selectFileListIsPending(state)
|
||||||
|
|
||||||
|
if (!isPending) {
|
||||||
|
dispatch({
|
||||||
|
type: types.FILE_LIST_STARTED,
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.file_list().then((fileInfos) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.FILE_LIST_COMPLETED,
|
||||||
|
data: {
|
||||||
|
fileInfos,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(outpoint, deleteFromComputer) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: types.FILE_DELETE,
|
||||||
|
data: {
|
||||||
|
outpoint
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lbry.file_delete({
|
||||||
|
outpoint: outpoint,
|
||||||
|
delete_target_file: deleteFromComputer,
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(doCloseModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function doFetchFileInfosAndPublishedClaims() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState(),
|
||||||
|
isClaimListMinePending = selectClaimListMineIsPending(state),
|
||||||
|
isFileInfoListPending = selectFileListIsPending(state)
|
||||||
|
|
||||||
|
if (isClaimListMinePending === undefined) {
|
||||||
|
dispatch(doClaimListMine())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFileInfoListPending === undefined) {
|
||||||
|
dispatch(doFileList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
ui/js/actions/search.js
Normal file
54
ui/js/actions/search.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import lighthouse from 'lighthouse'
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
doHistoryPush
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
} from 'selectors/app'
|
||||||
|
|
||||||
|
export function doSearch(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 != 'search') {
|
||||||
|
dispatch(doNavigate('search', { query: query }))
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
ui/js/actions/settings.js
Normal file
31
ui/js/actions/settings.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as types from 'constants/action_types'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
|
||||||
|
export function doFetchDaemonSettings() {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
lbry.get_settings().then((settings) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.DAEMON_SETTINGS_RECEIVED,
|
||||||
|
data: {
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetDaemonSetting(key, value) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
let settings = {};
|
||||||
|
settings[key] = value;
|
||||||
|
lbry.settings_set(settings).then(settings)
|
||||||
|
lbry.get_settings().then((settings) => {
|
||||||
|
dispatch({
|
||||||
|
type: types.DAEMON_SETTINGS_RECEIVED,
|
||||||
|
data: {
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
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 }
|
||||||
|
}
|
||||||
|
}
|
347
ui/js/app.js
347
ui/js/app.js
|
@ -1,335 +1,18 @@
|
||||||
import React from 'react';
|
import store from 'store.js';
|
||||||
import {Line} from 'rc-progress';
|
|
||||||
|
|
||||||
import lbry from './lbry.js';
|
const env = ENV;
|
||||||
import SettingsPage from './page/settings.js';
|
const config = require(`./config/${env}`);
|
||||||
import HelpPage from './page/help.js';
|
const logs = [];
|
||||||
import WatchPage from './page/watch.js';
|
const app = {
|
||||||
import ReportPage from './page/report.js';
|
env: env,
|
||||||
import StartPage from './page/start.js';
|
config: config,
|
||||||
import RewardsPage from './page/rewards.js';
|
store: store,
|
||||||
import RewardPage from './page/reward.js';
|
logs: logs,
|
||||||
import WalletPage from './page/wallet.js';
|
log: function(message) {
|
||||||
import ShowPage from './page/show.js';
|
console.log(message);
|
||||||
import PublishPage from './page/publish.js';
|
logs.push(message);
|
||||||
import SearchPage from './page/search.js';
|
|
||||||
import DiscoverPage from './page/discover.js';
|
|
||||||
import DeveloperPage from './page/developer.js';
|
|
||||||
import lbryuri from './lbryuri.js';
|
|
||||||
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
|
|
||||||
import Header from './component/header.js';
|
|
||||||
import {Modal, ExpandableModal} from './component/modal.js';
|
|
||||||
import {Link} from './component/link.js';
|
|
||||||
|
|
||||||
|
|
||||||
const {remote, ipcRenderer, shell} = require('electron');
|
|
||||||
const {download} = remote.require('electron-dl');
|
|
||||||
const path = require('path');
|
|
||||||
const app = require('electron').remote.app;
|
|
||||||
const fs = remote.require('fs');
|
|
||||||
|
|
||||||
|
|
||||||
var App = React.createClass({
|
|
||||||
_error_key_labels: {
|
|
||||||
connectionString: 'API connection string',
|
|
||||||
method: 'Method',
|
|
||||||
params: 'Parameters',
|
|
||||||
code: 'Error code',
|
|
||||||
message: 'Error message',
|
|
||||||
data: 'Error data',
|
|
||||||
},
|
|
||||||
_fullScreenPages: ['watch'],
|
|
||||||
_storeHistoryOfNextRender: false,
|
|
||||||
|
|
||||||
_upgradeDownloadItem: null,
|
|
||||||
_isMounted: false,
|
|
||||||
_version: null,
|
|
||||||
getUpdateUrl: function() {
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'darwin':
|
|
||||||
return 'https://lbry.io/get/lbry.dmg';
|
|
||||||
case 'linux':
|
|
||||||
return 'https://lbry.io/get/lbry.deb';
|
|
||||||
case 'win32':
|
|
||||||
return 'https://lbry.io/get/lbry.exe';
|
|
||||||
default:
|
|
||||||
throw 'Unknown platform';
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// Hard code the filenames as a temporary workaround, because
|
|
||||||
// electron-dl throws errors when you try to get the filename
|
|
||||||
getUpgradeFilename: function() {
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'darwin':
|
|
||||||
return `LBRY-${this._version}.dmg`;
|
|
||||||
case 'linux':
|
|
||||||
return `LBRY_${this._version}_amd64.deb`;
|
|
||||||
case 'windows':
|
|
||||||
return `LBRY.Setup.${this._version}.exe`;
|
|
||||||
default:
|
|
||||||
throw 'Unknown platform';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getViewingPageAndArgs: function(address) {
|
|
||||||
// For now, routes are in format ?page or ?page=args
|
|
||||||
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
|
|
||||||
return {
|
|
||||||
viewingPage: viewingPage,
|
|
||||||
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
|
|
||||||
viewingPage: 'discover',
|
|
||||||
appUrl: null,
|
|
||||||
errorInfo: null,
|
|
||||||
modal: null,
|
|
||||||
downloadProgress: null,
|
|
||||||
downloadComplete: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
global.app = app;
|
||||||
if ('openUri' in this.props) { // A URI was requested by an external app
|
module.exports = app;
|
||||||
this.showUri(this.props.openUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("popstate", this.onHistoryPop);
|
|
||||||
|
|
||||||
document.addEventListener('unhandledError', (event) => {
|
|
||||||
this.alertError(event.detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on('open-uri-requested', (event, uri) => {
|
|
||||||
this.showUri(uri);
|
|
||||||
});
|
|
||||||
|
|
||||||
//open links in external browser and skip full redraw on changing page
|
|
||||||
document.addEventListener('click', (event) => {
|
|
||||||
var target = event.target;
|
|
||||||
while (target && target !== document) {
|
|
||||||
if (target.matches('a[href^="http"]')) {
|
|
||||||
event.preventDefault();
|
|
||||||
shell.openExternal(target.href);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.matches('a[href^="?"]')) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (this._isMounted) {
|
|
||||||
let appUrl = target.getAttribute('href');
|
|
||||||
this._storeHistoryOfNextRender = true;
|
|
||||||
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
|
|
||||||
document.body.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target = target.parentNode;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionStorage.getItem('upgradeSkipped')) {
|
|
||||||
lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
|
|
||||||
if (upgradeAvailable) {
|
|
||||||
this._version = remoteVersion;
|
|
||||||
this.setState({
|
|
||||||
modal: 'upgrade',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeModal: function() {
|
|
||||||
this.setState({
|
|
||||||
modal: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
|
||||||
if ('openUri' in nextProps && (!('openUri' in this.props) || nextProps.openUri != this.props.openUri)) {
|
|
||||||
this.showUri(nextProps.openUri);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
window.removeEventListener("popstate", this.onHistoryPop);
|
|
||||||
},
|
|
||||||
onHistoryPop: function() {
|
|
||||||
this.setState(this.getViewingPageAndArgs(location.search));
|
|
||||||
},
|
|
||||||
onSearch: function(term) {
|
|
||||||
this._storeHistoryOfNextRender = true;
|
|
||||||
const isShow = term.startsWith('lbry://');
|
|
||||||
this.setState({
|
|
||||||
viewingPage: isShow ? "show" : "search",
|
|
||||||
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
|
|
||||||
pageArgs: term
|
|
||||||
});
|
|
||||||
},
|
|
||||||
showUri: function(uri) {
|
|
||||||
this._storeHistoryOfNextRender = true;
|
|
||||||
this.setState({
|
|
||||||
address: uri,
|
|
||||||
appUrl: "?show=" + encodeURIComponent(uri),
|
|
||||||
viewingPage: "show",
|
|
||||||
pageArgs: uri,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleUpgradeClicked: function() {
|
|
||||||
// Make a new directory within temp directory so the filename is guaranteed to be available
|
|
||||||
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
|
|
||||||
directory: dir,
|
|
||||||
};
|
|
||||||
download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
|
|
||||||
.then(downloadItem => {
|
|
||||||
/**
|
|
||||||
* TODO: get the download path directly from the download object. It should just be
|
|
||||||
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
|
|
||||||
* too soon.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this._upgradeDownloadItem = downloadItem;
|
|
||||||
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
|
|
||||||
this.setState({
|
|
||||||
downloadComplete: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.setState({modal: 'downloading'});
|
|
||||||
},
|
|
||||||
handleStartUpgradeClicked: function() {
|
|
||||||
ipcRenderer.send('upgrade', this._upgradeDownloadPath);
|
|
||||||
},
|
|
||||||
cancelUpgrade: function() {
|
|
||||||
if (this._upgradeDownloadItem) {
|
|
||||||
/*
|
|
||||||
* Right now the remote reference to the download item gets garbage collected as soon as the
|
|
||||||
* the download is over (maybe even earlier), so trying to cancel a finished download may
|
|
||||||
* throw an error.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
this._upgradeDownloadItem.cancel();
|
|
||||||
} catch (err) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
downloadProgress: null,
|
|
||||||
downloadComplete: false,
|
|
||||||
modal: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSkipClicked: function() {
|
|
||||||
sessionStorage.setItem('upgradeSkipped', true);
|
|
||||||
this.setState({
|
|
||||||
modal: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
alertError: function(error) {
|
|
||||||
var errorInfoList = [];
|
|
||||||
for (let key of Object.keys(error)) {
|
|
||||||
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
|
|
||||||
let label = this._error_key_labels[key];
|
|
||||||
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
modal: 'error',
|
|
||||||
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getContentAndAddress: function()
|
|
||||||
{
|
|
||||||
switch(this.state.viewingPage)
|
|
||||||
{
|
|
||||||
case 'search':
|
|
||||||
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
|
|
||||||
case 'settings':
|
|
||||||
return ["Settings", "icon-gear", <SettingsPage />];
|
|
||||||
case 'help':
|
|
||||||
return ["Help", "icon-question", <HelpPage />];
|
|
||||||
case 'report':
|
|
||||||
return ['Report an Issue', 'icon-file', <ReportPage />];
|
|
||||||
case 'downloaded':
|
|
||||||
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
|
|
||||||
case 'published':
|
|
||||||
return ["Publishes", "icon-folder", <FileListPublished />];
|
|
||||||
case 'start':
|
|
||||||
return ["Start", "icon-file", <StartPage />];
|
|
||||||
case 'rewards':
|
|
||||||
return ["Rewards", "icon-bank", <RewardsPage />];
|
|
||||||
case 'wallet':
|
|
||||||
case 'send':
|
|
||||||
case 'receive':
|
|
||||||
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
|
|
||||||
case 'show':
|
|
||||||
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
|
|
||||||
case 'publish':
|
|
||||||
return ["Publish", "icon-upload", <PublishPage />];
|
|
||||||
case 'developer':
|
|
||||||
return ["Developer", "icon-file", <DeveloperPage />];
|
|
||||||
case 'discover':
|
|
||||||
default:
|
|
||||||
return ["Home", "icon-home", <DiscoverPage />];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
|
|
||||||
|
|
||||||
lbry.setTitle(address);
|
|
||||||
|
|
||||||
if (this._storeHistoryOfNextRender) {
|
|
||||||
this._storeHistoryOfNextRender = false;
|
|
||||||
history.pushState({}, document.title, this.state.appUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
this._fullScreenPages.includes(this.state.viewingPage) ?
|
|
||||||
mainContent :
|
|
||||||
<div id="window">
|
|
||||||
<Header onSearch={this.onSearch} onSubmit={this.showUri} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
|
|
||||||
<div id="main-content">
|
|
||||||
{mainContent}
|
|
||||||
</div>
|
|
||||||
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
|
|
||||||
type="confirm" confirmButtonLabel="Upgrade" abortButtonLabel="Skip"
|
|
||||||
onConfirmed={this.handleUpgradeClicked} onAborted={this.handleSkipClicked}>
|
|
||||||
Your version of LBRY is out of date and may be unreliable or insecure.
|
|
||||||
</Modal>
|
|
||||||
<Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom">
|
|
||||||
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
|
|
||||||
<Line percent={this.state.downloadProgress} strokeWidth="4"/>
|
|
||||||
{this.state.downloadComplete ? (
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<p>Click "Begin Upgrade" to start the upgrade process.</p>
|
|
||||||
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
|
|
||||||
<p>After the install is complete, please reopen the app.</p>
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
<div className="modal__buttons">
|
|
||||||
{this.state.downloadComplete
|
|
||||||
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={this.handleStartUpgradeClicked} />
|
|
||||||
: null}
|
|
||||||
<Link button="alt" label="Cancel" className="modal__button" onClick={this.cancelUpgrade} />
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
|
|
||||||
overlayClassName="modal-overlay error-modal-overlay" onConfirmed={this.closeModal}
|
|
||||||
extraContent={this.state.errorInfo}>
|
|
||||||
<h3 className="modal__header">Error</h3>
|
|
||||||
|
|
||||||
<div className="error-modal__content">
|
|
||||||
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
|
|
||||||
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
|
|
||||||
</div>
|
|
||||||
</ExpandableModal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export default App;
|
|
26
ui/js/component/app/index.js
Normal file
26
ui/js/component/app/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doCheckUpgradeAvailable,
|
||||||
|
doAlertError,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doUpdateBalance,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import App from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
alertError: (errorList) => dispatch(doAlertError(errorList)),
|
||||||
|
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
|
||||||
|
updateBalance: (balance) => dispatch(doUpdateBalance(balance))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(App)
|
42
ui/js/component/app/view.jsx
Normal file
42
ui/js/component/app/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Router from 'component/router'
|
||||||
|
import Header from 'component/header';
|
||||||
|
import ErrorModal from 'component/errorModal'
|
||||||
|
import DownloadingModal from 'component/downloadingModal'
|
||||||
|
import UpgradeModal from 'component/upgradeModal'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {Line} from 'rc-progress'
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
document.addEventListener('unhandledError', (event) => {
|
||||||
|
this.props.alertError(event.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.props.upgradeSkipped) {
|
||||||
|
this.props.checkUpgradeAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
lbry.balanceSubscribe((balance) => {
|
||||||
|
this.props.updateBalance(balance)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
modal,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return <div id="window">
|
||||||
|
<Header />
|
||||||
|
<div id="main-content">
|
||||||
|
<Router />
|
||||||
|
</div>
|
||||||
|
{modal == 'upgrade' && <UpgradeModal />}
|
||||||
|
{modal == 'downloading' && <DownloadingModal />}
|
||||||
|
{modal == 'error' && <ErrorModal />}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
|
@ -1,30 +1,38 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import lbry from "../lbry.js";
|
||||||
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 Link from "component/link"
|
||||||
|
import {RewardLink} from 'component/reward-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, setLocal} from '../utils.js';
|
||||||
|
import rewards from '../rewards'
|
||||||
|
|
||||||
|
|
||||||
const SubmitEmailStage = React.createClass({
|
class SubmitEmailStage extends React.Component {
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
rewardType: null,
|
rewardType: null,
|
||||||
email: '',
|
email: '',
|
||||||
submitting: false
|
submitting: false
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
handleEmailChanged: function(event) {
|
|
||||||
|
handleEmailChanged(event) {
|
||||||
this.setState({
|
this.setState({
|
||||||
email: event.target.value,
|
email: event.target.value,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
onEmailSaved: function(email) {
|
|
||||||
|
onEmailSaved(email) {
|
||||||
this.props.setStage("confirm", { email: email })
|
this.props.setStage("confirm", { email: email })
|
||||||
},
|
}
|
||||||
handleSubmit: function(event) {
|
|
||||||
|
handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -41,38 +49,43 @@ const SubmitEmailStage = React.createClass({
|
||||||
}
|
}
|
||||||
this.setState({ submitting: false });
|
this.setState({ submitting: false });
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={(event) => { this.handleSubmit(event) }}>
|
||||||
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io"
|
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io"
|
||||||
name="email" value={this.state.email}
|
name="email" value={this.state.email}
|
||||||
onChange={this.handleEmailChanged} />
|
onChange={(event) => { this.handleEmailChanged(event) }} />
|
||||||
<div className="form-row-submit">
|
<div className="form-row-submit">
|
||||||
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
|
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={(event) => { this.handleSubmit(event) }} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const ConfirmEmailStage = React.createClass({
|
class ConfirmEmailStage extends React.Component {
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
rewardType: null,
|
rewardType: null,
|
||||||
code: '',
|
code: '',
|
||||||
submitting: false,
|
submitting: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
handleCodeChanged: function(event) {
|
|
||||||
|
handleCodeChanged(event) {
|
||||||
this.setState({
|
this.setState({
|
||||||
code: event.target.value,
|
code: event.target.value,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleSubmit: function(event) {
|
|
||||||
|
handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setState({
|
this.setState({
|
||||||
submitting: true,
|
submitting: true,
|
||||||
|
@ -86,22 +99,23 @@ const ConfirmEmailStage = React.createClass({
|
||||||
};
|
};
|
||||||
|
|
||||||
lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => {
|
lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => {
|
||||||
if (userEmail.IsVerified) {
|
if (userEmail.is_verified) {
|
||||||
this.props.setStage("welcome")
|
this.props.setStage("welcome")
|
||||||
} else {
|
} else {
|
||||||
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
|
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
|
||||||
}
|
}
|
||||||
}, onSubmitError);
|
}, onSubmitError);
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={(event) => { this.handleSubmit(event) }}>
|
||||||
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
|
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
|
||||||
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
|
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={(event) => { this.handleCodeChanged(event) }}
|
||||||
helper="A verification code is required to access this version."/>
|
helper="A verification code is required to access this version."/>
|
||||||
<div className="form-row-submit form-row-submit--with-footer">
|
<div className="form-row-submit form-row-submit--with-footer">
|
||||||
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
|
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={(event) => { this.handleSubmit(event)}} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-field__helper">
|
<div className="form-field__helper">
|
||||||
No code? <Link onClick={() => { this.props.setStage("nocode")}} label="Click here" />.
|
No code? <Link onClick={() => { this.props.setStage("nocode")}} label="Click here" />.
|
||||||
|
@ -110,25 +124,30 @@ const ConfirmEmailStage = React.createClass({
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const WelcomeStage = React.createClass({
|
class WelcomeStage extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
endAuth: React.PropTypes.func,
|
endAuth: React.PropTypes.func,
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
hasReward: false,
|
hasReward: false,
|
||||||
rewardAmount: null,
|
rewardAmount: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onRewardClaim: function(reward) {
|
onRewardClaim(reward) {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasReward: true,
|
hasReward: true,
|
||||||
rewardAmount: reward.amount
|
rewardAmount: reward.amount
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
!this.state.hasReward ?
|
!this.state.hasReward ?
|
||||||
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
|
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
|
||||||
|
@ -139,7 +158,7 @@ const WelcomeStage = React.createClass({
|
||||||
<p>Below, LBRY is controlled by users -- you -- via blockchain and decentralization.</p>
|
<p>Below, LBRY is controlled by users -- you -- via blockchain and decentralization.</p>
|
||||||
<p>Thank you for making content freedom possible! Here's a nickel, kid.</p>
|
<p>Thank you for making content freedom possible! Here's a nickel, kid.</p>
|
||||||
<div style={{textAlign: "center", marginBottom: "12px"}}>
|
<div style={{textAlign: "center", marginBottom: "12px"}}>
|
||||||
<RewardLink type="new_user" button="primary" onRewardClaim={this.onRewardClaim} onRewardFailure={() => this.props.setStage(null)} onConfirmed={() => { this.props.setStage(null) }} />
|
<RewardLink type="new_user" button="primary" onRewardClaim={(event) => { this.onRewardClaim(event) }} onRewardFailure={() => this.props.setStage(null)} onConfirmed={() => { this.props.setStage(null) }} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Modal> :
|
</Modal> :
|
||||||
|
@ -150,48 +169,42 @@ const WelcomeStage = React.createClass({
|
||||||
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
|
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
|
||||||
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
|
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
|
||||||
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
|
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
|
||||||
<p>Finally, know that LBRY is a beta and that it earns the name.</p>
|
<p>Finally, know that LBRY is an early beta and that it earns the name.</p>
|
||||||
</section>
|
</section>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const ErrorStage = (props) => {
|
||||||
const ErrorStage = React.createClass({
|
return <section>
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<p>An error was encountered that we cannot continue from.</p>
|
<p>An error was encountered that we cannot continue from.</p>
|
||||||
<p>At least we're earning the name beta.</p>
|
<p>At least we're earning the name beta.</p>
|
||||||
{ this.props.errorText ? <p>Message: {this.props.errorText}</p> : '' }
|
{ props.errorText ? <p>Message: {props.errorText}</p> : '' }
|
||||||
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
|
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
|
||||||
</section>
|
</section>
|
||||||
);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const PendingStage = React.createClass({
|
const PendingStage = (props) => {
|
||||||
render: function() {
|
return <section>
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<p>Preparing for first access <span className="busy-indicator"></span></p>
|
<p>Preparing for first access <span className="busy-indicator"></span></p>
|
||||||
</section>
|
</section>
|
||||||
);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const CodeRequiredStage = React.createClass({
|
class CodeRequiredStage extends React.Component {
|
||||||
_balanceSubscribeId: null,
|
constructor(props) {
|
||||||
getInitialState: function() {
|
super(props);
|
||||||
return {
|
|
||||||
|
this._balanceSubscribeId = null
|
||||||
|
|
||||||
|
this.state = {
|
||||||
balance: 0,
|
balance: 0,
|
||||||
address: getLocal('wallet_address')
|
address: getLocal('wallet_address')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount() {
|
||||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
balance: balance
|
balance: balance
|
||||||
|
@ -199,18 +212,20 @@ const CodeRequiredStage = React.createClass({
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!this.state.address) {
|
if (!this.state.address) {
|
||||||
lbry.getUnusedAddress((address) => {
|
lbry.wallet_unused_address().then((address) => {
|
||||||
setLocal('wallet_address', address);
|
setLocal('wallet_address', address);
|
||||||
this.setState({ address: address });
|
this.setState({ address: address });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
componentWillUnmount: function() {
|
|
||||||
|
componentWillUnmount() {
|
||||||
if (this._balanceSubscribeId) {
|
if (this._balanceSubscribeId) {
|
||||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
const disabled = this.state.balance < 1;
|
const disabled = this.state.balance < 1;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -233,33 +248,38 @@ const CodeRequiredStage = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
export const AuthOverlay = React.createClass({
|
export class AuthOverlay extends React.Component {
|
||||||
_stages: {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._stages = {
|
||||||
pending: PendingStage,
|
pending: PendingStage,
|
||||||
error: ErrorStage,
|
error: ErrorStage,
|
||||||
nocode: CodeRequiredStage,
|
nocode: CodeRequiredStage,
|
||||||
email: SubmitEmailStage,
|
email: SubmitEmailStage,
|
||||||
confirm: ConfirmEmailStage,
|
confirm: ConfirmEmailStage,
|
||||||
welcome: WelcomeStage
|
welcome: WelcomeStage
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
this.state = {
|
||||||
stage: "pending",
|
stage: "pending",
|
||||||
stageProps: {}
|
stageProps: {}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
setStage: function(stage, stageProps = {}) {
|
|
||||||
|
setStage(stage, stageProps = {}) {
|
||||||
this.setState({
|
this.setState({
|
||||||
stage: stage,
|
stage: stage,
|
||||||
stageProps: stageProps
|
stageProps: stageProps
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
componentWillMount: function() {
|
|
||||||
|
componentWillMount() {
|
||||||
lbryio.authenticate().then((user) => {
|
lbryio.authenticate().then((user) => {
|
||||||
if (!user.HasVerifiedEmail) {
|
if (!user.has_verified_email) {
|
||||||
if (getLocal('auth_bypassed')) {
|
if (getLocal('auth_bypassed')) {
|
||||||
this.setStage(null)
|
this.setStage(null)
|
||||||
} else {
|
} else {
|
||||||
|
@ -268,7 +288,7 @@ export const AuthOverlay = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
lbryio.call('reward', 'list', {}).then((userRewards) => {
|
lbryio.call('reward', 'list', {}).then((userRewards) => {
|
||||||
userRewards.filter(function(reward) {
|
userRewards.filter(function(reward) {
|
||||||
return reward.RewardType == "new_user" && reward.TransactionID;
|
return reward.reward_type == rewards.TYPE_NEW_USER && reward.transaction_id;
|
||||||
}).length ?
|
}).length ?
|
||||||
this.setStage(null) :
|
this.setStage(null) :
|
||||||
this.setStage("welcome")
|
this.setStage("welcome")
|
||||||
|
@ -283,19 +303,25 @@ export const AuthOverlay = React.createClass({
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
if (!this.state.stage) {
|
if (!this.state.stage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const StageContent = this._stages[this.state.stage];
|
const StageContent = this._stages[this.state.stage];
|
||||||
|
|
||||||
|
if (!StageContent) {
|
||||||
|
return <span className="empty">Unknown authentication step.</span>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.state.stage != "welcome" ?
|
this.state.stage != "welcome" ?
|
||||||
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
|
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication">
|
||||||
<h1>LBRY Early Access</h1>
|
<h1>LBRY Early Access</h1>
|
||||||
<StageContent {...this.state.stageProps} setStage={this.setStage} />
|
<StageContent {...this.state.stageProps} setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} />
|
||||||
</ModalPage> :
|
</ModalPage> :
|
||||||
<StageContent setStage={this.setStage} {...this.state.stageProps} />
|
<StageContent setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} {...this.state.stageProps} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
import lbryuri from '../lbryuri.js';
|
|
||||||
import {Icon} from './common.js';
|
|
||||||
|
|
||||||
const UriIndicator = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string.isRequired,
|
|
||||||
hasSignature: React.PropTypes.bool.isRequired,
|
|
||||||
signatureIsValid: React.PropTypes.bool,
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
|
|
||||||
const uriObj = lbryuri.parse(this.props.uri);
|
|
||||||
|
|
||||||
if (!this.props.hasSignature || !uriObj.isChannel) {
|
|
||||||
return <span className="empty">Anonymous</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelUriObj = Object.assign({}, uriObj);
|
|
||||||
delete channelUriObj.path;
|
|
||||||
delete channelUriObj.contentName;
|
|
||||||
const channelUri = lbryuri.build(channelUriObj, false);
|
|
||||||
|
|
||||||
let icon, modifier;
|
|
||||||
if (this.props.signatureIsValid) {
|
|
||||||
modifier = 'valid';
|
|
||||||
} else {
|
|
||||||
icon = 'icon-times-circle';
|
|
||||||
modifier = 'invalid';
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{channelUri} {' '}
|
|
||||||
{ !this.props.signatureIsValid ?
|
|
||||||
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
|
|
||||||
'' }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default UriIndicator;
|
|
|
@ -2,65 +2,69 @@ import React from 'react';
|
||||||
import lbry from '../lbry.js';
|
import lbry from '../lbry.js';
|
||||||
|
|
||||||
//component/icon.js
|
//component/icon.js
|
||||||
export let Icon = React.createClass({
|
export class Icon extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
fixed: React.PropTypes.bool,
|
fixed: React.PropTypes.bool,
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
const {fixed, className, ...other} = this.props;
|
render() {
|
||||||
|
const {fixed, className} = this.props;
|
||||||
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
|
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
|
||||||
this.props.icon + ' ' + (this.props.className || ''));
|
this.props.icon + ' ' + (this.props.className || ''));
|
||||||
return <span className={spanClassName} {... other}></span>
|
return <span className={spanClassName}></span>
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let TruncatedText = React.createClass({
|
export class TruncatedText extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
lines: React.PropTypes.number
|
lines: React.PropTypes.number,
|
||||||
},
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
lines: null,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
render: function() {
|
static defaultProps = {
|
||||||
|
lines: null
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
|
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let BusyMessage = React.createClass({
|
export class BusyMessage extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
message: React.PropTypes.string
|
message: React.PropTypes.string,
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return <span>{this.props.message} <span className="busy-indicator"></span></span>
|
return <span>{this.props.message} <span className="busy-indicator"></span></span>
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let CurrencySymbol = React.createClass({
|
export class CurrencySymbol extends React.Component {
|
||||||
render: function() { return <span>LBC</span>; }
|
render() {
|
||||||
});
|
return <span>LBC</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export let CreditAmount = React.createClass({
|
export class CreditAmount extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
amount: React.PropTypes.number.isRequired,
|
amount: React.PropTypes.number.isRequired,
|
||||||
precision: React.PropTypes.number,
|
precision: React.PropTypes.number,
|
||||||
isEstimate: React.PropTypes.bool,
|
isEstimate: React.PropTypes.bool,
|
||||||
label: React.PropTypes.bool,
|
label: React.PropTypes.bool,
|
||||||
showFree: React.PropTypes.bool,
|
showFree: React.PropTypes.bool,
|
||||||
look: React.PropTypes.oneOf(['indicator', 'plain']),
|
look: React.PropTypes.oneOf(['indicator', 'plain']),
|
||||||
},
|
}
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
static defaultProps = {
|
||||||
precision: 1,
|
precision: 1,
|
||||||
label: true,
|
label: true,
|
||||||
showFree: false,
|
showFree: false,
|
||||||
look: 'indicator',
|
look: 'indicator',
|
||||||
}
|
}
|
||||||
},
|
|
||||||
render: function() {
|
render() {
|
||||||
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
|
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
|
||||||
let amountText;
|
let amountText;
|
||||||
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
|
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
|
||||||
|
@ -80,93 +84,56 @@ export let CreditAmount = React.createClass({
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let FilePrice = React.createClass({
|
let addressStyle = {
|
||||||
_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 = {
|
|
||||||
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
|
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
|
||||||
};
|
};
|
||||||
export let Address = React.createClass({
|
export class Address extends React.Component {
|
||||||
_inputElem: null,
|
static propTypes = {
|
||||||
propTypes: {
|
|
||||||
address: React.PropTypes.string,
|
address: React.PropTypes.string,
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._inputElem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
|
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
|
||||||
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
|
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let Thumbnail = React.createClass({
|
export class Thumbnail extends React.Component {
|
||||||
_defaultImageUri: lbry.imagePath('default-thumb.svg'),
|
static propTypes = {
|
||||||
_maxLoadTime: 10000,
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
src: React.PropTypes.string,
|
src: React.PropTypes.string,
|
||||||
},
|
}
|
||||||
handleError: function() {
|
|
||||||
|
handleError() {
|
||||||
if (this.state.imageUrl != this._defaultImageUri) {
|
if (this.state.imageUrl != this._defaultImageUri) {
|
||||||
this.setState({
|
this.setState({
|
||||||
imageUri: this._defaultImageUri,
|
imageUri: this._defaultImageUri,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._defaultImageUri = lbry.imagePath('default-thumb.svg')
|
||||||
|
this._maxLoadTime = 10000
|
||||||
|
this._isMounted = false
|
||||||
|
|
||||||
|
this.state = {
|
||||||
imageUri: this.props.src || this._defaultImageUri,
|
imageUri: this.props.src || this._defaultImageUri,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
componentDidMount: function() {
|
|
||||||
|
componentDidMount() {
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._isMounted && !this.refs.img.complete) {
|
if (this._isMounted && !this.refs.img.complete) {
|
||||||
|
@ -175,14 +142,16 @@ export let Thumbnail = React.createClass({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, this._maxLoadTime);
|
}, this._maxLoadTime);
|
||||||
},
|
}
|
||||||
componentWillUnmount: function() {
|
|
||||||
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
const className = this.props.className ? this.props.className : '',
|
const className = this.props.className ? this.props.className : '',
|
||||||
otherProps = Object.assign({}, this.props)
|
otherProps = Object.assign({}, this.props)
|
||||||
delete otherProps.className;
|
delete otherProps.className;
|
||||||
return <img ref="img" onError={this.handleError} {...otherProps} className={className} src={this.state.imageUri} />
|
return <img ref="img" onError={() => { this.handleError() }} {...otherProps} className={className} src={this.state.imageUri} />
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
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 ? downloadProgress : 0} 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
|
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,
|
||||||
|
selectModalExtraContent,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import ErrorModal from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
error: selectModalExtraContent(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
closeModal: () => dispatch(doCloseModal())
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(ErrorModal)
|
54
ui/js/component/errorModal/view.jsx
Normal file
54
ui/js/component/errorModal/view.jsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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 errorObj = typeof error === "string" ? { error: error } : error
|
||||||
|
|
||||||
|
const error_key_labels = {
|
||||||
|
connectionString: 'API connection string',
|
||||||
|
method: 'Method',
|
||||||
|
params: 'Parameters',
|
||||||
|
code: 'Error code',
|
||||||
|
message: 'Error message',
|
||||||
|
data: 'Error data',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const errorInfoList = []
|
||||||
|
for (let key of Object.keys(error)) {
|
||||||
|
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
|
||||||
|
let label = error_key_labels[key];
|
||||||
|
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
|
||||||
|
}
|
||||||
|
const errorInfo = <ul className="error-modal__error-list">{errorInfoList}</ul>
|
||||||
|
|
||||||
|
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>);
|
|
||||||
}
|
|
||||||
});
|
|
58
ui/js/component/file-selector.js
Normal file
58
ui/js/component/file-selector.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const {remote} = require('electron');
|
||||||
|
class FileSelector extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
type: React.PropTypes.oneOf(['file', 'directory']),
|
||||||
|
initPath: React.PropTypes.string,
|
||||||
|
onFileChosen: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
type: 'file',
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({
|
||||||
|
path: this.props.initPath || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleButtonClick() {
|
||||||
|
remote.dialog.showOpenDialog({
|
||||||
|
properties: [this.props.type == 'file' ? 'openFile' : 'openDirectory'],
|
||||||
|
}, (paths) => {
|
||||||
|
if (!paths) { // User hit cancel, so do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = paths[0];
|
||||||
|
this.setState({
|
||||||
|
path: path,
|
||||||
|
});
|
||||||
|
if (this.props.onFileChosen) {
|
||||||
|
this.props.onFileChosen(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="file-selector">
|
||||||
|
<button type="button" className="file-selector__choose-button" onClick={() => this.handleButtonClick()}>
|
||||||
|
{this.props.type == 'file' ?
|
||||||
|
'Choose File' :
|
||||||
|
'Choose Directory'}
|
||||||
|
</button>
|
||||||
|
{' '}
|
||||||
|
<span className="file-selector__path">
|
||||||
|
{this.state.path ?
|
||||||
|
this.state.path :
|
||||||
|
'No File Chosen'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileSelector;
|
|
@ -1,284 +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 {BusyMessage, 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;
|
|
||||||
const primaryUrl = "?show=" + uri;
|
|
||||||
return (
|
|
||||||
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
|
||||||
<a href={primaryUrl} className="card__link">
|
|
||||||
<div className={"card__inner file-tile__row"}>
|
|
||||||
<div className="card__media"
|
|
||||||
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
|
|
||||||
</div>
|
|
||||||
<div className="file-tile__content">
|
|
||||||
<div className="card__title-primary">
|
|
||||||
{ !this.props.hidePrice
|
|
||||||
? <FilePrice uri={this.props.uri} />
|
|
||||||
: null}
|
|
||||||
<div className="meta">{uri}</div>
|
|
||||||
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
|
|
||||||
</div>
|
|
||||||
<div className="card__content card__subtext">
|
|
||||||
<TruncatedText lines={3}>
|
|
||||||
{isConfirmed
|
|
||||||
? metadata.description
|
|
||||||
: <span className="empty">This file is pending confirmation.</span>}
|
|
||||||
</TruncatedText>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{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,
|
|
||||||
_isResolvePending: false,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
uri: React.PropTypes.string.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
outpoint: null,
|
|
||||||
claimInfo: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolve: function(uri) {
|
|
||||||
this._isResolvePending = true;
|
|
||||||
lbry.resolve({uri: uri}).then((resolutionInfo) => {
|
|
||||||
this._isResolvePending = false;
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
|
||||||
if (nextProps.uri != this.props.uri) {
|
|
||||||
this.setState(this.getInitialState());
|
|
||||||
this.resolve(nextProps.uri);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
this.resolve(this.props.uri);
|
|
||||||
},
|
|
||||||
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} />
|
|
||||||
}
|
|
||||||
if (this.props.showEmpty)
|
|
||||||
{
|
|
||||||
return this._isResolvePending ?
|
|
||||||
<BusyMessage message="Loading magic decentralized data" /> :
|
|
||||||
<div className="empty">{lbryuri.normalize(this.props.uri)} is unclaimed. <Link label="Put something here" href="?publish" /></div>;
|
|
||||||
}
|
|
||||||
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 {
|
||||||
|
selectPlatform,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectDownloadingForUri,
|
||||||
|
makeSelectLoadingForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
makeSelectIsAvailableForUri,
|
||||||
|
} from 'selectors/availability'
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
doOpenModal,
|
||||||
|
doHistoryBack,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doFetchAvailability
|
||||||
|
} from 'actions/availability'
|
||||||
|
import {
|
||||||
|
doOpenFileInShell,
|
||||||
|
doOpenFileInFolder,
|
||||||
|
doDeleteFile,
|
||||||
|
} from 'actions/file_info'
|
||||||
|
import {
|
||||||
|
doPurchaseUri,
|
||||||
|
doLoadVideo,
|
||||||
|
} from 'actions/content'
|
||||||
|
import FileActions from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectIsAvailableForUri = makeSelectIsAvailableForUri()
|
||||||
|
const selectDownloadingForUri = makeSelectDownloadingForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
isAvailable: selectIsAvailableForUri(state, props),
|
||||||
|
platform: selectPlatform(state),
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
downloading: selectDownloadingForUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
checkAvailability: (uri) => dispatch(doFetchAvailability(uri)),
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
openInFolder: (fileInfo) => dispatch(doOpenFileInFolder(fileInfo)),
|
||||||
|
openInShell: (fileInfo) => dispatch(doOpenFileInShell(fileInfo)),
|
||||||
|
deleteFile: (fileInfo, deleteFromComputer) => {
|
||||||
|
dispatch(doHistoryBack())
|
||||||
|
dispatch(doDeleteFile(fileInfo, deleteFromComputer))
|
||||||
|
},
|
||||||
|
openModal: (modal) => dispatch(doOpenModal(modal)),
|
||||||
|
startDownload: (uri) => dispatch(doPurchaseUri(uri)),
|
||||||
|
loadVideo: (uri) => dispatch(doLoadVideo(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileActions)
|
147
ui/js/component/fileActions/view.jsx
Normal file
147
ui/js/component/fileActions/view.jsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Icon,BusyMessage} 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 FileActions extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
forceShowActions: false,
|
||||||
|
deleteChecked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.checkAvailability(this.props.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.checkAvailability(nextProps.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAvailability(uri) {
|
||||||
|
if (!this._uri || uri !== this._uri) {
|
||||||
|
this._uri = uri;
|
||||||
|
this.props.checkAvailability(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowFileActionsRowClicked() {
|
||||||
|
this.setState({
|
||||||
|
forceShowActions: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteCheckboxClicked(event) {
|
||||||
|
this.setState({
|
||||||
|
deleteChecked: event.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onAffirmPurchase() {
|
||||||
|
this.props.closeModal()
|
||||||
|
this.props.loadVideo(this.props.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fileInfo,
|
||||||
|
isAvailable,
|
||||||
|
platform,
|
||||||
|
downloading,
|
||||||
|
uri,
|
||||||
|
deleteFile,
|
||||||
|
openInFolder,
|
||||||
|
openInShell,
|
||||||
|
modal,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
startDownload,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const deleteChecked = this.state.deleteChecked,
|
||||||
|
metadata = fileInfo ? fileInfo.metadata : null,
|
||||||
|
openInFolderMessage = platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
|
||||||
|
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
|
||||||
|
title = metadata ? metadata.title : uri;
|
||||||
|
|
||||||
|
let content
|
||||||
|
|
||||||
|
if (downloading) {
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
content = <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 if (!fileInfo && isAvailable === undefined) {
|
||||||
|
|
||||||
|
content = <BusyMessage message="Checking availability" />
|
||||||
|
|
||||||
|
} else if (!fileInfo && !isAvailable && !this.state.forceShowActions) {
|
||||||
|
|
||||||
|
content = <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>
|
||||||
|
|
||||||
|
} else if (fileInfo === null && !downloading) {
|
||||||
|
|
||||||
|
content = <Link button="text" label="Download" icon="icon-download" onClick={() => { startDownload(uri) } } />;
|
||||||
|
|
||||||
|
} else if (fileInfo && fileInfo.download_path) {
|
||||||
|
content = <Link label="Open" button="text" icon="icon-folder-open" onClick={() => openInShell(fileInfo)} />;
|
||||||
|
} else {
|
||||||
|
console.log('handle this case of file action props?');
|
||||||
|
console.log(this.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="file-actions">
|
||||||
|
{ content }
|
||||||
|
{ 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={this.props.closeModal}>
|
||||||
|
This will purchase <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(fileInfo.outpoint, 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>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileActions
|
50
ui/js/component/fileCard/index.js
Normal file
50
ui/js/component/fileCard/index.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
doCancelResolveUri,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
selectObscureNsfw,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
makeSelectIsResolvingForUri,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import FileCard from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaimForUri = makeSelectClaimForUri()
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectMetadataForUri = makeSelectMetadataForUri()
|
||||||
|
const selectResolvingUri = makeSelectIsResolvingForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaimForUri(state, props),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
obscureNsfw: selectObscureNsfw(state),
|
||||||
|
metadata: selectMetadataForUri(state, props),
|
||||||
|
isResolvingUri: selectResolvingUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||||
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
|
cancelResolveUri: (uri) => dispatch(doCancelResolveUri(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileCard)
|
113
ui/js/component/fileCard/view.jsx
Normal file
113
ui/js/component/fileCard/view.jsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry.js';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {Thumbnail, TruncatedText, Icon} from 'component/common';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
|
||||||
|
class FileCard extends React.Component {
|
||||||
|
componentWillMount() {
|
||||||
|
this.resolve(this.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.resolve(nextProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(props) {
|
||||||
|
const {
|
||||||
|
isResolvingUri,
|
||||||
|
resolveUri,
|
||||||
|
claim,
|
||||||
|
uri,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if(!isResolvingUri && claim === undefined && uri) {
|
||||||
|
resolveUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const {
|
||||||
|
isResolvingUri,
|
||||||
|
cancelResolveUri,
|
||||||
|
uri
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (isResolvingUri) {
|
||||||
|
cancelResolveUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOver() {
|
||||||
|
this.setState({
|
||||||
|
hovered: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOut() {
|
||||||
|
this.setState({
|
||||||
|
hovered: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
fileInfo,
|
||||||
|
metadata,
|
||||||
|
isResolvingUri,
|
||||||
|
navigate,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const uri = lbryuri.normalize(this.props.uri);
|
||||||
|
const title = !isResolvingUri && metadata && metadata.title ? metadata.title : uri;
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||||
|
|
||||||
|
let description = ""
|
||||||
|
if (isResolvingUri) {
|
||||||
|
description = "Loading..."
|
||||||
|
} else if (metadata && metadata.description) {
|
||||||
|
description = metadata.description
|
||||||
|
} else if (claim === null) {
|
||||||
|
description = 'This address contains no content.'
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Link onClick={() => navigate('/show', { uri })} className="card__link">
|
||||||
|
<div className="card__title-identity">
|
||||||
|
<h5 title={title}><TruncatedText lines={1}>{title}</TruncatedText></h5>
|
||||||
|
<div className="card__subtitle">
|
||||||
|
<span style={{float: "right"}}>
|
||||||
|
<FilePrice uri={uri} />
|
||||||
|
{ fileInfo ? <span>{' '}<Icon fixed icon="icon-folder" /></span> : '' }
|
||||||
|
</span>
|
||||||
|
<UriIndicator uri={uri} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{metadata && metadata.thumbnail &&
|
||||||
|
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
|
||||||
|
}
|
||||||
|
<div className="card__content card__subtext card__subtext--two-lines">
|
||||||
|
<TruncatedText lines={2}>{description}</TruncatedText>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{obscureNsfw && 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" onClick={() => navigate('settings')} label="Settings" />.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileCard
|
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)
|
93
ui/js/component/fileList/view.jsx
Normal file
93
ui/js/component/fileList/view.jsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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 FileTile from 'component/fileTile';
|
||||||
|
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,
|
||||||
|
fetching,
|
||||||
|
fileInfos,
|
||||||
|
} = 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(<FileTile key={uri} uri={uri} hidePrice={true} showEmpty={this.props.fileTileShowEmpty} />)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<section className="file-list__header">
|
||||||
|
{ fetching && <span className="busy-indicator"/> }
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileList
|
29
ui/js/component/fileListSearch/index.js
Normal file
29
ui/js/component/fileListSearch/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doSearch,
|
||||||
|
} from 'actions/search'
|
||||||
|
import {
|
||||||
|
selectIsSearching,
|
||||||
|
selectCurrentSearchResults,
|
||||||
|
selectSearchQuery,
|
||||||
|
} from 'selectors/search'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import FileListSearch from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
isSearching: selectIsSearching(state),
|
||||||
|
query: selectSearchQuery(state),
|
||||||
|
results: selectCurrentSearchResults(state)
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
search: (search) => dispatch(doSearch(search))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileListSearch)
|
76
ui/js/component/fileListSearch/view.jsx
Normal file
76
ui/js/component/fileListSearch/view.jsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry';
|
||||||
|
import lbryio from 'lbryio';
|
||||||
|
import lbryuri from 'lbryuri';
|
||||||
|
import lighthouse from 'lighthouse';
|
||||||
|
import FileTile from 'component/fileTile'
|
||||||
|
import Link from 'component/link'
|
||||||
|
import {ToolTip} from 'component/tooltip.js';
|
||||||
|
import {BusyMessage} from 'component/common.js';
|
||||||
|
|
||||||
|
const SearchNoResults = (props) => {
|
||||||
|
const {
|
||||||
|
navigate,
|
||||||
|
query,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return <section>
|
||||||
|
<span className="empty">
|
||||||
|
No one has checked anything in for {query} yet. { ' ' }
|
||||||
|
<Link label="Be the first" onClick={() => navigate('/publish')} />
|
||||||
|
</span>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileListSearchResults = (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(
|
||||||
|
<FileTile key={uri} uri={uri} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>{rows}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileListSearch extends React.Component{
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.search(this.props.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isSearching,
|
||||||
|
results
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isSearching && !results &&
|
||||||
|
<BusyMessage message="Looking up the Dewey Decimals" />}
|
||||||
|
|
||||||
|
{isSearching && results &&
|
||||||
|
<BusyMessage message="Refreshing the Dewey Decimals" />}
|
||||||
|
|
||||||
|
{(results && !!results.length) ?
|
||||||
|
<FileListSearchResults {...this.props} /> :
|
||||||
|
<SearchNoResults {...this.props} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileListSearch
|
28
ui/js/component/filePrice/index.js
Normal file
28
ui/js/component/filePrice/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doFetchCostInfoForUri,
|
||||||
|
} from 'actions/cost_info'
|
||||||
|
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) => ({
|
||||||
|
fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
|
// cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FilePrice)
|
43
ui/js/component/filePrice/view.jsx
Normal file
43
ui/js/component/filePrice/view.jsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
CreditAmount,
|
||||||
|
} from 'component/common'
|
||||||
|
|
||||||
|
class FilePrice extends React.Component{
|
||||||
|
componentWillMount() {
|
||||||
|
this.fetchCost(this.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.fetchCost(nextProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCost(props) {
|
||||||
|
const {
|
||||||
|
costInfo,
|
||||||
|
fetchCostInfo,
|
||||||
|
uri
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (costInfo === undefined) {
|
||||||
|
fetchCostInfo(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
costInfo,
|
||||||
|
look = 'indicator',
|
||||||
|
} = this.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
|
48
ui/js/component/fileTile/index.js
Normal file
48
ui/js/component/fileTile/index.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
selectObscureNsfw,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
makeSelectIsResolvingForUri,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import FileTile from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaimForUri = makeSelectClaimForUri()
|
||||||
|
const selectFileInfoForUri = makeSelectFileInfoForUri()
|
||||||
|
const selectMetadataForUri = makeSelectMetadataForUri()
|
||||||
|
const selectResolvingUri = makeSelectIsResolvingForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaimForUri(state, props),
|
||||||
|
fileInfo: selectFileInfoForUri(state, props),
|
||||||
|
obscureNsfw: selectObscureNsfw(state),
|
||||||
|
metadata: selectMetadataForUri(state, props),
|
||||||
|
isResolvingUri: selectResolvingUri(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||||
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(FileTile)
|
116
ui/js/component/fileTile/view.jsx
Normal file
116
ui/js/component/fileTile/view.jsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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/uriIndicator';
|
||||||
|
|
||||||
|
class FileTile extends React.Component {
|
||||||
|
static SHOW_EMPTY_PUBLISH = "publish"
|
||||||
|
static SHOW_EMPTY_PENDING = "pending"
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
showNsfwHelp: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
isResolvingUri,
|
||||||
|
resolveUri,
|
||||||
|
claim,
|
||||||
|
uri,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if(!isResolvingUri && !claim && uri) {
|
||||||
|
resolveUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
metadata,
|
||||||
|
isResolvingUri,
|
||||||
|
showEmpty,
|
||||||
|
navigate,
|
||||||
|
hidePrice,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const uri = lbryuri.normalize(this.props.uri);
|
||||||
|
const isClaimed = !!claim;
|
||||||
|
const isClaimable = lbryuri.isClaimable(uri)
|
||||||
|
const title = isClaimed && metadata && metadata.title ? metadata.title : uri;
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||||
|
let onClick = () => navigate('/show', { uri })
|
||||||
|
|
||||||
|
let description = ""
|
||||||
|
if (isClaimed) {
|
||||||
|
description = metadata && metadata.description
|
||||||
|
} else if (isResolvingUri) {
|
||||||
|
description = "Loading..."
|
||||||
|
} else if (showEmpty === FileTile.SHOW_EMPTY_PUBLISH) {
|
||||||
|
onClick = () => navigate('/publish', { })
|
||||||
|
description = <span className="empty">
|
||||||
|
This location is unused. { ' ' }
|
||||||
|
{ isClaimable && <span className="button-text">Put something here!</span> }
|
||||||
|
</span>
|
||||||
|
} else if (showEmpty === FileTile.SHOW_EMPTY_PENDING) {
|
||||||
|
description = <span className="empty">This file is pending confirmation.</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
|
||||||
|
<Link onClick={onClick} className="card__link">
|
||||||
|
<div className={"card__inner file-tile__row"}>
|
||||||
|
<div className="card__media"
|
||||||
|
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
|
||||||
|
</div>
|
||||||
|
<div className="file-tile__content">
|
||||||
|
<div className="card__title-primary">
|
||||||
|
{ !hidePrice ? <FilePrice uri={this.props.uri} /> : null}
|
||||||
|
<div className="meta">{uri}</div>
|
||||||
|
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
|
||||||
|
</div>
|
||||||
|
<div className="card__content card__subtext">
|
||||||
|
<TruncatedText lines={3}>
|
||||||
|
{description}
|
||||||
|
</TruncatedText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{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" onClick={() => navigate('/settings')} label="Settings" />.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileTile
|
|
@ -1,65 +1,95 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import FileSelector from './file-selector.js';
|
||||||
import {Icon} from './common.js';
|
import {Icon} from './common.js';
|
||||||
|
|
||||||
var formFieldCounter = 0,
|
var formFieldCounter = 0,
|
||||||
|
formFieldFileSelectorTypes = ['file', 'directory'],
|
||||||
formFieldNestedLabelTypes = ['radio', 'checkbox'];
|
formFieldNestedLabelTypes = ['radio', 'checkbox'];
|
||||||
|
|
||||||
function formFieldId() {
|
function formFieldId() {
|
||||||
return "form-field-" + (++formFieldCounter);
|
return "form-field-" + (++formFieldCounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
export let FormField = React.createClass({
|
export class FormField extends React.Component {
|
||||||
_fieldRequiredText: 'This field is required',
|
static propTypes = {
|
||||||
_type: null,
|
|
||||||
_element: null,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
type: React.PropTypes.string.isRequired,
|
type: React.PropTypes.string.isRequired,
|
||||||
prefix: React.PropTypes.string,
|
prefix: React.PropTypes.string,
|
||||||
postfix: React.PropTypes.string,
|
postfix: React.PropTypes.string,
|
||||||
hasError: React.PropTypes.bool
|
hasError: React.PropTypes.bool
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._fieldRequiredText = 'This field is required';
|
||||||
|
this._type = null;
|
||||||
|
this._element = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
isError: null,
|
isError: null,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
componentWillMount() {
|
||||||
if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
|
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
|
||||||
this._element = 'input';
|
this._element = 'input';
|
||||||
this._type = this.props.type;
|
this._type = this.props.type;
|
||||||
} else if (this.props.type == 'text-number') {
|
} else if (this.props.type == 'text-number') {
|
||||||
this._element = 'input';
|
this._element = 'input';
|
||||||
this._type = 'text';
|
this._type = 'text';
|
||||||
|
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
|
||||||
|
this._element = 'input';
|
||||||
|
this._type = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
// Non <input> field, e.g. <select>, <textarea>
|
// Non <input> field, e.g. <select>, <textarea>
|
||||||
this._element = this.props.type;
|
this._element = this.props.type;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
showError: function(text) {
|
|
||||||
|
componentDidMount() {
|
||||||
|
/**
|
||||||
|
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
|
||||||
|
* https://github.com/facebook/react/issues/3468
|
||||||
|
*/
|
||||||
|
if (this.props.type == 'directory') {
|
||||||
|
this.refs.field.webkitdirectory = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileChosen(path) {
|
||||||
|
this.refs.field.value = path;
|
||||||
|
if (this.props.onChange) { // Updating inputs programmatically doesn't generate an event, so we have to make our own
|
||||||
|
const event = new Event('change', {bubbles: true})
|
||||||
|
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
|
||||||
|
this.props.onChange(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(text) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isError: true,
|
isError: true,
|
||||||
errorMessage: text,
|
errorMessage: text,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
focus: function() {
|
|
||||||
|
focus() {
|
||||||
this.refs.field.focus();
|
this.refs.field.focus();
|
||||||
},
|
}
|
||||||
getValue: function() {
|
|
||||||
|
getValue() {
|
||||||
if (this.props.type == 'checkbox') {
|
if (this.props.type == 'checkbox') {
|
||||||
return this.refs.field.checked;
|
return this.refs.field.checked;
|
||||||
} else if (this.props.type == 'file') {
|
|
||||||
return this.refs.field.files.length && this.refs.field.files[0].path ?
|
|
||||||
this.refs.field.files[0].path : null;
|
|
||||||
} else {
|
} else {
|
||||||
return this.refs.field.value;
|
return this.refs.field.value;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
getSelectedElement: function() {
|
|
||||||
|
getSelectedElement() {
|
||||||
return this.refs.field.options[this.refs.field.selectedIndex];
|
return this.refs.field.options[this.refs.field.selectedIndex];
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
// Pass all unhandled props to the field element
|
// Pass all unhandled props to the field element
|
||||||
const otherProps = Object.assign({}, this.props),
|
const otherProps = Object.assign({}, this.props),
|
||||||
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
|
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
|
||||||
|
@ -79,7 +109,7 @@ export let FormField = React.createClass({
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</this._element>;
|
</this._element>;
|
||||||
|
|
||||||
return <div className="form-field">
|
return <div className={"form-field form-field--" + this.props.type}>
|
||||||
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
|
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
|
||||||
{ renderElementInsideLabel ?
|
{ renderElementInsideLabel ?
|
||||||
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
|
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
|
||||||
|
@ -87,49 +117,64 @@ export let FormField = React.createClass({
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</label> :
|
</label> :
|
||||||
element }
|
element }
|
||||||
|
{ formFieldFileSelectorTypes.includes(this.props.type) ?
|
||||||
|
<FileSelector type={this.props.type} onFileChosen={this.handleFileChosen.bind(this)}
|
||||||
|
{... this.props.defaultValue ? {initPath: this.props.defaultValue} : {}} /> :
|
||||||
|
null }
|
||||||
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
|
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
|
||||||
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
export let FormRow = React.createClass({
|
export class FormRow extends React.Component {
|
||||||
_fieldRequiredText: 'This field is required',
|
static propTypes = {
|
||||||
propTypes: {
|
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
|
||||||
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
|
|
||||||
// helper: React.PropTypes.html,
|
// helper: React.PropTypes.html,
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._fieldRequiredText = 'This field is required';
|
||||||
|
|
||||||
|
this.state = {
|
||||||
isError: false,
|
isError: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
showError: function(text) {
|
showError(text) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isError: true,
|
isError: true,
|
||||||
errorMessage: text,
|
errorMessage: text,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
showRequiredError: function() {
|
|
||||||
|
showRequiredError() {
|
||||||
this.showError(this._fieldRequiredText);
|
this.showError(this._fieldRequiredText);
|
||||||
},
|
}
|
||||||
clearError: function(text) {
|
|
||||||
|
clearError(text) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isError: false,
|
isError: false,
|
||||||
errorMessage: ''
|
errorMessage: ''
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
getValue: function() {
|
|
||||||
|
getValue() {
|
||||||
return this.refs.field.getValue();
|
return this.refs.field.getValue();
|
||||||
},
|
}
|
||||||
getSelectedElement: function() {
|
|
||||||
|
getSelectedElement() {
|
||||||
return this.refs.field.getSelectedElement();
|
return this.refs.field.getSelectedElement();
|
||||||
},
|
}
|
||||||
focus: function() {
|
|
||||||
|
focus() {
|
||||||
this.refs.field.focus();
|
this.refs.field.focus();
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
const fieldProps = Object.assign({}, this.props),
|
const fieldProps = Object.assign({}, this.props),
|
||||||
elementId = formFieldId(),
|
elementId = formFieldId(),
|
||||||
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
|
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
|
||||||
|
@ -151,4 +196,4 @@ export let FormRow = React.createClass({
|
||||||
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
|
@ -1,213 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbryuri from '../lbryuri.js';
|
|
||||||
import {Link} from './link.js';
|
|
||||||
import {Icon, CreditAmount} from './common.js';
|
|
||||||
|
|
||||||
var Header = React.createClass({
|
|
||||||
_balanceSubscribeId: null,
|
|
||||||
_isMounted: false,
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
onSearch: React.PropTypes.func.isRequired,
|
|
||||||
onSubmit: React.PropTypes.func.isRequired
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
balance: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._isMounted = true;
|
|
||||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState({balance: balance});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._isMounted = false;
|
|
||||||
if (this._balanceSubscribeId) {
|
|
||||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return <header id="header">
|
|
||||||
<div className="header__item">
|
|
||||||
<Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link href="?discover" button="alt button--flat" icon="icon-home" />
|
|
||||||
</div>
|
|
||||||
<div className="header__item header__item--wunderbar">
|
|
||||||
<WunderBar address={this.props.address} icon={this.props.wunderBarIcon}
|
|
||||||
onSearch={this.props.onSearch} onSubmit={this.props.onSubmit} viewingPage={this.props.viewingPage} />
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link href="?wallet" button="text" icon="icon-bank" label={lbry.formatCredits(this.state.balance, 1)} ></Link>
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link button="primary button--flat" href="?publish" icon="icon-upload" label="Publish" />
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link button="alt button--flat" href="?downloaded" icon="icon-folder" />
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link button="alt button--flat" href="?settings" icon="icon-gear" />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class WunderBar extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
onSearch: React.PropTypes.func.isRequired,
|
|
||||||
onSubmit: React.PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this._userTypingTimer = null;
|
|
||||||
this._input = null;
|
|
||||||
this._stateBeforeSearch = null;
|
|
||||||
this._resetOnNextBlur = true;
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
this.onFocus = this.onFocus.bind(this);
|
|
||||||
this.onBlur = this.onBlur.bind(this);
|
|
||||||
this.onKeyPress = this.onKeyPress.bind(this);
|
|
||||||
this.onReceiveRef = this.onReceiveRef.bind(this);
|
|
||||||
this.state = {
|
|
||||||
address: this.props.address,
|
|
||||||
icon: this.props.icon
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.userTypingTimer) {
|
|
||||||
clearTimeout(this._userTypingTimer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(event) {
|
|
||||||
|
|
||||||
if (this._userTypingTimer)
|
|
||||||
{
|
|
||||||
clearTimeout(this._userTypingTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ address: event.target.value })
|
|
||||||
|
|
||||||
let searchTerm = event.target.value;
|
|
||||||
|
|
||||||
this._userTypingTimer = setTimeout(() => {
|
|
||||||
this._resetOnNextBlur = false;
|
|
||||||
this.props.onSearch(searchTerm);
|
|
||||||
}, 800); // 800ms delay, tweak for faster/slower
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
|
|
||||||
this.setState({ address: nextProps.address, icon: nextProps.icon });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
this._stateBeforeSearch = this.state;
|
|
||||||
let newState = {
|
|
||||||
icon: "icon-search",
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
|
|
||||||
this._focusPending = true;
|
|
||||||
//below is hacking, improved when we have proper routing
|
|
||||||
if (!this.state.address.startsWith('lbry://') && this.state.icon !== "icon-search") //onFocus, if they are not on an exact URL or a search page, clear the bar
|
|
||||||
{
|
|
||||||
newState.address = '';
|
|
||||||
}
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
let commonState = {isActive: false};
|
|
||||||
if (this._resetOnNextBlur) {
|
|
||||||
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
|
|
||||||
this._input.value = this.state.address;
|
|
||||||
} else {
|
|
||||||
this._resetOnNextBlur = true;
|
|
||||||
this._stateBeforeSearch = this.state;
|
|
||||||
this.setState(commonState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this._input.value = this.state.address;
|
|
||||||
if (this._input && this._focusPending) {
|
|
||||||
this._input.select();
|
|
||||||
this._focusPending = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyPress(event) {
|
|
||||||
if (event.charCode == 13 && this._input.value) {
|
|
||||||
|
|
||||||
let uri = null,
|
|
||||||
method = "onSubmit";
|
|
||||||
|
|
||||||
this._resetOnNextBlur = false;
|
|
||||||
clearTimeout(this._userTypingTimer);
|
|
||||||
|
|
||||||
try {
|
|
||||||
uri = lbryuri.normalize(this._input.value);
|
|
||||||
this.setState({ value: uri });
|
|
||||||
} catch (error) { //then it's not a valid URL, so let's search
|
|
||||||
uri = this._input.value;
|
|
||||||
method = "onSearch";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props[method](uri);
|
|
||||||
this._input.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReceiveRef(ref) {
|
|
||||||
this._input = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
|
|
||||||
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
|
|
||||||
<input className="wunderbar__input" type="search" placeholder="Type a LBRY address or search term"
|
|
||||||
ref={this.onReceiveRef}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onKeyPress={this.onKeyPress}
|
|
||||||
value={this.state.address}
|
|
||||||
placeholder="Find movies, music, games, and more" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let SubHeader = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
let links = [],
|
|
||||||
viewingUrl = '?' + this.props.viewingPage;
|
|
||||||
|
|
||||||
for (let link of Object.keys(this.props.links)) {
|
|
||||||
links.push(
|
|
||||||
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>
|
|
||||||
{this.props.links[link]}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
|
|
||||||
{links}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Header;
|
|
24
ui/js/component/header/index.js
Normal file
24
ui/js/component/header/index.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react'
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectBalance
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
doHistoryBack,
|
||||||
|
} from 'actions/app'
|
||||||
|
import Header from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
balance: lbry.formatCredits(selectBalance(state), 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
back: () => dispatch(doHistoryBack()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(Header)
|
37
ui/js/component/header/view.jsx
Normal file
37
ui/js/component/header/view.jsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import WunderBar from 'component/wunderbar';
|
||||||
|
|
||||||
|
export const Header = (props) => {
|
||||||
|
const {
|
||||||
|
balance,
|
||||||
|
back,
|
||||||
|
navigate
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return <header id="header">
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={back} button="alt button--flat" icon="icon-arrow-left" />
|
||||||
|
</div>
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={() => navigate('/discover')} button="alt button--flat" icon="icon-home" />
|
||||||
|
</div>
|
||||||
|
<div className="header__item header__item--wunderbar">
|
||||||
|
<WunderBar/>
|
||||||
|
</div>
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={() => navigate('/wallet')} button="text" icon="icon-bank" label={balance} ></Link>
|
||||||
|
</div>
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={() => navigate('/publish')} button="primary button--flat" icon="icon-upload" label="Publish" />
|
||||||
|
</div>
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={() => navigate('/downloaded')} button="alt button--flat" icon="icon-folder" />
|
||||||
|
</div>
|
||||||
|
<div className="header__item">
|
||||||
|
<Link onClick={() => navigate('/settings')} button="alt button--flat" icon="icon-gear" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -1,133 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {Icon} from './common.js';
|
|
||||||
import Modal from '../component/modal.js';
|
|
||||||
import rewards from '../rewards.js';
|
|
||||||
|
|
||||||
export let Link = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
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}
|
|
||||||
{this.props.label ? <span className="link-label">{this.props.label}</span> : null}
|
|
||||||
{'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({
|
|
||||||
propTypes: {
|
|
||||||
type: React.PropTypes.string.isRequired,
|
|
||||||
claimed: React.PropTypes.bool,
|
|
||||||
onRewardClaim: React.PropTypes.func,
|
|
||||||
onRewardFailure: React.PropTypes.func
|
|
||||||
},
|
|
||||||
refreshClaimable: function() {
|
|
||||||
switch(this.props.type) {
|
|
||||||
case 'new_user':
|
|
||||||
this.setState({ claimable: true });
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'first_publish':
|
|
||||||
lbry.claim_list_mine().then((list) => {
|
|
||||||
this.setState({
|
|
||||||
claimable: list.length > 0
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.refreshClaimable();
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
claimable: true,
|
|
||||||
pending: false,
|
|
||||||
errorMessage: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
claimReward: function() {
|
|
||||||
this.setState({
|
|
||||||
pending: true
|
|
||||||
})
|
|
||||||
rewards.claimReward(this.props.type).then((reward) => {
|
|
||||||
this.setState({
|
|
||||||
pending: false,
|
|
||||||
errorMessage: null
|
|
||||||
})
|
|
||||||
if (this.props.onRewardClaim) {
|
|
||||||
this.props.onRewardClaim(reward);
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
this.setState({
|
|
||||||
errorMessage: error.message,
|
|
||||||
pending: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
clearError: function() {
|
|
||||||
if (this.props.onRewardFailure) {
|
|
||||||
this.props.onRewardFailure()
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
errorMessage: null
|
|
||||||
})
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<div className="reward-link">
|
|
||||||
{this.props.claimed
|
|
||||||
? <span><Icon icon="icon-check" /> Reward claimed.</span>
|
|
||||||
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable }
|
|
||||||
label={ this.state.pending ? "Claiming..." : "Claim Reward"} onClick={this.claimReward} />}
|
|
||||||
{this.state.errorMessage ?
|
|
||||||
<Modal isOpen={true} contentLabel="Reward Claim Error" className="error-modal" onConfirmed={this.clearError}>
|
|
||||||
{this.state.errorMessage}
|
|
||||||
</Modal>
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
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)
|
47
ui/js/component/link/view.js
Normal file
47
ui/js/component/link/view.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Icon} from 'component/common.js';
|
||||||
|
|
||||||
|
const Link = (props) => {
|
||||||
|
const {
|
||||||
|
href,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
style,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
badge,
|
||||||
|
button,
|
||||||
|
hidden,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
} = 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 (children) {
|
||||||
|
content = children
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<span {... 'button' in props ? {className: 'button__content'} : {}}>
|
||||||
|
{'icon' in props ? <Icon icon={icon} fixed={true} /> : null}
|
||||||
|
{label ? <span className="link-label">{label}</span> : null}
|
||||||
|
{'badge' in props ? <span className="badge">{badge}</span> : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className={className} href={href || 'javascript:;'} title={title}
|
||||||
|
onClick={onClick}
|
||||||
|
{... 'style' in props ? {style: style} : {}}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Link
|
|
@ -1,27 +1,30 @@
|
||||||
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({
|
class LoadScreen extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
message: React.PropTypes.string.isRequired,
|
message: React.PropTypes.string.isRequired,
|
||||||
details: React.PropTypes.string,
|
details: React.PropTypes.string,
|
||||||
isWarning: React.PropTypes.bool,
|
isWarning: React.PropTypes.bool,
|
||||||
},
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
isWarning: false,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
message: null,
|
message: null,
|
||||||
details: null,
|
details: null,
|
||||||
isLagging: false,
|
isLagging: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
render: function() {
|
static defaultProps = {
|
||||||
|
isWarning: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
const imgSrc = lbry.imagePath('lbry-white-485x160.png');
|
const imgSrc = lbry.imagePath('lbry-white-485x160.png');
|
||||||
return (
|
return (
|
||||||
<div className="load-screen">
|
<div className="load-screen">
|
||||||
|
@ -35,7 +38,7 @@ var LoadScreen = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
export default LoadScreen;
|
export default LoadScreen;
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
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 class DropDownMenuItem extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
href: React.PropTypes.string,
|
href: React.PropTypes.string,
|
||||||
label: React.PropTypes.string,
|
label: React.PropTypes.string,
|
||||||
icon: React.PropTypes.string,
|
icon: React.PropTypes.string,
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
},
|
}
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
static defaultProps = {
|
||||||
iconPosition: 'left',
|
iconPosition: 'left',
|
||||||
}
|
}
|
||||||
},
|
|
||||||
render: function() {
|
render() {
|
||||||
var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null);
|
var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,23 +26,27 @@ export let DropDownMenuItem = React.createClass({
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export let DropDownMenu = React.createClass({
|
export class DropDownMenu extends React.Component {
|
||||||
_isWindowClickBound: false,
|
constructor(props) {
|
||||||
_menuDiv: null,
|
super(props);
|
||||||
|
|
||||||
getInitialState: function() {
|
this._isWindowClickBound = false;
|
||||||
return {
|
this._menuDiv = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
componentWillUnmount: function() {
|
|
||||||
|
componentWillUnmount() {
|
||||||
if (this._isWindowClickBound) {
|
if (this._isWindowClickBound) {
|
||||||
window.removeEventListener('click', this.handleWindowClick, false);
|
window.removeEventListener('click', this.handleWindowClick, false);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
handleMenuIconClick: function(e) {
|
|
||||||
|
handleMenuIconClick(e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
menuOpen: !this.state.menuOpen,
|
menuOpen: !this.state.menuOpen,
|
||||||
});
|
});
|
||||||
|
@ -52,35 +56,38 @@ export let DropDownMenu = React.createClass({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
handleMenuClick: function(e) {
|
|
||||||
|
handleMenuClick(e) {
|
||||||
// Event bubbles up to the menu after a link is clicked
|
// Event bubbles up to the menu after a link is clicked
|
||||||
this.setState({
|
this.setState({
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleWindowClick: function(e) {
|
|
||||||
|
handleWindowClick(e) {
|
||||||
if (this.state.menuOpen &&
|
if (this.state.menuOpen &&
|
||||||
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
|
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
|
||||||
this.setState({
|
this.setState({
|
||||||
menuOpen: false
|
menuOpen: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
if (!this.state.menuOpen && this._isWindowClickBound) {
|
if (!this.state.menuOpen && this._isWindowClickBound) {
|
||||||
this._isWindowClickBound = false;
|
this._isWindowClickBound = false;
|
||||||
window.removeEventListener('click', this.handleWindowClick, false);
|
window.removeEventListener('click', this.handleWindowClick, false);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="menu-container">
|
<div className="menu-container">
|
||||||
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={this.handleMenuIconClick} />
|
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={(event) => { this.handleMenuIconClick(event) }} />
|
||||||
{this.state.menuOpen
|
{this.state.menuOpen
|
||||||
? <div ref={(div) => this._menuDiv = div} className="menu" onClick={this.handleMenuClick}>
|
? <div ref={(div) => this._menuDiv = div} className="menu" onClick={(event) => { this.handleMenuClick(event) }}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
export const ModalPage = React.createClass({
|
export class ModalPage extends React.Component {
|
||||||
render: function() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
||||||
className={(this.props.className || '') + ' modal-page'}
|
className={(this.props.className || '') + ' modal-page'}
|
||||||
|
@ -13,6 +13,6 @@ export const ModalPage = React.createClass({
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default ModalPage;
|
export default ModalPage
|
|
@ -1,10 +1,10 @@
|
||||||
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 class Modal extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
|
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
|
||||||
overlay: React.PropTypes.bool,
|
overlay: React.PropTypes.bool,
|
||||||
onConfirmed: React.PropTypes.func,
|
onConfirmed: React.PropTypes.func,
|
||||||
|
@ -13,18 +13,18 @@ export const Modal = React.createClass({
|
||||||
abortButtonLabel: React.PropTypes.string,
|
abortButtonLabel: React.PropTypes.string,
|
||||||
confirmButtonDisabled: React.PropTypes.bool,
|
confirmButtonDisabled: React.PropTypes.bool,
|
||||||
abortButtonDisabled: React.PropTypes.bool,
|
abortButtonDisabled: React.PropTypes.bool,
|
||||||
},
|
}
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
static defaultProps = {
|
||||||
type: 'alert',
|
type: 'alert',
|
||||||
overlay: true,
|
overlay: true,
|
||||||
confirmButtonLabel: 'OK',
|
confirmButtonLabel: 'OK',
|
||||||
abortButtonLabel: 'Cancel',
|
abortButtonLabel: 'Cancel',
|
||||||
confirmButtonDisabled: false,
|
confirmButtonDisabled: false,
|
||||||
abortButtonDisabled: false,
|
abortButtonDisabled: false,
|
||||||
};
|
}
|
||||||
},
|
|
||||||
render: function() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
||||||
className={(this.props.className || '') + ' modal'}
|
className={(this.props.className || '') + ' modal'}
|
||||||
|
@ -43,31 +43,35 @@ export const Modal = React.createClass({
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export const ExpandableModal = React.createClass({
|
export class ExpandableModal extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
expandButtonLabel: React.PropTypes.string,
|
expandButtonLabel: React.PropTypes.string,
|
||||||
extraContent: React.PropTypes.element,
|
extraContent: React.PropTypes.element,
|
||||||
},
|
}
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
static defaultProps = {
|
||||||
confirmButtonLabel: 'OK',
|
confirmButtonLabel: 'OK',
|
||||||
expandButtonLabel: 'Show More...',
|
expandButtonLabel: 'Show More...',
|
||||||
hideButtonLabel: 'Show Less',
|
hideButtonLabel: 'Show Less',
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
toggleExpanded: function() {
|
|
||||||
|
toggleExpanded() {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: !this.state.expanded,
|
expanded: !this.state.expanded,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<Modal type="custom" {... this.props}>
|
<Modal type="custom" {... this.props}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
@ -77,11 +81,11 @@ export const ExpandableModal = React.createClass({
|
||||||
<div className="modal__buttons">
|
<div className="modal__buttons">
|
||||||
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" onClick={this.props.onConfirmed} />
|
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" onClick={this.props.onConfirmed} />
|
||||||
<Link button="alt" label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
|
<Link button="alt" label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
|
||||||
className="modal__button" onClick={this.toggleExpanded} />
|
className="modal__button" onClick={() => { this.toggleExpanded() }} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Modal;
|
export default Modal;
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const Notice = React.createClass({
|
export class Notice extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
isError: React.PropTypes.bool,
|
isError: React.PropTypes.bool,
|
||||||
},
|
}
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
static defaultProps = {
|
||||||
isError: false,
|
isError: false,
|
||||||
};
|
}
|
||||||
},
|
|
||||||
render: function() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
|
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Notice;
|
export default Notice;
|
91
ui/js/component/reward-link.js
Normal file
91
ui/js/component/reward-link.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbry from 'lbry'
|
||||||
|
import {Icon} from 'component/common';
|
||||||
|
import Modal from 'component/modal';
|
||||||
|
import rewards from 'rewards';
|
||||||
|
import Link from 'component/link'
|
||||||
|
|
||||||
|
export class RewardLink extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
type: React.PropTypes.string.isRequired,
|
||||||
|
claimed: React.PropTypes.bool,
|
||||||
|
onRewardClaim: React.PropTypes.func,
|
||||||
|
onRewardFailure: React.PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
claimable: true,
|
||||||
|
pending: false,
|
||||||
|
errorMessage: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshClaimable() {
|
||||||
|
switch(this.props.type) {
|
||||||
|
case 'new_user':
|
||||||
|
this.setState({ claimable: true });
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'first_publish':
|
||||||
|
lbry.claim_list_mine().then((list) => {
|
||||||
|
this.setState({
|
||||||
|
claimable: list.length > 0
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.refreshClaimable();
|
||||||
|
}
|
||||||
|
|
||||||
|
claimReward() {
|
||||||
|
this.setState({
|
||||||
|
pending: true
|
||||||
|
})
|
||||||
|
|
||||||
|
rewards.claimReward(this.props.type).then((reward) => {
|
||||||
|
this.setState({
|
||||||
|
pending: false,
|
||||||
|
errorMessage: null
|
||||||
|
})
|
||||||
|
if (this.props.onRewardClaim) {
|
||||||
|
this.props.onRewardClaim(reward);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
errorMessage: error.message,
|
||||||
|
pending: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearError() {
|
||||||
|
if (this.props.onRewardFailure) {
|
||||||
|
this.props.onRewardFailure()
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errorMessage: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="reward-link">
|
||||||
|
{this.props.claimed
|
||||||
|
? <span><Icon icon="icon-check" /> Reward claimed.</span>
|
||||||
|
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable }
|
||||||
|
label={ this.state.pending ? "Claiming..." : "Claim Reward"} onClick={() => { this.claimReward() }} />}
|
||||||
|
{this.state.errorMessage ?
|
||||||
|
<Modal isOpen={true} contentLabel="Reward Claim Error" className="error-modal" onConfirmed={() => { this.clearError() }}>
|
||||||
|
{this.state.errorMessage}
|
||||||
|
</Modal>
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
ui/js/component/router/index.jsx
Normal file
14
ui/js/component/router/index.jsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Router from './view.jsx';
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentParams,
|
||||||
|
} from 'selectors/app.js';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
params: selectCurrentParams(state),
|
||||||
|
currentPage: selectCurrentPage(state)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, null)(Router);
|
51
ui/js/component/router/view.jsx
Normal file
51
ui/js/component/router/view.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SettingsPage from 'page/settings';
|
||||||
|
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';
|
||||||
|
import DiscoverPage from 'page/discover';
|
||||||
|
import SplashScreen from 'component/splash.js';
|
||||||
|
import DeveloperPage from 'page/developer.js';
|
||||||
|
import RewardsPage from 'page/rewards.js';
|
||||||
|
import FileListDownloaded from 'page/fileListDownloaded'
|
||||||
|
import FileListPublished from 'page/fileListPublished'
|
||||||
|
import ChannelPage from 'page/channel'
|
||||||
|
import SearchPage from 'page/search'
|
||||||
|
|
||||||
|
const route = (page, routesMap) => {
|
||||||
|
const component = routesMap[page]
|
||||||
|
|
||||||
|
return component
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const Router = (props) => {
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
params,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return route(currentPage, {
|
||||||
|
'settings': <SettingsPage {...params} />,
|
||||||
|
'help': <HelpPage {...params} />,
|
||||||
|
'report': <ReportPage {...params} />,
|
||||||
|
'downloaded': <FileListDownloaded {...params} />,
|
||||||
|
'published': <FileListPublished {...params} />,
|
||||||
|
'start': <StartPage {...params} />,
|
||||||
|
'wallet': <WalletPage {...params} />,
|
||||||
|
'send': <WalletPage {...params} />,
|
||||||
|
'receive': <WalletPage {...params} />,
|
||||||
|
'show': <ShowPage {...params} />,
|
||||||
|
'channel': <ChannelPage {...params} />,
|
||||||
|
'publish': <PublishPage {...params} />,
|
||||||
|
'developer': <DeveloperPage {...params} />,
|
||||||
|
'discover': <DiscoverPage {...params} />,
|
||||||
|
'rewards': <RewardsPage {...params} />,
|
||||||
|
'search': <SearchPage {...params} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Router
|
|
@ -1,57 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbry from '../lbry.js';
|
|
||||||
|
|
||||||
export const SnackBar = React.createClass({
|
|
||||||
|
|
||||||
_displayTime: 5, // in seconds
|
|
||||||
|
|
||||||
_hideTimeout: null,
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
snacks: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSnackReceived: function(event) {
|
|
||||||
// if (this._hideTimeout) {
|
|
||||||
// clearTimeout(this._hideTimeout);
|
|
||||||
// }
|
|
||||||
|
|
||||||
let snacks = this.state.snacks;
|
|
||||||
snacks.push(event.detail);
|
|
||||||
this.setState({ snacks: snacks});
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
document.addEventListener('globalNotice', this.handleSnackReceived);
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
document.removeEventListener('globalNotice', this.handleSnackReceived);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (!this.state.snacks.length) {
|
|
||||||
this._hideTimeout = null; //should be unmounting anyway, but be safe?
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let snack = this.state.snacks[0];
|
|
||||||
|
|
||||||
if (this._hideTimeout === null) {
|
|
||||||
this._hideTimeout = setTimeout(() => {
|
|
||||||
this._hideTimeout = null;
|
|
||||||
let snacks = this.state.snacks;
|
|
||||||
snacks.shift();
|
|
||||||
this.setState({ snacks: snacks });
|
|
||||||
}, this._displayTime * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="snack-bar">
|
|
||||||
{snack.message}
|
|
||||||
{snack.linkText && snack.linkTarget ?
|
|
||||||
<a className="snack-bar__action" href={snack.linkTarget}>{snack.linkText}</a> : ''}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default SnackBar;
|
|
23
ui/js/component/snackBar/index.js
Normal file
23
ui/js/component/snackBar/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
doRemoveSnackBarSnack,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectSnackBarSnacks,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import SnackBar from './view'
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
removeSnack: () => dispatch(doRemoveSnackBarSnack()),
|
||||||
|
})
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
snacks: selectSnackBarSnacks(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(SnackBar)
|
49
ui/js/component/snackBar/view.jsx
Normal file
49
ui/js/component/snackBar/view.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'component/link'
|
||||||
|
|
||||||
|
class SnackBar extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._displayTime = 5; // in seconds
|
||||||
|
this._hideTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
navigate,
|
||||||
|
snacks,
|
||||||
|
removeSnack,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (!snacks.length) {
|
||||||
|
this._hideTimeout = null; //should be unmounting anyway, but be safe?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snack = snacks[0];
|
||||||
|
const {
|
||||||
|
message,
|
||||||
|
linkText,
|
||||||
|
linkTarget,
|
||||||
|
} = snack
|
||||||
|
|
||||||
|
if (this._hideTimeout === null) {
|
||||||
|
this._hideTimeout = setTimeout(() => {
|
||||||
|
this._hideTimeout = null;
|
||||||
|
removeSnack()
|
||||||
|
}, this._displayTime * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="snack-bar">
|
||||||
|
{message}
|
||||||
|
{linkText && linkTarget &&
|
||||||
|
<Link onClick={() => navigate(linkTarget)} className="snack-bar__action" label={linkText} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SnackBar;
|
|
@ -2,21 +2,26 @@ import React from 'react';
|
||||||
import lbry from '../lbry.js';
|
import lbry from '../lbry.js';
|
||||||
import LoadScreen from './load_screen.js';
|
import LoadScreen from './load_screen.js';
|
||||||
|
|
||||||
var SplashScreen = React.createClass({
|
export class SplashScreen extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
message: React.PropTypes.string,
|
message: React.PropTypes.string,
|
||||||
onLoadDone: React.PropTypes.func,
|
onLoadDone: React.PropTypes.func,
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
details: 'Starting daemon',
|
details: 'Starting daemon',
|
||||||
isLagging: false,
|
isLagging: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
updateStatus: function() {
|
updateStatus() {
|
||||||
lbry.status().then(this._updateStatusCallback);
|
lbry.status().then((status) => { this._updateStatusCallback(status) });
|
||||||
},
|
}
|
||||||
_updateStatusCallback: function(status) {
|
|
||||||
|
_updateStatusCallback(status) {
|
||||||
const startupStatus = status.startup_status
|
const startupStatus = status.startup_status
|
||||||
if (startupStatus.code == 'started') {
|
if (startupStatus.code == 'started') {
|
||||||
// Wait until we are able to resolve a name before declaring
|
// Wait until we are able to resolve a name before declaring
|
||||||
|
@ -29,7 +34,6 @@ var SplashScreen = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
lbry.resolve({uri: 'lbry://one'}).then(() => {
|
lbry.resolve({uri: 'lbry://one'}).then(() => {
|
||||||
window.sessionStorage.setItem('loaded', 'y')
|
|
||||||
this.props.onLoadDone();
|
this.props.onLoadDone();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -41,8 +45,9 @@ var SplashScreen = React.createClass({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
}
|
||||||
componentDidMount: function() {
|
|
||||||
|
componentDidMount() {
|
||||||
lbry.connect().then((isConnected) => {
|
lbry.connect().then((isConnected) => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
|
@ -54,10 +59,11 @@ var SplashScreen = React.createClass({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
|
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default SplashScreen;
|
export default SplashScreen;
|
||||||
|
|
23
ui/js/component/subHeader/index.js
Normal file
23
ui/js/component/subHeader/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
selectCurrentPage,
|
||||||
|
selectHeaderLinks,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import SubHeader from './view'
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
currentPage: selectCurrentPage(state),
|
||||||
|
subLinks: selectHeaderLinks(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
navigate: (path) => dispatch(doNavigate(path)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(SubHeader)
|
29
ui/js/component/subHeader/view.jsx
Normal file
29
ui/js/component/subHeader/view.jsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Link from 'component/link'
|
||||||
|
|
||||||
|
const SubHeader = (props) => {
|
||||||
|
const {
|
||||||
|
subLinks,
|
||||||
|
currentPage,
|
||||||
|
navigate,
|
||||||
|
modifier,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const links = []
|
||||||
|
|
||||||
|
for(let link of Object.keys(subLinks)) {
|
||||||
|
links.push(
|
||||||
|
<Link onClick={(event) => navigate(`/${link}`, event)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
|
||||||
|
{subLinks[link]}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={'sub-header' + (modifier ? ' sub-header--' + modifier : '')}>
|
||||||
|
{links}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubHeader
|
|
@ -1,36 +1,44 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export let ToolTip = React.createClass({
|
export class ToolTip extends React.Component {
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
body: React.PropTypes.string.isRequired,
|
body: React.PropTypes.string.isRequired,
|
||||||
label: React.PropTypes.string.isRequired
|
label: React.PropTypes.string.isRequired
|
||||||
},
|
}
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
handleClick: function() {
|
|
||||||
|
handleClick() {
|
||||||
this.setState({
|
this.setState({
|
||||||
showTooltip: !this.state.showTooltip,
|
showTooltip: !this.state.showTooltip,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleTooltipMouseOut: function() {
|
|
||||||
|
handleTooltipMouseOut() {
|
||||||
this.setState({
|
this.setState({
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<span className={'tooltip ' + (this.props.className || '')}>
|
<span className={'tooltip ' + (this.props.className || '')}>
|
||||||
<a className="tooltip__link" onClick={this.handleClick}>
|
<a className="tooltip__link" onClick={() => { this.handleClick() }}>
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</a>
|
</a>
|
||||||
<div className={'tooltip__body ' + (this.state.showTooltip ? '' : ' hidden')}
|
<div className={'tooltip__body ' + (this.state.showTooltip ? '' : ' hidden')}
|
||||||
onMouseOut={this.handleTooltipMouseOut}>
|
onMouseOut={() => { this.handleTooltipMouseOut() }}>
|
||||||
{this.props.body}
|
{this.props.body}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default ToolTip
|
25
ui/js/component/transactionList/index.js
Normal file
25
ui/js/component/transactionList/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doFetchTransactions,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
selectTransactionItems,
|
||||||
|
selectIsFetchingTransactions,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
|
||||||
|
import TransactionList from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||||
|
transactionItems: selectTransactionItems(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
fetchTransactions: () => dispatch(doFetchTransactions())
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(TransactionList)
|
65
ui/js/component/transactionList/view.jsx
Normal file
65
ui/js/component/transactionList/view.jsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
BusyMessage,
|
||||||
|
CreditAmount
|
||||||
|
} from 'component/common';
|
||||||
|
|
||||||
|
class TransactionList extends React.Component{
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.fetchTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fetchingTransactions,
|
||||||
|
transactionItems,
|
||||||
|
} = this.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/#!/transaction?id="+item.id}>{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionList
|
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
|
31
ui/js/component/uriIndicator/index.js
Normal file
31
ui/js/component/uriIndicator/index.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react'
|
||||||
|
import lbryuri from 'lbryuri';
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
makeSelectIsResolvingForUri
|
||||||
|
} from 'selectors/content'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import UriIndicator from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaim = makeSelectClaimForUri(),
|
||||||
|
selectIsResolving = makeSelectIsResolvingForUri();
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaim(state, props),
|
||||||
|
isResolvingUri: selectIsResolving(state, props),
|
||||||
|
uri: lbryuri.normalize(props.uri),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
resolveUri: (uri) => dispatch(doResolveUri(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(UriIndicator)
|
70
ui/js/component/uriIndicator/view.jsx
Normal file
70
ui/js/component/uriIndicator/view.jsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Icon} from 'component/common';
|
||||||
|
|
||||||
|
class UriIndicator extends React.Component{
|
||||||
|
componentWillMount() {
|
||||||
|
this.resolve(this.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.resolve(nextProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(props) {
|
||||||
|
const {
|
||||||
|
isResolvingUri,
|
||||||
|
resolveUri,
|
||||||
|
claim,
|
||||||
|
uri,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if(!isResolvingUri && claim === undefined && uri) {
|
||||||
|
resolveUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
uri,
|
||||||
|
isResolvingUri
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (isResolvingUri) {
|
||||||
|
return <span className="empty">Validating...</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
return <span className="empty">Unused</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
channel_name: channelName,
|
||||||
|
has_signature: hasSignature,
|
||||||
|
signature_is_valid: signatureIsValid,
|
||||||
|
} = claim
|
||||||
|
|
||||||
|
if (!hasSignature || !channelName) {
|
||||||
|
return <span className="empty">Anonymous</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon, modifier;
|
||||||
|
if (signatureIsValid) {
|
||||||
|
modifier = 'valid';
|
||||||
|
} else {
|
||||||
|
icon = 'icon-times-circle';
|
||||||
|
modifier = 'invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{channelName} {' '}
|
||||||
|
{ !signatureIsValid ?
|
||||||
|
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
|
||||||
|
'' }
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UriIndicator;
|
54
ui/js/component/video/index.js
Normal file
54
ui/js/component/video/index.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect,
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
doPurchaseUri,
|
||||||
|
doLoadVideo,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
makeSelectMetadataForUri
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectLoadingForUri,
|
||||||
|
makeSelectDownloadingForUri,
|
||||||
|
} from 'selectors/file_info'
|
||||||
|
import {
|
||||||
|
makeSelectCostInfoForUri,
|
||||||
|
} from 'selectors/cost_info'
|
||||||
|
import Video from './view'
|
||||||
|
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectCostInfo = makeSelectCostInfoForUri()
|
||||||
|
const selectFileInfo = makeSelectFileInfoForUri()
|
||||||
|
const selectIsLoading = makeSelectLoadingForUri()
|
||||||
|
const selectIsDownloading = makeSelectDownloadingForUri()
|
||||||
|
const selectMetadata = makeSelectMetadataForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
costInfo: selectCostInfo(state, props),
|
||||||
|
fileInfo: selectFileInfo(state, props),
|
||||||
|
metadata: selectMetadata(state, props),
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
isLoading: selectIsLoading(state, props),
|
||||||
|
isDownloading: selectIsDownloading(state, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
loadVideo: (uri) => dispatch(doLoadVideo(uri)),
|
||||||
|
purchaseUri: (uri) => dispatch(doPurchaseUri(uri)),
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(Video)
|
151
ui/js/component/video/view.jsx
Normal file
151
ui/js/component/video/view.jsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import React from 'react';
|
||||||
|
import FilePrice from 'component/filePrice'
|
||||||
|
import Link from 'component/link';
|
||||||
|
import Modal from 'component/modal';
|
||||||
|
|
||||||
|
class VideoPlayButton extends React.Component {
|
||||||
|
onPurchaseConfirmed() {
|
||||||
|
this.props.closeModal()
|
||||||
|
this.props.startPlaying()
|
||||||
|
this.props.loadVideo(this.props.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
onWatchClick() {
|
||||||
|
this.props.purchaseUri(this.props.uri).then(() => {
|
||||||
|
if (!this.props.modal) {
|
||||||
|
this.props.startPlaying()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
button,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
metadata,
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
uri,
|
||||||
|
modal,
|
||||||
|
closeModal,
|
||||||
|
isLoading,
|
||||||
|
costInfo,
|
||||||
|
fileInfo,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
/*
|
||||||
|
title={
|
||||||
|
isLoading ? "Video is Loading" :
|
||||||
|
!costInfo ? "Waiting on cost info..." :
|
||||||
|
fileInfo === undefined ? "Waiting on file info..." : ""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<Link button={ button ? button : null }
|
||||||
|
disabled={isLoading || fileInfo === undefined || (fileInfo === null && (!costInfo || costInfo.cost === undefined))}
|
||||||
|
label={label ? label : ""}
|
||||||
|
className="video__play-button"
|
||||||
|
icon="icon-play"
|
||||||
|
onClick={this.onWatchClick.bind(this)} />
|
||||||
|
{modal}
|
||||||
|
<Modal contentLabel="Not enough credits" isOpen={modal == 'notEnoughCredits'} onConfirmed={() => { this.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.onPurchaseConfirmed.bind(this)}
|
||||||
|
onAborted={closeModal}>
|
||||||
|
This will purchase <strong>{title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits.
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={modal == 'timedOut'} onConfirmed={() => { this.closeModal() }} contentLabel="Timed Out">
|
||||||
|
Sorry, your download timed out :(
|
||||||
|
</Modal>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plyr = require('plyr')
|
||||||
|
|
||||||
|
class Video extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = { isPlaying: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
startPlaying() {
|
||||||
|
this.setState({
|
||||||
|
isPlaying: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
metadata,
|
||||||
|
isLoading,
|
||||||
|
isDownloading,
|
||||||
|
fileInfo,
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
isPlaying = false,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0
|
||||||
|
|
||||||
|
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 className={"video " + this.props.className + (isPlaying ? " video--active" : " video--hidden")}>{
|
||||||
|
isPlaying || isLoading ?
|
||||||
|
(!isReadyToPlay ?
|
||||||
|
<span>this is the world's worst loading screen and we shipped our software with it anyway... <br /><br />{loadStatusMessage}</span> :
|
||||||
|
<VideoPlayer poster={metadata.thumbnail} autoplay={isPlaying} downloadPath={fileInfo.download_path} />) :
|
||||||
|
<div className="video__cover" style={{backgroundImage: 'url("' + metadata.thumbnail + '")'}}>
|
||||||
|
<VideoPlayButton startPlaying={this.startPlaying.bind(this)} {...this.props} />
|
||||||
|
</div>
|
||||||
|
}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoPlayer extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const elem = this.refs.video
|
||||||
|
const {
|
||||||
|
autoplay,
|
||||||
|
downloadPath,
|
||||||
|
contentType,
|
||||||
|
} = this.props
|
||||||
|
const players = plyr.setup(elem)
|
||||||
|
if (autoplay) {
|
||||||
|
players[0].play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
downloadPath,
|
||||||
|
contentType,
|
||||||
|
poster,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video controls id="video" ref="video" style={{backgroundImage: "url('" + poster + "')"}} >
|
||||||
|
<source src={downloadPath} type={contentType} />
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Video
|
25
ui/js/component/walletAddress/index.js
Normal file
25
ui/js/component/walletAddress/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doCheckAddressIsMine,
|
||||||
|
doGetNewAddress,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
selectReceiveAddress,
|
||||||
|
selectGettingNewAddress
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
import WalletPage from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
receiveAddress: selectReceiveAddress(state),
|
||||||
|
gettingNewAddress: selectGettingNewAddress(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
|
||||||
|
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(WalletPage)
|
41
ui/js/component/walletAddress/view.jsx
Normal file
41
ui/js/component/walletAddress/view.jsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import {
|
||||||
|
Address
|
||||||
|
} from 'component/common';
|
||||||
|
|
||||||
|
class WalletAddress 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WalletAddress
|
36
ui/js/component/walletSend/index.js
Normal file
36
ui/js/component/walletSend/index.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doCloseModal,
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doSendDraftTransaction,
|
||||||
|
doSetDraftTransactionAmount,
|
||||||
|
doSetDraftTransactionAddress,
|
||||||
|
} from 'actions/wallet'
|
||||||
|
import {
|
||||||
|
selectCurrentModal,
|
||||||
|
} from 'selectors/app'
|
||||||
|
import {
|
||||||
|
selectDraftTransactionAmount,
|
||||||
|
selectDraftTransactionAddress,
|
||||||
|
} from 'selectors/wallet'
|
||||||
|
|
||||||
|
import WalletSend from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
modal: selectCurrentModal(state),
|
||||||
|
address: selectDraftTransactionAddress(state),
|
||||||
|
amount: selectDraftTransactionAmount(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
sendToAddress: () => dispatch(doSendDraftTransaction()),
|
||||||
|
setAmount: (event) => dispatch(doSetDraftTransactionAmount(event.target.value)),
|
||||||
|
setAddress: (event) => dispatch(doSetDraftTransactionAddress(event.target.value)),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(WalletSend)
|
49
ui/js/component/walletSend/view.jsx
Normal file
49
ui/js/component/walletSend/view.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import Modal from 'component/modal';
|
||||||
|
import {
|
||||||
|
FormRow
|
||||||
|
} from 'component/form';
|
||||||
|
|
||||||
|
const WalletSend = (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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WalletSend
|
25
ui/js/component/wunderbar/index.js
Normal file
25
ui/js/component/wunderbar/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import lbryuri from 'lbryuri.js'
|
||||||
|
import {
|
||||||
|
selectWunderBarAddress,
|
||||||
|
selectWunderBarIcon
|
||||||
|
} from 'selectors/search'
|
||||||
|
import {
|
||||||
|
doNavigate,
|
||||||
|
} from 'actions/app'
|
||||||
|
import Wunderbar from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
address: selectWunderBarAddress(state),
|
||||||
|
icon: selectWunderBarIcon(state)
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
onSearch: (query) => dispatch(doNavigate('/search', { query, })),
|
||||||
|
onSubmit: (query) => dispatch(doNavigate('/show', { uri: lbryuri.normalize(query) } ))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(Wunderbar)
|
160
ui/js/component/wunderbar/view.jsx
Normal file
160
ui/js/component/wunderbar/view.jsx
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbryuri from 'lbryuri.js';
|
||||||
|
import {Icon} from 'component/common.js';
|
||||||
|
|
||||||
|
class WunderBar extends React.PureComponent {
|
||||||
|
static TYPING_TIMEOUT = 800
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onSearch: React.PropTypes.func.isRequired,
|
||||||
|
onSubmit: React.PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._userTypingTimer = null;
|
||||||
|
this._isSearchDispatchPending = false;
|
||||||
|
this._input = null;
|
||||||
|
this._stateBeforeSearch = null;
|
||||||
|
this._resetOnNextBlur = true;
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.onFocus = this.onFocus.bind(this);
|
||||||
|
this.onBlur = this.onBlur.bind(this);
|
||||||
|
this.onKeyPress = this.onKeyPress.bind(this);
|
||||||
|
this.onReceiveRef = this.onReceiveRef.bind(this);
|
||||||
|
this.state = {
|
||||||
|
address: this.props.address,
|
||||||
|
icon: this.props.icon
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.userTypingTimer) {
|
||||||
|
clearTimeout(this._userTypingTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(event) {
|
||||||
|
|
||||||
|
if (this._userTypingTimer)
|
||||||
|
{
|
||||||
|
clearTimeout(this._userTypingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ address: event.target.value })
|
||||||
|
|
||||||
|
this._isSearchDispatchPending = true;
|
||||||
|
|
||||||
|
let searchQuery = event.target.value;
|
||||||
|
|
||||||
|
this._userTypingTimer = setTimeout(() => {
|
||||||
|
const hasQuery = searchQuery.length === 0;
|
||||||
|
this._resetOnNextBlur = hasQuery;
|
||||||
|
this._isSearchDispatchPending = false;
|
||||||
|
if (searchQuery) {
|
||||||
|
this.props.onSearch(searchQuery);
|
||||||
|
}
|
||||||
|
}, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
|
||||||
|
this.setState({ address: nextProps.address, icon: nextProps.icon });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus() {
|
||||||
|
this._stateBeforeSearch = this.state;
|
||||||
|
let newState = {
|
||||||
|
icon: "icon-search",
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
this._focusPending = true;
|
||||||
|
//below is hacking, improved when we have proper routing
|
||||||
|
if (!this.state.address.startsWith('lbry://') && this.state.icon !== "icon-search") //onFocus, if they are not on an exact URL or a search page, clear the bar
|
||||||
|
{
|
||||||
|
newState.address = '';
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur() {
|
||||||
|
if (this._isSearchDispatchPending) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onBlur();
|
||||||
|
}, WunderBar.TYPING_TIMEOUT + 1)
|
||||||
|
} else {
|
||||||
|
let commonState = {isActive: false};
|
||||||
|
if (this._resetOnNextBlur) {
|
||||||
|
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
|
||||||
|
this._input.value = this.state.address;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._resetOnNextBlur = true;
|
||||||
|
this._stateBeforeSearch = this.state;
|
||||||
|
this.setState(commonState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this._input) {
|
||||||
|
const start = this._input.selectionStart,
|
||||||
|
end = this._input.selectionEnd;
|
||||||
|
|
||||||
|
this._input.value = this.state.address; //this causes cursor to go to end of input
|
||||||
|
|
||||||
|
this._input.setSelectionRange(start, end);
|
||||||
|
|
||||||
|
if (this._focusPending) {
|
||||||
|
this._input.select();
|
||||||
|
this._focusPending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress(event) {
|
||||||
|
if (event.charCode == 13 && this._input.value) {
|
||||||
|
|
||||||
|
let uri = null,
|
||||||
|
method = "onSubmit";
|
||||||
|
|
||||||
|
this._resetOnNextBlur = false;
|
||||||
|
clearTimeout(this._userTypingTimer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
uri = lbryuri.normalize(this._input.value);
|
||||||
|
this.setState({ value: uri });
|
||||||
|
} catch (error) { //then it's not a valid URL, so let's search
|
||||||
|
uri = this._input.value;
|
||||||
|
method = "onSearch";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props[method](uri);
|
||||||
|
this._input.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReceiveRef(ref) {
|
||||||
|
this._input = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
|
||||||
|
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
|
||||||
|
<input className="wunderbar__input" type="search"
|
||||||
|
ref={this.onReceiveRef}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onKeyPress={this.onKeyPress}
|
||||||
|
value={this.state.address}
|
||||||
|
placeholder="Find movies, music, games, and more" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WunderBar;
|
2
ui/js/config/development.js
Normal file
2
ui/js/config/development.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports = {
|
||||||
|
}
|
2
ui/js/config/production.js
Normal file
2
ui/js/config/production.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports = {
|
||||||
|
}
|
68
ui/js/constants/action_types.js
Normal file
68
ui/js/constants/action_types.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
export const CHANGE_PATH = 'CHANGE_PATH'
|
||||||
|
export const OPEN_MODAL = 'OPEN_MODAL'
|
||||||
|
export const CLOSE_MODAL = 'CLOSE_MODAL'
|
||||||
|
export const HISTORY_BACK = 'HISTORY_BACK'
|
||||||
|
export const SHOW_SNACKBAR = 'SHOW_SNACKBAR'
|
||||||
|
export const REMOVE_SNACKBAR_SNACK = 'REMOVE_SNACKBAR_SNACK'
|
||||||
|
|
||||||
|
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 RESOLVE_URI_CANCELED = 'RESOLVE_URI_CANCELED'
|
||||||
|
export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED'
|
||||||
|
export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED'
|
||||||
|
export const CLAIM_LIST_MINE_STARTED = 'CLAIM_LIST_MINE_STARTED'
|
||||||
|
export const CLAIM_LIST_MINE_COMPLETED = 'CLAIM_LIST_MINE_COMPLETED'
|
||||||
|
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'
|
||||||
|
export const FILE_LIST_COMPLETED = 'FILE_LIST_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 FILE_DELETE = 'FILE_DELETE'
|
||||||
|
|
||||||
|
// Search
|
||||||
|
export const SEARCH_STARTED = 'SEARCH_STARTED'
|
||||||
|
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'
|
||||||
|
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'
|
|
@ -68,6 +68,8 @@ jsonrpc.call = function (connectionString, method, params, callback, errorCallba
|
||||||
}));
|
}));
|
||||||
|
|
||||||
sessionStorage.setItem('JSONRPCCounter', counter + 1);
|
sessionStorage.setItem('JSONRPCCounter', counter + 1);
|
||||||
|
|
||||||
|
return xhr
|
||||||
};
|
};
|
||||||
|
|
||||||
export default jsonrpc;
|
export default jsonrpc;
|
||||||
|
|
313
ui/js/lbry.js
313
ui/js/lbry.js
|
@ -7,6 +7,20 @@ import {getLocal, getSession, setSession, setLocal} from './utils.js';
|
||||||
const {remote, ipcRenderer} = require('electron');
|
const {remote, ipcRenderer} = require('electron');
|
||||||
const menu = remote.require('./menu/main-menu');
|
const menu = remote.require('./menu/main-menu');
|
||||||
|
|
||||||
|
let lbry = {
|
||||||
|
isConnected: false,
|
||||||
|
daemonConnectionString: 'http://localhost:5279/lbryapi',
|
||||||
|
pendingPublishTimeout: 20 * 60 * 1000,
|
||||||
|
defaultClientSettings: {
|
||||||
|
showNsfw: false,
|
||||||
|
showUnavailable: true,
|
||||||
|
debug: false,
|
||||||
|
useCustomLighthouseServers: false,
|
||||||
|
customLighthouseServers: [],
|
||||||
|
showDeveloperMenu: false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
|
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
|
||||||
* needed to make a dummy claim or file info object.
|
* needed to make a dummy claim or file info object.
|
||||||
|
@ -40,14 +54,14 @@ function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
|
||||||
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
|
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
|
setLocal('pendingPublishes', lbry.getPendingPublishes().filter(pub => !pubMatches(pub)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current list of pending publish attempts. Filters out any that have timed out and
|
* Gets the current list of pending publish attempts. Filters out any that have timed out and
|
||||||
* removes them from the list.
|
* removes them from the list.
|
||||||
*/
|
*/
|
||||||
function getPendingPublishes() {
|
lbry.getPendingPublishes = function() {
|
||||||
const pendingPublishes = getLocal('pendingPublishes') || [];
|
const pendingPublishes = getLocal('pendingPublishes') || [];
|
||||||
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
|
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
|
||||||
setLocal('pendingPublishes', newPendingPublishes);
|
setLocal('pendingPublishes', newPendingPublishes);
|
||||||
|
@ -59,7 +73,7 @@ function getPendingPublishes() {
|
||||||
* provided along withe the name. If no pending publish is found, returns null.
|
* provided along withe the name. If no pending publish is found, returns null.
|
||||||
*/
|
*/
|
||||||
function getPendingPublish({name, channel_name, outpoint}) {
|
function getPendingPublish({name, channel_name, outpoint}) {
|
||||||
const pendingPublishes = getPendingPublishes();
|
const pendingPublishes = lbry.getPendingPublishes();
|
||||||
return pendingPublishes.find(
|
return pendingPublishes.find(
|
||||||
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
|
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
|
||||||
) || null;
|
) || null;
|
||||||
|
@ -72,30 +86,9 @@ function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txi
|
||||||
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
|
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
|
||||||
return {name, outpoint, claim_id, metadata: null};
|
return {name, outpoint, claim_id, metadata: null};
|
||||||
}
|
}
|
||||||
window.pptdfi = pendingPublishToDummyFileInfo;
|
|
||||||
|
|
||||||
let lbry = {
|
|
||||||
isConnected: false,
|
|
||||||
rootPath: '.',
|
|
||||||
daemonConnectionString: 'http://localhost:5279/lbryapi',
|
|
||||||
webUiUri: 'http://localhost:5279',
|
|
||||||
peerListTimeout: 6000,
|
|
||||||
pendingPublishTimeout: 20 * 60 * 1000,
|
|
||||||
colors: {
|
|
||||||
primary: '#155B4A'
|
|
||||||
},
|
|
||||||
defaultClientSettings: {
|
|
||||||
showNsfw: false,
|
|
||||||
showUnavailable: true,
|
|
||||||
debug: false,
|
|
||||||
useCustomLighthouseServers: false,
|
|
||||||
customLighthouseServers: [],
|
|
||||||
showDeveloperMenu: false,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
|
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
|
||||||
jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
|
return jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
//core
|
//core
|
||||||
|
@ -131,93 +124,19 @@ lbry.connect = function() {
|
||||||
return lbry._connectPromise;
|
return lbry._connectPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//kill this but still better than document.title =, which this replaced
|
|
||||||
lbry.setTitle = function(title) {
|
|
||||||
document.title = title + " - LBRY";
|
|
||||||
}
|
|
||||||
|
|
||||||
//kill this with proper routing
|
|
||||||
lbry.back = function() {
|
|
||||||
if (window.history.length > 1) {
|
|
||||||
window.history.back();
|
|
||||||
} else {
|
|
||||||
window.location.href = "?discover";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.isDaemonAcceptingConnections = function (callback) {
|
lbry.isDaemonAcceptingConnections = function (callback) {
|
||||||
// Returns true/false whether the daemon is at a point it will start returning status
|
// Returns true/false whether the daemon is at a point it will start returning status
|
||||||
lbry.call('status', {}, () => callback(true), null, () => callback(false))
|
lbry.call('status', {}, () => callback(true), null, () => callback(false))
|
||||||
};
|
};
|
||||||
|
|
||||||
lbry.checkFirstRun = function(callback) {
|
|
||||||
lbry.call('is_first_run', {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getNewAddress = function(callback) {
|
|
||||||
lbry.call('wallet_new_address', {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getUnusedAddress = function(callback) {
|
|
||||||
lbry.call('wallet_unused_address', {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.checkAddressIsMine = function(address, callback) {
|
lbry.checkAddressIsMine = function(address, callback) {
|
||||||
lbry.call('address_is_mine', {address: address}, callback);
|
lbry.call('address_is_mine', {address: address}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
lbry.getDaemonSettings = function(callback) {
|
|
||||||
lbry.call('get_settings', {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.setDaemonSettings = function(settings, callback) {
|
|
||||||
lbry.call('set_settings', settings, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.setDaemonSetting = function(setting, value, callback) {
|
|
||||||
var setSettingsArgs = {};
|
|
||||||
setSettingsArgs[setting] = value;
|
|
||||||
lbry.call('set_settings', setSettingsArgs, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
lbry.getBalance = function(callback) {
|
|
||||||
lbry.call("wallet_balance", {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.sendToAddress = function(amount, address, callback, errorCallback) {
|
lbry.sendToAddress = function(amount, address, callback, errorCallback) {
|
||||||
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
|
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
lbry.getClaimInfo = function(name, callback) {
|
|
||||||
if (!name) {
|
|
||||||
throw new Error(`Name required.`);
|
|
||||||
}
|
|
||||||
lbry.call('get_claim_info', { name: name }, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getMyClaim = function(name, callback) {
|
|
||||||
lbry.call('claim_list_mine', {}, (claims) => {
|
|
||||||
callback(claims.find((claim) => claim.name == name) || null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getPeersForBlobHash = function(blobHash, callback) {
|
|
||||||
let timedOut = false;
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
timedOut = true;
|
|
||||||
callback([]);
|
|
||||||
}, lbry.peerListTimeout);
|
|
||||||
|
|
||||||
lbry.call('peer_list', { blob_hash: blobHash }, function(peers) {
|
|
||||||
if (!timedOut) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
callback(peers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a LBRY URI; will first try and calculate a total cost using
|
* Takes a LBRY URI; will first try and calculate a total cost using
|
||||||
* Lighthouse. If Lighthouse can't be reached, it just retrives the
|
* Lighthouse. If Lighthouse can't be reached, it just retrives the
|
||||||
|
@ -255,30 +174,9 @@ lbry.getCostInfo = function(uri) {
|
||||||
}, reject);
|
}, reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCostGenerous(uri) {
|
|
||||||
// If generous is on, the calculation is simple enough that we might as well do it here in the front end
|
|
||||||
lbry.resolve({uri: uri}).then((resolutionInfo) => {
|
|
||||||
const fee = resolutionInfo.claim.value.stream.metadata.fee;
|
|
||||||
if (fee === undefined) {
|
|
||||||
cacheAndResolve(0, true);
|
|
||||||
} else if (fee.currency == 'LBC') {
|
|
||||||
cacheAndResolve(fee.amount, true);
|
|
||||||
} else {
|
|
||||||
lbryio.getExchangeRates().then(({lbc_usd}) => {
|
|
||||||
cacheAndResolve(fee.amount / lbc_usd, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const uriObj = lbryuri.parse(uri);
|
const uriObj = lbryuri.parse(uri);
|
||||||
const name = uriObj.path || uriObj.name;
|
const name = uriObj.path || uriObj.name;
|
||||||
|
|
||||||
lbry.settings_get({allow_cached: true}).then(({is_generous_host}) => {
|
|
||||||
if (is_generous_host) {
|
|
||||||
return getCostGenerous(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
lighthouse.get_size_for_name(name).then((size) => {
|
lighthouse.get_size_for_name(name).then((size) => {
|
||||||
if (size) {
|
if (size) {
|
||||||
getCost(name, size);
|
getCost(name, size);
|
||||||
|
@ -286,49 +184,12 @@ lbry.getCostInfo = function(uri) {
|
||||||
else {
|
else {
|
||||||
getCost(name, null);
|
getCost(name, null);
|
||||||
}
|
}
|
||||||
}, () => {
|
})
|
||||||
getCost(name, null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return lbry.costPromiseCache[uri];
|
return lbry.costPromiseCache[uri];
|
||||||
}
|
}
|
||||||
|
|
||||||
lbry.getMyClaims = function(callback) {
|
|
||||||
lbry.call('get_name_claims', {}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
|
|
||||||
this._removedFiles.push(outpoint);
|
|
||||||
this._updateFileInfoSubscribers(outpoint);
|
|
||||||
|
|
||||||
lbry.file_delete({
|
|
||||||
outpoint: outpoint,
|
|
||||||
delete_target_file: deleteTargetFile,
|
|
||||||
}).then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
|
|
||||||
function scheduleNextCheckOrTimeout() {
|
|
||||||
if (timeoutCallback && tryNum > 200) {
|
|
||||||
timeoutCallback();
|
|
||||||
} else {
|
|
||||||
setTimeout(() => lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1), 250);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls callback with file info when it appears in the lbrynet file manager.
|
|
||||||
// If timeoutCallback is provided, it will be called if the file fails to appear.
|
|
||||||
lbry.file_list({name: name}).then(([fileInfo]) => {
|
|
||||||
if (fileInfo) {
|
|
||||||
callback(fileInfo);
|
|
||||||
} else {
|
|
||||||
scheduleNextCheckOrTimeout();
|
|
||||||
}
|
|
||||||
}, () => scheduleNextCheckOrTimeout());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
|
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
|
||||||
* lbry.file_list() during the publish process.
|
* lbry.file_list() during the publish process.
|
||||||
|
@ -369,10 +230,6 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
|
||||||
fileListedCallback(true);
|
fileListedCallback(true);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
//lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
|
|
||||||
// fileListedCallback(fileInfo);
|
|
||||||
//});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -427,29 +284,10 @@ lbry.formatName = function(name) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
lbry.nameIsValid = function(name, checkCase=true) {
|
|
||||||
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
|
|
||||||
return regexp.test(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.loadJs = function(src, type, onload)
|
|
||||||
{
|
|
||||||
var lbryScriptTag = document.getElementById('lbry'),
|
|
||||||
newScriptTag = document.createElement('script'),
|
|
||||||
type = type || 'text/javascript';
|
|
||||||
|
|
||||||
newScriptTag.src = src;
|
|
||||||
newScriptTag.type = type;
|
|
||||||
if (onload)
|
|
||||||
{
|
|
||||||
newScriptTag.onload = onload;
|
|
||||||
}
|
|
||||||
lbryScriptTag.parentNode.insertBefore(newScriptTag, lbryScriptTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.imagePath = function(file)
|
lbry.imagePath = function(file)
|
||||||
{
|
{
|
||||||
return lbry.rootPath + '/img/' + file;
|
return 'img/' + file;
|
||||||
}
|
}
|
||||||
|
|
||||||
lbry.getMediaType = function(contentType, fileName) {
|
lbry.getMediaType = function(contentType, fileName) {
|
||||||
|
@ -480,71 +318,9 @@ lbry.stop = function(callback) {
|
||||||
lbry.call('stop', {}, callback);
|
lbry.call('stop', {}, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
lbry.fileInfo = {};
|
|
||||||
lbry._subscribeIdCount = 0;
|
lbry._subscribeIdCount = 0;
|
||||||
lbry._fileInfoSubscribeCallbacks = {};
|
|
||||||
lbry._fileInfoSubscribeInterval = 500000;
|
|
||||||
lbry._balanceSubscribeCallbacks = {};
|
lbry._balanceSubscribeCallbacks = {};
|
||||||
lbry._balanceSubscribeInterval = 5000;
|
lbry._balanceSubscribeInterval = 5000;
|
||||||
lbry._removedFiles = [];
|
|
||||||
lbry._claimIdOwnershipCache = {};
|
|
||||||
|
|
||||||
lbry._updateClaimOwnershipCache = function(claimId) {
|
|
||||||
lbry.getMyClaims((claimInfos) => {
|
|
||||||
lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) {
|
|
||||||
return match || claimInfo.claim_id == claimId;
|
|
||||||
}, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
lbry._updateFileInfoSubscribers = function(outpoint) {
|
|
||||||
const callSubscribedCallbacks = (outpoint, fileInfo) => {
|
|
||||||
for (let callback of Object.values(this._fileInfoSubscribeCallbacks[outpoint])) {
|
|
||||||
callback(fileInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lbry._removedFiles.includes(outpoint)) {
|
|
||||||
callSubscribedCallbacks(outpoint, false);
|
|
||||||
} else {
|
|
||||||
lbry.file_list({
|
|
||||||
outpoint: outpoint,
|
|
||||||
full_status: true,
|
|
||||||
}).then(([fileInfo]) => {
|
|
||||||
if (fileInfo) {
|
|
||||||
if (this._claimIdOwnershipCache[fileInfo.claim_id] === undefined) {
|
|
||||||
this._updateClaimOwnershipCache(fileInfo.claim_id);
|
|
||||||
}
|
|
||||||
fileInfo.isMine = !!this._claimIdOwnershipCache[fileInfo.claim_id];
|
|
||||||
}
|
|
||||||
|
|
||||||
callSubscribedCallbacks(outpoint, fileInfo);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this._updateFileInfoSubscribers(outpoint);
|
|
||||||
}, lbry._fileInfoSubscribeInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.fileInfoSubscribe = function(outpoint, callback) {
|
|
||||||
if (!lbry._fileInfoSubscribeCallbacks[outpoint])
|
|
||||||
{
|
|
||||||
lbry._fileInfoSubscribeCallbacks[outpoint] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscribeId = ++lbry._subscribeIdCount;
|
|
||||||
lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback;
|
|
||||||
lbry._updateFileInfoSubscribers(outpoint);
|
|
||||||
return subscribeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) {
|
|
||||||
delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
lbry._balanceUpdateInterval = null;
|
lbry._balanceUpdateInterval = null;
|
||||||
lbry._updateBalanceSubscribers = function() {
|
lbry._updateBalanceSubscribers = function() {
|
||||||
|
@ -584,7 +360,7 @@ lbry.showMenuIfNeeded = function() {
|
||||||
sessionStorage.setItem('menuShown', chosenMenu);
|
sessionStorage.setItem('menuShown', chosenMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
lbry.getVersionInfo = function() {
|
lbry.getAppVersionInfo = function() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) });
|
ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) });
|
||||||
ipcRenderer.send('version-info-requested');
|
ipcRenderer.send('version-info-requested');
|
||||||
|
@ -621,7 +397,7 @@ lbry.file_list = function(params={}) {
|
||||||
lbry.call('file_list', params, (fileInfos) => {
|
lbry.call('file_list', params, (fileInfos) => {
|
||||||
removePendingPublishIfNeeded({name, channel_name, outpoint});
|
removePendingPublishIfNeeded({name, channel_name, outpoint});
|
||||||
|
|
||||||
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
|
const dummyFileInfos = lbry.getPendingPublishes().map(pendingPublishToDummyFileInfo);
|
||||||
resolve([...fileInfos, ...dummyFileInfos]);
|
resolve([...fileInfos, ...dummyFileInfos]);
|
||||||
}, reject, reject);
|
}, reject, reject);
|
||||||
});
|
});
|
||||||
|
@ -634,56 +410,41 @@ lbry.claim_list_mine = function(params={}) {
|
||||||
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
|
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
|
const dummyClaims = lbry.getPendingPublishes().map(pendingPublishToDummyClaim);
|
||||||
resolve([...claims, ...dummyClaims]);
|
resolve([...claims, ...dummyClaims]);
|
||||||
}, reject, reject)
|
}, reject, reject)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const claimCacheKey = 'resolve_claim_cache';
|
||||||
|
lbry._claimCache = getSession(claimCacheKey, {});
|
||||||
|
lbry._resolveXhrs = {}
|
||||||
lbry.resolve = function(params={}) {
|
lbry.resolve = function(params={}) {
|
||||||
const claimCacheKey = 'resolve_claim_cache',
|
|
||||||
claimCache = getSession(claimCacheKey, {})
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!params.uri) {
|
if (!params.uri) {
|
||||||
throw "Resolve has hacked cache on top of it that requires a URI"
|
throw "Resolve has hacked cache on top of it that requires a URI"
|
||||||
}
|
}
|
||||||
if (params.uri && claimCache[params.uri] !== undefined) {
|
if (params.uri && lbry._claimCache[params.uri] !== undefined) {
|
||||||
resolve(claimCache[params.uri]);
|
resolve(lbry._claimCache[params.uri]);
|
||||||
} else {
|
} else {
|
||||||
lbry.call('resolve', params, function(data) {
|
lbry._resolveXhrs[params.uri] = lbry.call('resolve', params, function(data) {
|
||||||
claimCache[params.uri] = data;
|
if (data !== undefined) {
|
||||||
setSession(claimCacheKey, claimCache)
|
lbry._claimCache[params.uri] = data;
|
||||||
|
}
|
||||||
|
setSession(claimCacheKey, lbry._claimCache)
|
||||||
resolve(data)
|
resolve(data)
|
||||||
}, reject)
|
}, reject)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds caching.
|
lbry.cancelResolve = function(params={}) {
|
||||||
lbry.settings_get = function(params={}) {
|
const xhr = lbry._resolveXhrs[params.uri]
|
||||||
return new Promise((resolve, reject) => {
|
if (xhr && xhr.readyState > 0 && xhr.readyState < 4) {
|
||||||
if (params.allow_cached) {
|
xhr.abort()
|
||||||
const cached = getSession('settings');
|
|
||||||
if (cached) {
|
|
||||||
return resolve(cached);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lbry.call('settings_get', {}, (settings) => {
|
|
||||||
setSession('settings', settings);
|
|
||||||
resolve(settings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lbry.get = function(params={}) {
|
|
||||||
// return function(params={}) {
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// jsonrpc.call(lbry.daemonConnectionString, "get", params, resolve, reject, reject);
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
lbry = new Proxy(lbry, {
|
lbry = new Proxy(lbry, {
|
||||||
get: function(target, name) {
|
get: function(target, name) {
|
||||||
if (name in target) {
|
if (name in target) {
|
||||||
|
|
|
@ -7,33 +7,34 @@ const lbryio = {
|
||||||
_accessToken: getLocal('accessToken'),
|
_accessToken: getLocal('accessToken'),
|
||||||
_authenticationPromise: null,
|
_authenticationPromise: null,
|
||||||
_user : null,
|
_user : null,
|
||||||
enabled: false
|
enabled: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
|
|
||||||
|
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ?
|
||||||
|
process.env.LBRY_APP_API_URL.replace(/\/*$/,'/') : // exactly one slash at the end
|
||||||
|
'https://api.lbry.io/'
|
||||||
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
||||||
|
|
||||||
|
lbryio._exchangePromise = null;
|
||||||
|
lbryio._exchangeLastFetched = null;
|
||||||
lbryio.getExchangeRates = function() {
|
lbryio.getExchangeRates = function() {
|
||||||
return new Promise((resolve, reject) => {
|
if (!lbryio._exchangeLastFetched || Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) {
|
||||||
const cached = getSession('exchangeRateCache');
|
lbryio._exchangePromise = new Promise((resolve, reject) => {
|
||||||
if (!cached || Date.now() - cached.time > EXCHANGE_RATE_TIMEOUT) {
|
|
||||||
lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => {
|
lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => {
|
||||||
const rates = {lbc_usd, lbc_btc, btc_usd};
|
const rates = {lbc_usd, lbc_btc, btc_usd};
|
||||||
setSession('exchangeRateCache', {
|
|
||||||
rates: rates,
|
|
||||||
time: Date.now(),
|
|
||||||
});
|
|
||||||
resolve(rates);
|
resolve(rates);
|
||||||
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
} else {
|
lbryio._exchangeLastFetched = Date.now();
|
||||||
resolve(cached.rates);
|
|
||||||
}
|
}
|
||||||
});
|
return lbryio._exchangePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled=false) { // evenIfDisabled is just for development, when we may have some calls working and some not
|
lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled=false) { // evenIfDisabled is just for development, when we may have some calls working and some not
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!lbryio.enabled && !evenIfDisabled && (resource != 'discover' || action != 'list')) {
|
if (!lbryio.enabled && !evenIfDisabled && (resource != 'discover' || action != 'list')) {
|
||||||
|
console.log("Internal API disabled");
|
||||||
reject(new Error("LBRY internal API is disabled"))
|
reject(new Error("LBRY internal API is disabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -87,6 +88,8 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled
|
||||||
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
|
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
|
||||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
xhr.send(querystring.stringify(fullParams));
|
xhr.send(querystring.stringify(fullParams));
|
||||||
|
} else {
|
||||||
|
reject(new Error("Invalid method"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -103,8 +106,8 @@ lbryio.authenticate = function() {
|
||||||
if (!lbryio.enabled) {
|
if (!lbryio.enabled) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
resolve({
|
resolve({
|
||||||
ID: 1,
|
id: 1,
|
||||||
HasVerifiedEmail: true
|
has_verified_email: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -134,7 +137,7 @@ lbryio.authenticate = function() {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
app_id: installation_id,
|
app_id: installation_id,
|
||||||
}, 'post').then(function(responseData) {
|
}, 'post').then(function(responseData) {
|
||||||
if (!responseData.ID) {
|
if (!responseData.id) {
|
||||||
reject(new Error("Received invalid authentication response."));
|
reject(new Error("Received invalid authentication response."));
|
||||||
}
|
}
|
||||||
lbryio.setAccessToken(installation_id)
|
lbryio.setAccessToken(installation_id)
|
||||||
|
|
|
@ -165,5 +165,30 @@ lbryuri.normalize= function(uri) {
|
||||||
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
|
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lbryuri.isValid = function(uri) {
|
||||||
|
let parts
|
||||||
|
try {
|
||||||
|
parts = lbryuri.parse(lbryuri.normalize(uri))
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return parts && parts.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
lbryuri.isValidName = function(name, checkCase=true) {
|
||||||
|
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
|
||||||
|
return regexp.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
lbryuri.isClaimable = function(uri) {
|
||||||
|
let parts
|
||||||
|
try {
|
||||||
|
parts = lbryuri.parse(lbryuri.normalize(uri))
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return parts && parts.name && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path;
|
||||||
|
}
|
||||||
|
|
||||||
window.lbryuri = lbryuri;
|
window.lbryuri = lbryuri;
|
||||||
export default lbryuri;
|
export default lbryuri;
|
||||||
|
|
|
@ -3,13 +3,31 @@ 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/snackBar';
|
||||||
import {AuthOverlay} from './component/auth.js';
|
import {AuthOverlay} from 'component/auth.js';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import batchActions from 'util/batchActions'
|
||||||
|
import store from 'store.js';
|
||||||
|
import {
|
||||||
|
doChangePath,
|
||||||
|
doNavigate,
|
||||||
|
doDaemonReady
|
||||||
|
} from 'actions/app'
|
||||||
|
import {
|
||||||
|
doFetchDaemonSettings
|
||||||
|
} from 'actions/settings'
|
||||||
|
import {
|
||||||
|
doFileList
|
||||||
|
} from 'actions/file_info'
|
||||||
|
import {
|
||||||
|
toQueryString,
|
||||||
|
} from 'util/query_params'
|
||||||
|
|
||||||
const {remote, ipcRenderer} = require('electron');
|
const {remote, ipcRenderer, shell} = 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,31 +37,56 @@ window.addEventListener('contextmenu', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
let openUri = null;
|
window.addEventListener('popstate', (event, param) => {
|
||||||
|
const params = event.state
|
||||||
|
const pathParts = document.location.pathname.split('/')
|
||||||
|
const route = '/' + pathParts[pathParts.length - 1]
|
||||||
|
const queryString = toQueryString(params)
|
||||||
|
|
||||||
function onOpenUriRequested(event, uri) {
|
let action
|
||||||
/**
|
if (route.match(/html$/)) {
|
||||||
* If an external app requests a URI while we're still on the splash screen, we store it to
|
action = doChangePath('/discover')
|
||||||
* later pass into the App component.
|
} else {
|
||||||
*/
|
action = doChangePath(`${route}?${queryString}`)
|
||||||
openUri = uri;
|
}
|
||||||
};
|
|
||||||
ipcRenderer.on('open-uri-requested', onOpenUriRequested);
|
|
||||||
|
|
||||||
|
app.store.dispatch(action)
|
||||||
|
})
|
||||||
|
|
||||||
let init = function() {
|
ipcRenderer.on('open-uri-requested', (event, uri) => {
|
||||||
window.lbry = lbry;
|
if (uri && uri.startsWith('lbry://')) {
|
||||||
window.lighthouse = lighthouse;
|
app.store.dispatch(doNavigate('/show', { uri }))
|
||||||
let canvas = document.getElementById('canvas');
|
}
|
||||||
|
});
|
||||||
|
|
||||||
lbry.connect().then(function(isConnected) {
|
document.addEventListener('click', (event) => {
|
||||||
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
|
var target = event.target;
|
||||||
})
|
while (target && target !== document) {
|
||||||
|
if (target.matches('a[href^="http"]')) {
|
||||||
|
event.preventDefault();
|
||||||
|
shell.openExternal(target.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target = target.parentNode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialState = app.store.getState();
|
||||||
|
|
||||||
|
var init = function() {
|
||||||
|
|
||||||
function onDaemonReady() {
|
function onDaemonReady() {
|
||||||
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
|
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
|
||||||
ipcRenderer.removeListener('open-uri-requested', onOpenUriRequested); // <App /> will handle listening for URI requests once it's loaded
|
const actions = []
|
||||||
ReactDOM.render(<div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App {... openUri ? {openUri: openUri} : {}} /><SnackBar /></div>, canvas)
|
|
||||||
|
actions.push(doDaemonReady())
|
||||||
|
actions.push(doChangePath('/discover'))
|
||||||
|
actions.push(doFetchDaemonSettings())
|
||||||
|
actions.push(doFileList())
|
||||||
|
|
||||||
|
app.store.dispatch(batchActions(actions))
|
||||||
|
|
||||||
|
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') {
|
||||||
|
|
30
ui/js/page/channel/index.js
Normal file
30
ui/js/page/channel/index.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doFetchClaimsByChannel
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectClaimsInChannelForUri
|
||||||
|
} from 'selectors/claims'
|
||||||
|
import ChannelPage from './view'
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectClaim = makeSelectClaimForUri(),
|
||||||
|
selectClaimsInChannel = makeSelectClaimsInChannelForUri()
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: selectClaim(state, props),
|
||||||
|
claimsInChannel: selectClaimsInChannel(state, props)
|
||||||
|
})
|
||||||
|
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
fetchClaims: (uri) => dispatch(doFetchClaimsByChannel(uri))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(ChannelPage)
|
54
ui/js/page/channel/view.jsx
Normal file
54
ui/js/page/channel/view.jsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react';
|
||||||
|
import lbryuri from 'lbryuri'
|
||||||
|
import {BusyMessage} from 'component/common'
|
||||||
|
import FileTile from 'component/fileTile'
|
||||||
|
|
||||||
|
class ChannelPage extends React.Component{
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchClaims(this.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.fetchClaims(nextProps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchClaims(props) {
|
||||||
|
if (props.claimsInChannel === undefined) {
|
||||||
|
props.fetchClaims(props.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
claimsInChannel,
|
||||||
|
claim,
|
||||||
|
uri
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
let contentList
|
||||||
|
if (claimsInChannel === undefined) {
|
||||||
|
contentList = <BusyMessage message="Fetching content" />
|
||||||
|
} else if (claimsInChannel) {
|
||||||
|
contentList = claimsInChannel.length ?
|
||||||
|
claimsInChannel.map((claim) => <FileTile key={claim.claim_id} uri={lbryuri.build({name: claim.name, claimId: claim.claim_id})} />) :
|
||||||
|
<span className="empty">No content found.</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <main className="main--single-column">
|
||||||
|
<section className="card">
|
||||||
|
<div className="card__inner">
|
||||||
|
<div className="card__title-identity"><h1>{uri}</h1></div>
|
||||||
|
</div>
|
||||||
|
<div className="card__content">
|
||||||
|
<p>
|
||||||
|
This channel page is a stub.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<h3 className="card-row__header">Published Content</h3>
|
||||||
|
{contentList}
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelPage;
|
|
@ -1,39 +1,45 @@
|
||||||
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');
|
||||||
|
|
||||||
const DeveloperPage = React.createClass({
|
class DeveloperPage extends React.Component {
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'),
|
showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'),
|
||||||
useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'),
|
useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'),
|
||||||
customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'),
|
customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'),
|
||||||
upgradePath: '',
|
upgradePath: '',
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
handleShowDeveloperMenuChange: function(event) {
|
|
||||||
|
handleShowDeveloperMenuChange(event) {
|
||||||
lbry.setClientSetting('showDeveloperMenu', event.target.checked);
|
lbry.setClientSetting('showDeveloperMenu', event.target.checked);
|
||||||
lbry.showMenuIfNeeded();
|
lbry.showMenuIfNeeded();
|
||||||
this.setState({
|
this.setState({
|
||||||
showDeveloperMenu: event.target.checked,
|
showDeveloperMenu: event.target.checked,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleUseCustomLighthouseServersChange: function(event) {
|
|
||||||
|
handleUseCustomLighthouseServersChange(event) {
|
||||||
lbry.setClientSetting('useCustomLighthouseServers', event.target.checked);
|
lbry.setClientSetting('useCustomLighthouseServers', event.target.checked);
|
||||||
this.setState({
|
this.setState({
|
||||||
useCustomLighthouseServers: event.target.checked,
|
useCustomLighthouseServers: event.target.checked,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleUpgradeFileChange: function(event) {
|
|
||||||
|
handleUpgradeFileChange(event) {
|
||||||
this.setState({
|
this.setState({
|
||||||
upgradePath: event.target.files[0].path,
|
upgradePath: event.target.value,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
handleForceUpgradeClick: function() {
|
|
||||||
|
handleForceUpgradeClick() {
|
||||||
let upgradeSent = false;
|
let upgradeSent = false;
|
||||||
if (!this.state.upgradePath) {
|
if (!this.state.upgradePath) {
|
||||||
alert('Please select a file to upgrade from');
|
alert('Please select a file to upgrade from');
|
||||||
|
@ -51,37 +57,38 @@ const DeveloperPage = React.createClass({
|
||||||
alert('Failed to start upgrade. Is "' + this.state.upgradePath + '" a valid path to the upgrade?');
|
alert('Failed to start upgrade. Is "' + this.state.upgradePath + '" a valid path to the upgrade?');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
render: function() {
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h3>Developer Settings</h3>
|
<h3>Developer Settings</h3>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label><FormField type="checkbox" onChange={this.handleShowDeveloperMenuChange} checked={this.state.showDeveloperMenu} /> Show developer menu</label>
|
<label><FormField type="checkbox" onChange={(event) => { this.handleShowDeveloperMenuChange() }} checked={this.state.showDeveloperMenu} /> Show developer menu</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label><FormField type="checkbox" onChange={this.handleUseCustomLighthouseServersChange} checked={this.state.useCustomLighthouseServers} /> Use custom search servers</label>
|
<label><FormField type="checkbox" onChange={(event) => { this.handleUseCustomLighthouseServersChange() }} checked={this.state.useCustomLighthouseServers} /> Use custom search servers</label>
|
||||||
</div>
|
</div>
|
||||||
{this.state.useCustomLighthouseServers
|
{this.state.useCustomLighthouseServers
|
||||||
? <div className="form-row">
|
? <div className="form-row">
|
||||||
<label>
|
<label>
|
||||||
Custom search servers (one per line)
|
Custom search servers (one per line)
|
||||||
<div><FormField type="textarea" className="developer-page__custom-lighthouse-servers" value={this.state.customLighthouseServers} onChange={this.handleCustomLighthouseServersChange} checked={this.state.debugMode} /></div>
|
<div><FormField type="textarea" className="developer-page__custom-lighthouse-servers" value={this.state.customLighthouseServers} onChange={(event) => { this.handleCustomLighthouseServersChange() }} checked={this.state.debugMode} /></div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
</section>
|
</section>
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<FormField name="file" ref="file" type="file" onChange={this.handleUpgradeFileChange}/>
|
<FormField name="file" ref="file" type="file" onChange={(event) => { this.handleUpgradeFileChange() }}/>
|
||||||
|
|
||||||
<Link label="Force Upgrade" button="alt" onClick={this.handleForceUpgradeClick} />
|
<Link label="Force Upgrade" button="alt" onClick={(event) => { this.handleForceUpgradeClick() }} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default DeveloperPage;
|
export default DeveloperPage;
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import lbryio from '../lbryio.js';
|
|
||||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
|
||||||
import {ToolTip} from '../component/tooltip.js';
|
|
||||||
|
|
||||||
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!');
|
|
||||||
|
|
||||||
let 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.match(/^community/i) ?
|
|
||||||
<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>)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let DiscoverPage = 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 <main>{
|
|
||||||
this.state.failed ?
|
|
||||||
<div className="empty">Failed to load landing content.</div> :
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
Object.keys(this.state.featuredUris).map((category) => {
|
|
||||||
return this.state.featuredUris[category].length ?
|
|
||||||
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
|
|
||||||
'';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}</main>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DiscoverPage;
|
|
23
ui/js/page/discover/index.js
Normal file
23
ui/js/page/discover/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
connect
|
||||||
|
} from 'react-redux'
|
||||||
|
import {
|
||||||
|
doFetchFeaturedUris,
|
||||||
|
} from 'actions/content'
|
||||||
|
import {
|
||||||
|
selectFeaturedUris,
|
||||||
|
selectFetchingFeaturedUris,
|
||||||
|
} from 'selectors/content'
|
||||||
|
import DiscoverPage from './view'
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
featuredUris: selectFeaturedUris(state),
|
||||||
|
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
|
||||||
|
})
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris())
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(select, perform)(DiscoverPage)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue