Seed Support #56
100 changed files with 4694 additions and 3423 deletions
24
.appveyor.yml
Normal file
24
.appveyor.yml
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Test against the latest version of this Node.js version
|
||||
environment:
|
||||
nodejs_version: 7
|
||||
GH_TOKEN:
|
||||
secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN
|
||||
key_pass:
|
||||
secure: u6DydPcdrUJlxGL9uc7yQRYG8+5rY6aAEE9nfCSzFyNzZlX9NniOp8Uh5ZKQqX7bGEngLI6ipbLfiJvn0XFnhbn2iTkOuMqOXVJVOehvwlQ=
|
||||
pfx_key:
|
||||
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
|
||||
SIGNTOOL_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe
|
||||
|
||||
skip_branch_with_pr: true
|
||||
|
||||
clone_folder: C:\projects\lbry-app
|
||||
|
||||
build_script:
|
||||
- ps: build\build.ps1
|
||||
|
||||
test: off
|
||||
|
||||
artifacts:
|
||||
- path: dist\*.exe
|
||||
name: LBRY
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.9.2rc15
|
||||
current_version = 0.10.0
|
||||
commit = True
|
||||
tag = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,6 +6,7 @@ dist
|
|||
/app/node_modules
|
||||
/build/venv
|
||||
/lbry-app-venv
|
||||
/lbry-venv
|
||||
/daemon/build
|
||||
/daemon/venv
|
||||
/daemon/requirements.txt
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,6 +0,0 @@
|
|||
[submodule "lbry"]
|
||||
path = lbry
|
||||
url = https://github.com/lbryio/lbry.git
|
||||
[submodule "lbryum"]
|
||||
path = lbryum
|
||||
url = https://github.com/lbryio/lbryum.git
|
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -8,25 +8,56 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
|
||||
* lbry.js now offers a subscription model for wallet balance similar to file info.
|
||||
* Fixed file info subscribes not being unsubscribed in unmount.
|
||||
* Fixed drawer not highlighting selected page.
|
||||
* You can now make API calls directly on the lbry module, e.g. lbry.peer_list()
|
||||
* New-style API calls return promises instead of using callbacks
|
||||
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes
|
||||
* New publishes now display immediately in My Files, even before they hit the lbrynet file manager.
|
||||
*
|
||||
*
|
||||
|
||||
### Changed
|
||||
* Update process now easier and more reliable
|
||||
* Cleaned up shutdown logic
|
||||
*
|
||||
*
|
||||
|
||||
### Fixed
|
||||
* Fix Watch page and progress bars for new API changes
|
||||
* Error modals now display full screen properly
|
||||
*
|
||||
|
||||
### Deprecated
|
||||
*
|
||||
*
|
||||
|
||||
### Removed
|
||||
*
|
||||
*
|
||||
|
||||
## [0.10.0] - 2017-05-04
|
||||
|
||||
### Added
|
||||
* The UI has been overhauled to use an omnibar and drop the sidebar.
|
||||
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
|
||||
* lbry.js now offers a subscription model for wallet balance similar to file info.
|
||||
* Fixed file info subscribes not being unsubscribed in unmount.
|
||||
* Fixed drawer not highlighting selected page.
|
||||
* You can now make API calls directly on the lbry module, e.g. lbry.peer_list()
|
||||
* New-style API calls return promises instead of using callbacks
|
||||
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes
|
||||
* New publishes now display immediately in My Files, even before they hit the lbrynet file manager.
|
||||
* New welcome flow for new users
|
||||
* Redesigned UI for Discover
|
||||
* Handle more of price calculations at the daemon layer to improve page load time
|
||||
* Add special support for building channel claims in lbryuri module
|
||||
* Enable windows code signing of binary
|
||||
|
||||
|
||||
### Changed
|
||||
* Update process now easier and more reliable
|
||||
* Updated search to be compatible with new Lighthouse servers
|
||||
* Cleaned up shutdown logic
|
||||
* Support lbry v0.10 API signatures
|
||||
|
||||
|
||||
### Fixed
|
||||
* Fix Watch page and progress bars for new API changes
|
||||
|
||||
|
||||
|
||||
## [0.9.0rc15] - 2017-03-09
|
||||
### Added
|
||||
* A way to access the Developer Settings panel in Electron (Ctrl-Shift and click logo)
|
||||
|
|
|
@ -45,4 +45,4 @@ to create distributable packages, which is run by calling:
|
|||
### Development on Windows
|
||||
|
||||
This project has currently only been worked on in Linux and macOS. If you are on Windows, you can
|
||||
checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/appveyor.yml) and probably figure out something from there.
|
||||
checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there.
|
||||
|
|
92
app/main.js
92
app/main.js
|
@ -1,11 +1,18 @@
|
|||
const {app, BrowserWindow, ipcMain} = require('electron');
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
const jayson = require('jayson');
|
||||
const semver = require('semver');
|
||||
const https = require('https');
|
||||
// tree-kill has better cross-platform handling of
|
||||
// killing a process. child-process.kill was unreliable
|
||||
const kill = require('tree-kill');
|
||||
const child_process = require('child_process');
|
||||
const assert = require('assert');
|
||||
const {version: localVersion} = require(app.getAppPath() + '/package.json');
|
||||
|
||||
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
|
||||
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
|
||||
|
||||
|
||||
let client = jayson.client.http('http://localhost:5279/lbryapi');
|
||||
|
@ -17,12 +24,58 @@ let daemonSubprocess;
|
|||
|
||||
// This is set to true right before we try to shut the daemon subprocess --
|
||||
// if it dies when we didn't ask it to shut down, we want to alert the user.
|
||||
let daemonSubprocessKillRequested = false;
|
||||
let daemonStopRequested = false;
|
||||
|
||||
// When a quit is attempted, we cancel the quit, do some preparations, then
|
||||
// this is set to true and app.quit() is called again to quit for real.
|
||||
let readyToQuit = false;
|
||||
|
||||
function checkForNewVersion(callback) {
|
||||
function formatRc(ver) {
|
||||
// Adds dash if needed to make RC suffix semver friendly
|
||||
return ver.replace(/([^-])rc/, '$1-rc');
|
||||
}
|
||||
|
||||
let result = '';
|
||||
const opts = {
|
||||
headers: {
|
||||
'User-Agent': `LBRY/${localVersion}`,
|
||||
}
|
||||
};
|
||||
const req = https.get(Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), (res) => {
|
||||
res.on('data', (data) => {
|
||||
result += data;
|
||||
});
|
||||
res.on('end', () => {
|
||||
console.log('Local version:', localVersion);
|
||||
const tagName = JSON.parse(result).tag_name;
|
||||
const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/);
|
||||
if (!remoteVersion) {
|
||||
console.log('Malformed remote version string:', tagName);
|
||||
if (win) {
|
||||
win.webContents.send('version-info-received', null);
|
||||
}
|
||||
} else {
|
||||
console.log('Remote version:', remoteVersion);
|
||||
const upgradeAvailable = semver.gt(formatRc(remoteVersion), formatRc(localVersion));
|
||||
console.log(upgradeAvailable ? 'Upgrade available' : 'No upgrade available');
|
||||
if (win) {
|
||||
win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.log('Failed to get current version from GitHub. Error:', err);
|
||||
if (win) {
|
||||
win.webContents.send('version-info-received', null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('version-info-requested', checkForNewVersion);
|
||||
|
||||
/*
|
||||
* Replacement for Electron's shell.openItem. The Electron version doesn't
|
||||
* reliably work from the main process, and we need to be able to run it
|
||||
|
@ -62,9 +115,9 @@ function getPidsForProcessName(name) {
|
|||
}
|
||||
|
||||
function createWindow () {
|
||||
win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary
|
||||
win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
|
||||
win.maximize()
|
||||
//win.webContents.openDevTools()
|
||||
// win.webContents.openDevTools();
|
||||
win.loadURL(`file://${__dirname}/dist/index.html`)
|
||||
win.on('closed', () => {
|
||||
win = null
|
||||
|
@ -74,8 +127,8 @@ function createWindow () {
|
|||
function handleDaemonSubprocessExited() {
|
||||
console.log('The daemon has exited.');
|
||||
daemonSubprocess = null;
|
||||
if (!daemonSubprocessKillRequested) {
|
||||
// We didn't stop the daemon subprocess on purpose, so display a
|
||||
if (!daemonStopRequested) {
|
||||
// We didn't request to stop the daemon, so display a
|
||||
// warning and schedule a quit.
|
||||
//
|
||||
// TODO: maybe it would be better to restart the daemon?
|
||||
|
@ -107,7 +160,6 @@ function launchDaemon() {
|
|||
daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
daemonSubprocess.on('exit', handleDaemonSubprocessExited);
|
||||
console.log('lbrynet daemon has launched')
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -209,31 +261,27 @@ app.on('activate', () => {
|
|||
// When a quit is attempted, this is called. It attempts to shutdown the daemon,
|
||||
// then calls quitNow() to quit for real.
|
||||
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
|
||||
if (daemonSubprocess) {
|
||||
console.log('Killing lbrynet-daemon process');
|
||||
daemonSubprocessKillRequested = true;
|
||||
kill(daemonSubprocess.pid, undefined, (err) => {
|
||||
console.log('Killed lbrynet-daemon process');
|
||||
quitNow();
|
||||
});
|
||||
} else if (evenIfNotStartedByApp) {
|
||||
console.log('Stopping lbrynet-daemon, even though app did not start it');
|
||||
function doShutdown() {
|
||||
console.log('Asking daemon to shut down down');
|
||||
daemonStopRequested = true;
|
||||
client.request('daemon_stop', [], (err, res) => {
|
||||
if (err) {
|
||||
// We could get an error because the daemon is already stopped (good)
|
||||
// or because it's running but not responding properly (bad).
|
||||
// So try to force kill any daemons that are still running.
|
||||
|
||||
console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}`);
|
||||
forceKillAllDaemonsAndQuit();
|
||||
console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}\n`);
|
||||
console.log('You will need to manually kill the daemon.');
|
||||
} else {
|
||||
console.log('Successfully stopped daemon via RPC call.')
|
||||
quitNow();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (daemonSubprocess) {
|
||||
doShutdown();
|
||||
} else if (!evenIfNotStartedByApp) {
|
||||
console.log('Not killing lbrynet-daemon because app did not start it');
|
||||
quitNow();
|
||||
} else {
|
||||
doShutdown();
|
||||
}
|
||||
|
||||
// Is it safe to start the installer before the daemon finishes running?
|
||||
|
|
|
@ -24,6 +24,9 @@ const baseTemplate = [
|
|||
{
|
||||
role: 'paste',
|
||||
},
|
||||
{
|
||||
role: 'selectall',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"version": "0.9.2rc15",
|
||||
"version": "0.10.0",
|
||||
"main": "main.js",
|
||||
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
|
||||
"author": {
|
||||
|
@ -12,6 +12,7 @@
|
|||
"install": "^0.8.7",
|
||||
"jayson": "^2.0.2",
|
||||
"npm": "^4.2.0",
|
||||
"semver": "^5.3.0",
|
||||
"tree-kill": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
55
appveyor.yml
55
appveyor.yml
|
@ -1,55 +0,0 @@
|
|||
# Test against the latest version of this Node.js version
|
||||
environment:
|
||||
nodejs_version: "6"
|
||||
GH_TOKEN:
|
||||
secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN
|
||||
|
||||
skip_branch_with_pr: true
|
||||
|
||||
clone_folder: C:\projects\lbry-electron
|
||||
|
||||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
# needed to deal with submodules
|
||||
- git submodule update --init --recursive
|
||||
- python build\set_version.py
|
||||
- python build\set_build.py
|
||||
# Get the latest stable version of Node.js or io.js
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
# install modules
|
||||
- npm install
|
||||
- cd app
|
||||
- npm install
|
||||
- cd ..
|
||||
# create daemon and cli executable
|
||||
- cd daemon
|
||||
- ps: .\build.ps1
|
||||
- cd ..
|
||||
# build ui
|
||||
- cd ui
|
||||
- npm install
|
||||
- node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\
|
||||
- node_modules\.bin\webpack
|
||||
- ps: Copy-Item dist ..\app\ -recurse
|
||||
- cd ..
|
||||
# copy executables into ui
|
||||
- ps: Copy-Item daemon\dist\lbrynet-daemon.exe app\dist
|
||||
- ps: Copy-Item daemon\dist\lbrynet-cli.exe app\dist
|
||||
|
||||
build_script:
|
||||
# build electron app
|
||||
- node_modules\.bin\build -p never
|
||||
# for debugging, see what was built
|
||||
- python build\zip_daemon.py
|
||||
- dir dist
|
||||
- pip install -r build\requirements.txt
|
||||
- python build\release_on_tag.py
|
||||
|
||||
test: off
|
||||
|
||||
artifacts:
|
||||
- path: dist\*.exe
|
||||
name: LBRY
|
||||
|
||||
- path: dist\*.zip
|
||||
name: lbrynet-daemon
|
1
build/DAEMON_URL
Normal file
1
build/DAEMON_URL
Normal file
|
@ -0,0 +1 @@
|
|||
https://github.com/lbryio/lbry/releases/download/v0.10.3rc1/lbrynet-daemon-v0.10.3rc1-OSNAME.zip
|
40
build/build.ps1
Normal file
40
build/build.ps1
Normal file
|
@ -0,0 +1,40 @@
|
|||
pip install -r build\requirements.txt
|
||||
python build\set_version.py
|
||||
|
||||
# Get the latest stable version of Node.js or io.js
|
||||
Install-Product node $env:nodejs_version
|
||||
|
||||
# install node modules
|
||||
npm install
|
||||
cd app
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
# build ui
|
||||
cd ui
|
||||
npm install
|
||||
node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\
|
||||
node_modules\.bin\webpack
|
||||
Copy-Item dist ..\app\ -recurse
|
||||
cd ..
|
||||
|
||||
# get daemon and cli executable
|
||||
$daemon_url = (Get-Content build\DAEMON_URL -Raw).replace("OSNAME", "windows")
|
||||
Invoke-WebRequest -Uri $daemon_url -OutFile daemon.zip
|
||||
Expand-Archive daemon.zip -DestinationPath app\dist\
|
||||
dir app\dist\ # verify that daemon binary is there
|
||||
rm daemon.zip
|
||||
|
||||
# build electron app
|
||||
node_modules\.bin\build -p never
|
||||
$binary_name = Get-ChildItem -Path dist -Filter '*.exe' -Name
|
||||
$new_name = $binary_name -replace '^LBRY Setup (.*)\.exe$', 'LBRY_$1.exe'
|
||||
Rename-Item -Path "dist\$binary_name" -NewName $new_name
|
||||
dir dist # verify that binary was built/named correctly
|
||||
|
||||
# sign binary
|
||||
nuget install secure-file -ExcludeVersion
|
||||
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
|
||||
|
||||
python build\release_on_tag.py
|
|
@ -7,7 +7,18 @@ ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
|
|||
cd "$ROOT"
|
||||
BUILD_DIR="$ROOT/build"
|
||||
|
||||
LINUX=false
|
||||
OSX=false
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
OSX=true
|
||||
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
||||
LINUX=true
|
||||
else
|
||||
echo "Platform detection failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $OSX; then
|
||||
ICON="$BUILD_DIR/icon.icns"
|
||||
else
|
||||
ICON="$BUILD_DIR/icons/lbry48.png"
|
||||
|
@ -32,7 +43,6 @@ if [ "$FULL_BUILD" == "true" ]; then
|
|||
set -u
|
||||
pip install -r "$BUILD_DIR/requirements.txt"
|
||||
python "$BUILD_DIR/set_version.py"
|
||||
python "$BUILD_DIR/set_build.py"
|
||||
fi
|
||||
|
||||
[ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist"
|
||||
|
@ -62,24 +72,15 @@ npm install
|
|||
# daemon and cli #
|
||||
####################
|
||||
|
||||
(
|
||||
cd "$ROOT/daemon"
|
||||
|
||||
# copy requirements from lbry, but remove lbryum (we'll add it back in below)
|
||||
grep -v lbryum "$ROOT/lbry/requirements.txt" > requirements.txt
|
||||
# for electron, we install lbryum and lbry using submodules
|
||||
echo "../lbryum" >> requirements.txt
|
||||
echo "../lbry" >> requirements.txt
|
||||
# also add pyinstaller
|
||||
echo "PyInstaller==3.2.1" >> requirements.txt
|
||||
|
||||
pip install -r requirements.txt
|
||||
pyinstaller -y daemon.onefile.spec
|
||||
pyinstaller -y cli.onefile.spec
|
||||
mv dist/lbrynet-daemon dist/lbrynet-cli "$ROOT/app/dist/"
|
||||
)
|
||||
python "$BUILD_DIR/zip_daemon.py"
|
||||
|
||||
if $OSX; then
|
||||
OSNAME="macos"
|
||||
else
|
||||
OSNAME="linux"
|
||||
fi
|
||||
DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")"
|
||||
wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip"
|
||||
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
|
||||
rm "$BUILD_DIR/daemon.zip"
|
||||
|
||||
###################
|
||||
# Build the app #
|
||||
|
@ -91,12 +92,18 @@ python "$BUILD_DIR/zip_daemon.py"
|
|||
)
|
||||
|
||||
if [ "$FULL_BUILD" == "true" ]; then
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
if $OSX; then
|
||||
security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain
|
||||
fi
|
||||
|
||||
node_modules/.bin/build -p never
|
||||
|
||||
if $OSX; then
|
||||
binary_name=$(find "$ROOT/dist" -iname "*dmg")
|
||||
new_name=$(basename "$binary_name" | sed 's/-/_/')
|
||||
mv "$binary_name" "$(dirname "$binary_name")/$new_name"
|
||||
fi
|
||||
|
||||
# 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
|
||||
# this, but it seemed better to write my own.
|
||||
|
@ -107,4 +114,4 @@ if [ "$FULL_BUILD" == "true" ]; then
|
|||
echo 'Build and packaging complete.'
|
||||
else
|
||||
echo 'Build complete. Run `./node_modules/.bin/electron app` to launch the app'
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import argparse
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]')
|
||||
CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}')
|
||||
|
@ -14,118 +11,119 @@ EMPTY_RE = re.compile(r'^\w*\*\w*$')
|
|||
ENTRY_RE = re.compile(r'\* (.*)')
|
||||
VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
|
||||
|
||||
|
||||
# allocate some entries to cut-down on merge conflicts
|
||||
TEMPLATE = """### Added
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
### Changed
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
### Fixed
|
||||
*
|
||||
*
|
||||
|
||||
### Deprecated
|
||||
*
|
||||
*
|
||||
|
||||
### Removed
|
||||
*
|
||||
*
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
print "i am broken"
|
||||
return 1
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('changelog')
|
||||
parser.add_argument('version')
|
||||
args = parser.parse_args()
|
||||
bump(changelog, version)
|
||||
class Changelog(object):
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.start = []
|
||||
self.unreleased = []
|
||||
self.rest = []
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
with open(self.path) as fp:
|
||||
lines = fp.readlines()
|
||||
|
||||
def bump(changelog, version):
|
||||
with open(changelog) as fp:
|
||||
lines = fp.readlines()
|
||||
unreleased_start_found = False
|
||||
unreleased_end_found = False
|
||||
|
||||
start = []
|
||||
unreleased = []
|
||||
rest = []
|
||||
unreleased_start_found = False
|
||||
unreleased_end_found = False
|
||||
for line in lines:
|
||||
if not unreleased_start_found:
|
||||
start.append(line)
|
||||
if CHANGELOG_START_RE.search(line):
|
||||
unreleased_start_found = True
|
||||
continue
|
||||
if unreleased_end_found:
|
||||
rest.append(line)
|
||||
continue
|
||||
if CHANGELOG_END_RE.search(line):
|
||||
rest.append(line)
|
||||
unreleased_end_found = True
|
||||
continue
|
||||
if CHANGELOG_ERROR_RE.search(line):
|
||||
raise Exception(
|
||||
'Failed to parse {}: {}'.format(changelog, 'unexpected section header found'))
|
||||
unreleased.append(line)
|
||||
for line in lines:
|
||||
if not unreleased_start_found:
|
||||
self.start.append(line)
|
||||
if CHANGELOG_START_RE.search(line):
|
||||
unreleased_start_found = True
|
||||
continue
|
||||
if unreleased_end_found:
|
||||
self.rest.append(line)
|
||||
continue
|
||||
if CHANGELOG_END_RE.search(line):
|
||||
self.rest.append(line)
|
||||
unreleased_end_found = True
|
||||
continue
|
||||
if CHANGELOG_ERROR_RE.search(line):
|
||||
raise Exception(
|
||||
'Failed to parse {}: {}'.format(self.path, 'unexpected section header found'))
|
||||
self.unreleased.append(line)
|
||||
|
||||
today = datetime.datetime.today()
|
||||
header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d'))
|
||||
released = normalize(unreleased)
|
||||
if not released:
|
||||
# If we don't have anything in the Unreleased section, then leave the
|
||||
# changelog as it is and return None
|
||||
return
|
||||
self.unreleased = self._normalize_section(self.unreleased)
|
||||
|
||||
changelog_data = (
|
||||
''.join(start) +
|
||||
TEMPLATE +
|
||||
header +
|
||||
'\n'.join(released) + '\n\n'
|
||||
+ ''.join(rest)
|
||||
)
|
||||
with open(changelog, 'w') as fp:
|
||||
fp.write(changelog_data)
|
||||
return '\n'.join(released) + '\n\n'
|
||||
@staticmethod
|
||||
def _normalize_section(lines):
|
||||
"""Parse a changelog entry and output a normalized form"""
|
||||
sections = {}
|
||||
current_section_name = None
|
||||
current_section_contents = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or EMPTY_RE.match(line):
|
||||
continue
|
||||
match = SECTION_RE.match(line)
|
||||
if match:
|
||||
if current_section_contents:
|
||||
sections[current_section_name] = current_section_contents
|
||||
current_section_contents = []
|
||||
current_section_name = match.group(1)
|
||||
if current_section_name not in VALID_SECTIONS:
|
||||
raise ValueError("Section '{}' is not valid".format(current_section_name))
|
||||
continue
|
||||
match = ENTRY_RE.match(line)
|
||||
if match:
|
||||
current_section_contents.append(match.group(1))
|
||||
continue
|
||||
raise Exception('Something is wrong with line: {}'.format(line))
|
||||
if current_section_contents:
|
||||
sections[current_section_name] = current_section_contents
|
||||
|
||||
output = []
|
||||
for section in VALID_SECTIONS:
|
||||
if section not in sections:
|
||||
continue
|
||||
output.append('### {}'.format(section))
|
||||
for entry in sections[section]:
|
||||
output.append(' * {}'.format(entry))
|
||||
output.append("\n")
|
||||
return output
|
||||
|
||||
def normalize(lines):
|
||||
"""Parse a changelog entry and output a normalized form"""
|
||||
sections = {}
|
||||
current_section_name = None
|
||||
current_section_contents = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or EMPTY_RE.match(line):
|
||||
continue
|
||||
match = SECTION_RE.match(line)
|
||||
if match:
|
||||
if current_section_contents:
|
||||
sections[current_section_name] = current_section_contents
|
||||
current_section_contents = []
|
||||
current_section_name = match.group(1)
|
||||
if current_section_name not in VALID_SECTIONS:
|
||||
raise ValueError("Section '{}' is not valid".format(current_section_name))
|
||||
continue
|
||||
match = ENTRY_RE.match(line)
|
||||
if match:
|
||||
current_section_contents.append(match.group(1))
|
||||
continue
|
||||
raise Exception('Something is wrong with line: {}'.format(line))
|
||||
if current_section_contents:
|
||||
sections[current_section_name] = current_section_contents
|
||||
def get_unreleased(self):
|
||||
return '\n'.join(self.unreleased) if self.unreleased else None
|
||||
|
||||
output = []
|
||||
for section in VALID_SECTIONS:
|
||||
if section not in sections:
|
||||
continue
|
||||
output.append('### {}'.format(section))
|
||||
for entry in sections[section]:
|
||||
output.append(' * {}'.format(entry))
|
||||
return output
|
||||
def bump(self, version):
|
||||
if not self.unreleased:
|
||||
return
|
||||
|
||||
today = datetime.datetime.today()
|
||||
header = "## [{}] - {}\n\n".format(version, today.strftime('%Y-%m-%d'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
changelog_data = (
|
||||
''.join(self.start) +
|
||||
TEMPLATE +
|
||||
header +
|
||||
'\n'.join(self.unreleased) + '\n\n'
|
||||
+ ''.join(self.rest)
|
||||
)
|
||||
|
||||
with open(self.path, 'w') as fp:
|
||||
fp.write(changelog_data)
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# https://github.com/lbryio/lbry-app/commit/4386102ba3bf8c731a075797756111d73c31a47a
|
||||
# https://github.com/lbryio/lbry-app/commit/a3a376922298b94615f7514ca59988b73a522f7f
|
||||
|
||||
# Appveyor and Teamcity struggle with SSH urls in submodules, so we use HTTPS
|
||||
# But locally, SSH urls are way better since they dont require a password
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "DIR"
|
||||
|
||||
git config submodule.lbry.url git@github.com:lbryio/lbry.git
|
||||
git config submodule.lbryum.url git@github.com:lbryio/lbryum.git
|
BIN
build/lbry2.pfx.enc
Normal file
BIN
build/lbry2.pfx.enc
Normal file
Binary file not shown.
|
@ -72,7 +72,6 @@ if ! cmd_exists pip; then
|
|||
fi
|
||||
|
||||
if $LINUX && [ "$(pip list --format=columns | grep setuptools | wc -l)" -ge 1 ]; then
|
||||
#$INSTALL python-setuptools
|
||||
$SUDO pip install setuptools
|
||||
fi
|
||||
|
||||
|
@ -88,3 +87,14 @@ if ! cmd_exists node; then
|
|||
brew install node
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! cmd_exists unzip; then
|
||||
if $LINUX; then
|
||||
$INSTALL unzip
|
||||
elif $OSX; then
|
||||
echo "unzip required"
|
||||
exit 1
|
||||
# not sure this works, but OSX should come with unzip
|
||||
# brew install unzip
|
||||
fi
|
||||
fi
|
||||
|
|
391
build/release.py
391
build/release.py
|
@ -1,13 +1,12 @@
|
|||
"""Trigger a release.
|
||||
"""Bump version and create Github release
|
||||
|
||||
This script is to be run locally (not on a build server).
|
||||
This script should be run locally, not on a build server.
|
||||
"""
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
@ -16,122 +15,137 @@ import github
|
|||
|
||||
import changelog
|
||||
|
||||
# TODO: ask bumpversion for these
|
||||
LBRY_PARTS = ('major', 'minor', 'patch', 'release', 'candidate')
|
||||
LBRYUM_PARTS = ('major', 'minor', 'patch')
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
DAEMON_URL_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DAEMON_URL')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"lbry_part", help="part of lbry version to bump",
|
||||
choices=LBRY_PARTS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-lbryum", help="skip bumping lbryum, even if there are changes",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lbryum-part", help="part of lbryum version to bump",
|
||||
choices=LBRYUM_PARTS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--last-release",
|
||||
help=("manually set the last release version. The default is to query and parse the"
|
||||
" value from the release page.")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-sanity-checks", action="store_true")
|
||||
parser.add_argument(
|
||||
"--require-changelog", action="store_true",
|
||||
help=("Set this flag to raise an exception if a submodules has changes without a"
|
||||
" corresponding changelog entry. The default is to log a warning")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-push", action="store_true",
|
||||
help="Set to not push changes to remote repo"
|
||||
)
|
||||
bumpversion_parts = get_bumpversion_parts()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("part", choices=bumpversion_parts, help="part of version to bump")
|
||||
parser.add_argument("--skip-sanity-checks", action="store_true")
|
||||
parser.add_argument("--skip-push", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--confirm", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
base = git.Repo(os.getcwd())
|
||||
if args.dry_run:
|
||||
print "DRY RUN. Nothing will be committed/pushed."
|
||||
|
||||
repo = Repo('lbry-app', args.part, ROOT)
|
||||
branch = 'master'
|
||||
|
||||
print 'Current version: {}'.format(repo.current_version)
|
||||
print 'New version: {}'.format(repo.new_version)
|
||||
with open(DAEMON_URL_FILE, 'r') as f:
|
||||
daemon_url_template = f.read().strip()
|
||||
daemon_version = re.search('/(?P<version>v[^/]+)', daemon_url_template)
|
||||
print 'Daemon version: {} ({})'.format(
|
||||
daemon_version.group('version'), daemon_url_template)
|
||||
|
||||
if not args.confirm and not confirm():
|
||||
print "Aborting"
|
||||
return 1
|
||||
|
||||
if not args.skip_sanity_checks:
|
||||
run_sanity_checks(base, branch)
|
||||
run_sanity_checks(repo, branch)
|
||||
repo.assert_new_tag_is_absent()
|
||||
|
||||
base_repo = Repo('lbry-app', args.lbry_part, os.getcwd())
|
||||
base_repo.assert_new_tag_is_absent()
|
||||
is_rc = re.search('\drc\d+$', repo.new_version) is not None
|
||||
# only have a release message for real releases, not for RCs
|
||||
release_msg = '' if is_rc else repo.get_unreleased_changelog()
|
||||
|
||||
last_release = args.last_release or base_repo.get_last_tag()
|
||||
logging.info('Last release: %s', last_release)
|
||||
if args.dry_run:
|
||||
print "rc: " + ("yes" if is_rc else "no")
|
||||
print "release message: \n" + (release_msg or " NO MESSAGE FOR RCs")
|
||||
return
|
||||
|
||||
gh_token = get_gh_token()
|
||||
auth = github.Github(gh_token)
|
||||
github_repo = auth.get_repo('lbryio/lbry-app')
|
||||
|
||||
names = ['lbryum', 'lbry']
|
||||
repos = {name: Repo(name, get_part(args, name)) for name in names}
|
||||
if not is_rc:
|
||||
repo.bump_changelog()
|
||||
repo.bumpversion()
|
||||
|
||||
changelogs = {}
|
||||
new_tag = repo.get_new_tag()
|
||||
github_repo.create_git_release(new_tag, new_tag, release_msg, draft=True, prerelease=is_rc)
|
||||
|
||||
for repo in repos.values():
|
||||
logging.info('Processing repo: %s', repo.name)
|
||||
repo.checkout(branch)
|
||||
last_submodule_hash = base_repo.get_submodule_hash(last_release, repo.name)
|
||||
if repo.has_changes_from_revision(last_submodule_hash):
|
||||
if repo.name == 'lbryum':
|
||||
if args.skip_lbryum:
|
||||
continue
|
||||
if not repo.part:
|
||||
repo.part = get_lbryum_part()
|
||||
entry = repo.get_changelog_entry()
|
||||
if entry:
|
||||
changelogs[repo.name] = entry.strip()
|
||||
repo.add_changelog()
|
||||
else:
|
||||
msg = 'Changelog entry is missing for {}'.format(repo.name)
|
||||
if args.require_changelog:
|
||||
raise Exception(msg)
|
||||
else:
|
||||
logging.warning(msg)
|
||||
else:
|
||||
logging.warning('Submodule %s has no changes.', repo.name)
|
||||
if repo.name == 'lbryum':
|
||||
# The other repos have their version track each other so need to bump
|
||||
# them even if there aren't any changes, but lbryum should only be
|
||||
# bumped if it has changes
|
||||
continue
|
||||
# bumpversion will fail if there is already the tag we want in the repo
|
||||
repo.assert_new_tag_is_absent()
|
||||
repo.bumpversion()
|
||||
|
||||
release_msg = get_release_msg(changelogs, names)
|
||||
|
||||
for name in names:
|
||||
base.git.add(name)
|
||||
|
||||
base_repo.bumpversion()
|
||||
current_tag = base.git.describe()
|
||||
|
||||
is_rc = re.match('\drc\d+$', current_tag) is not None
|
||||
|
||||
github_repo.create_git_release(current_tag, current_tag, release_msg, draft=True,
|
||||
prerelease=is_rc)
|
||||
no_change_msg = ('No change since the last release. This release is simply a placeholder'
|
||||
' so that LBRY and LBRY App track the same version')
|
||||
lbrynet_daemon_release_msg = changelogs.get('lbry', no_change_msg)
|
||||
auth.get_repo('lbryio/lbry').create_git_release(
|
||||
current_tag, current_tag, lbrynet_daemon_release_msg, draft=True)
|
||||
|
||||
if not args.skip_push:
|
||||
for repo in repos.values():
|
||||
repo.git.push(follow_tags=True)
|
||||
base.git.push(follow_tags=True, recurse_submodules='check')
|
||||
if args.skip_push:
|
||||
print (
|
||||
'Skipping push; you will have to reset and delete tags if '
|
||||
'you want to run this script again.'
|
||||
)
|
||||
else:
|
||||
logging.info('Skipping push; you will have to reset and delete tags if '
|
||||
'you want to run this script again. Take a look at reset.sh; '
|
||||
'it probably does what you want.')
|
||||
repo.git_repo.git.push(follow_tags=True, recurse_submodules='check')
|
||||
|
||||
|
||||
class Repo(object):
|
||||
def __init__(self, name, part, directory):
|
||||
self.name = name
|
||||
self.part = part
|
||||
if not self.part:
|
||||
raise Exception('Part required')
|
||||
self.directory = directory
|
||||
self.git_repo = git.Repo(self.directory)
|
||||
self._bumped = False
|
||||
|
||||
self.current_version = self._get_current_version()
|
||||
self.new_version = self._get_new_version()
|
||||
self._changelog = changelog.Changelog(os.path.join(self.directory, 'CHANGELOG.md'))
|
||||
|
||||
def get_new_tag(self):
|
||||
return 'v' + self.new_version
|
||||
|
||||
def get_unreleased_changelog(self):
|
||||
return self._changelog.get_unreleased()
|
||||
|
||||
def bump_changelog(self):
|
||||
self._changelog.bump(self.new_version)
|
||||
with pushd(self.directory):
|
||||
self.git_repo.git.add(os.path.basename(self._changelog.path))
|
||||
|
||||
def _get_current_version(self):
|
||||
with pushd(self.directory):
|
||||
output = subprocess.check_output(
|
||||
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
|
||||
return re.search('^current_version=(.*)$', output, re.M).group(1)
|
||||
|
||||
def _get_new_version(self):
|
||||
with pushd(self.directory):
|
||||
output = subprocess.check_output(
|
||||
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
|
||||
return re.search('^new_version=(.*)$', output, re.M).group(1)
|
||||
|
||||
def bumpversion(self):
|
||||
if self._bumped:
|
||||
raise Exception('Cowardly refusing to bump a repo twice')
|
||||
with pushd(self.directory):
|
||||
subprocess.check_call(['bumpversion', '--allow-dirty', self.part])
|
||||
self._bumped = True
|
||||
|
||||
def assert_new_tag_is_absent(self):
|
||||
new_tag = self.get_new_tag()
|
||||
tags = self.git_repo.git.tag()
|
||||
if new_tag in tags.split('\n'):
|
||||
raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name))
|
||||
|
||||
def is_behind(self, branch):
|
||||
self.git_repo.remotes.origin.fetch()
|
||||
rev_list = '{branch}...origin/{branch}'.format(branch=branch)
|
||||
commits_behind = self.git_repo.git.rev_list(rev_list, right_only=True, count=True)
|
||||
commits_behind = int(commits_behind)
|
||||
return commits_behind > 0
|
||||
|
||||
|
||||
def get_bumpversion_parts():
|
||||
with pushd(ROOT):
|
||||
output = subprocess.check_output([
|
||||
'bumpversion', '--dry-run', '--list', '--allow-dirty', 'fake-part',
|
||||
])
|
||||
parse_line = re.search('^parse=(.*)$', output, re.M).group(1)
|
||||
return tuple(re.findall('<([^>]+)>', parse_line))
|
||||
|
||||
|
||||
def get_gh_token():
|
||||
|
@ -148,131 +162,72 @@ in the future"""
|
|||
return raw_input('token: ').strip()
|
||||
|
||||
|
||||
def get_lbryum_part():
|
||||
print """The lbryum repo has changes but you didn't specify how to bump the
|
||||
version. Please enter one of: {}""".format(', '.join(LBRYUM_PARTS))
|
||||
while True:
|
||||
part = raw_input('part: ').strip()
|
||||
if part in LBRYUM_PARTS:
|
||||
return part
|
||||
print 'Invalid part. Enter one of: {}'.format(', '.join(LBRYUM_PARTS))
|
||||
def confirm():
|
||||
try:
|
||||
return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y'
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
|
||||
|
||||
def get_release_msg(changelogs, names):
|
||||
lines = []
|
||||
for name in names:
|
||||
entry = changelogs.get(name)
|
||||
if not entry:
|
||||
continue
|
||||
lines.append('## {}\n'.format(name))
|
||||
lines.append('{}\n'.format(entry))
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def run_sanity_checks(base, branch):
|
||||
if base.is_dirty():
|
||||
def run_sanity_checks(repo, branch):
|
||||
if repo.git_repo.is_dirty():
|
||||
print 'Cowardly refusing to release a dirty repo'
|
||||
sys.exit(1)
|
||||
if base.active_branch.name != branch:
|
||||
if repo.git_repo.active_branch.name != branch:
|
||||
print 'Cowardly refusing to release when not on the {} branch'.format(branch)
|
||||
sys.exit(1)
|
||||
if is_behind(base, branch):
|
||||
if repo.is_behind(branch):
|
||||
print 'Cowardly refusing to release when behind origin'
|
||||
sys.exit(1)
|
||||
check_bumpversion()
|
||||
|
||||
|
||||
def is_behind(base, branch):
|
||||
base.remotes.origin.fetch()
|
||||
rev_list = '{branch}...origin/{branch}'.format(branch=branch)
|
||||
commits_behind = base.git.rev_list(rev_list, right_only=True, count=True)
|
||||
commits_behind = int(commits_behind)
|
||||
return commits_behind > 0
|
||||
|
||||
|
||||
def check_bumpversion():
|
||||
def require_new_version():
|
||||
print 'Install bumpversion: pip install -U git+https://github.com/lbryio/bumpversion.git'
|
||||
if not is_custom_bumpversion_version():
|
||||
print (
|
||||
'Install LBRY\'s fork of bumpversion: '
|
||||
'pip install -U git+https://github.com/lbryio/bumpversion.git'
|
||||
)
|
||||
sys.exit(1)
|
||||
if not check_daemon_urls():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_daemon_urls():
|
||||
success = True
|
||||
with open(DAEMON_URL_FILE, 'r') as f:
|
||||
daemon_url_template = f.read().strip()
|
||||
if "OSNAME" not in daemon_url_template:
|
||||
print "Daemon URL must include the string 'OSNAME'"
|
||||
return False
|
||||
for osname in ('linux', 'macos', 'windows'):
|
||||
if not check_url(daemon_url_template.replace('OSNAME', osname)):
|
||||
success = False
|
||||
print "Daemon URL for " + osname + " does not work"
|
||||
return success
|
||||
|
||||
|
||||
def check_url(url):
|
||||
url = url.strip()
|
||||
r = requests.head(url)
|
||||
if r.status_code >= 400:
|
||||
return False
|
||||
elif r.status_code >= 300:
|
||||
new_location = r.headers.get('Location').strip()
|
||||
if new_location == url:
|
||||
# self-loop
|
||||
return False
|
||||
if "github-cloud.s3.amazonaws.com/releases" in new_location:
|
||||
# HEAD doesnt work on s3 links, so assume its good
|
||||
return True
|
||||
return check_url(new_location)
|
||||
return True
|
||||
|
||||
|
||||
def is_custom_bumpversion_version():
|
||||
try:
|
||||
output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT)
|
||||
output = output.strip()
|
||||
if output != 'bumpversion 0.5.4-lbry':
|
||||
require_new_version()
|
||||
except (subprocess.CalledProcessError, OSError) as err:
|
||||
require_new_version()
|
||||
|
||||
|
||||
def get_part(args, name):
|
||||
return getattr(args, name + '_part') or args.lbry_part
|
||||
|
||||
|
||||
class Repo(object):
|
||||
def __init__(self, name, part, directory=None):
|
||||
self.name = name
|
||||
self.part = part
|
||||
self.directory = directory or os.path.join(os.getcwd(), name)
|
||||
self.git_repo = git.Repo(self.directory)
|
||||
self.saved_commit = None
|
||||
self._bumped = False
|
||||
|
||||
def get_last_tag(self):
|
||||
return string.split(self.git_repo.git.describe(tags=True), '-')[0]
|
||||
|
||||
def get_submodule_hash(self, revision, submodule_path):
|
||||
line = getattr(self.git_repo.git, 'ls-tree')(revision, submodule_path)
|
||||
return string.split(line)[2] if line else None
|
||||
|
||||
def has_changes_from_revision(self, revision):
|
||||
commit = str(self.git_repo.commit())
|
||||
logging.info('%s =? %s', commit, revision)
|
||||
return commit != revision
|
||||
|
||||
def save_commit(self):
|
||||
self.saved_commit = self.git_repo.commit()
|
||||
logging.info('Saved ', self.git_repo.commit(), self.saved_commit)
|
||||
|
||||
def checkout(self, branch):
|
||||
self.git_repo.git.checkout(branch)
|
||||
self.git_repo.git.pull(rebase=True)
|
||||
|
||||
def get_changelog_entry(self):
|
||||
filename = os.path.join(self.directory, 'CHANGELOG.md')
|
||||
return changelog.bump(filename, self.new_version())
|
||||
|
||||
def add_changelog(self):
|
||||
with pushd(self.directory):
|
||||
self.git_repo.git.add('CHANGELOG.md')
|
||||
|
||||
def new_version(self):
|
||||
if self._bumped:
|
||||
raise Exception('Cannot calculate a new version on an already bumped repo')
|
||||
if not self.part:
|
||||
raise Exception('Cannot calculate a new version without a part')
|
||||
with pushd(self.directory):
|
||||
output = subprocess.check_output(
|
||||
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
|
||||
return re.search('^new_version=(.*)$', output, re.M).group(1)
|
||||
|
||||
def bumpversion(self):
|
||||
if self._bumped:
|
||||
raise Exception('Cowardly refusing to bump a repo twice')
|
||||
if not self.part:
|
||||
raise Exception('Cannot bump version for {}: no part specified'.format(repo.name))
|
||||
with pushd(self.directory):
|
||||
subprocess.check_call(['bumpversion', '--allow-dirty', self.part])
|
||||
self._bumped = True
|
||||
|
||||
def assert_new_tag_is_absent(self):
|
||||
new_tag = 'v' + self.new_version()
|
||||
tags = self.git_repo.git.tag()
|
||||
if new_tag in tags.split('\n'):
|
||||
raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name))
|
||||
|
||||
@property
|
||||
def git(self):
|
||||
return self.git_repo.git
|
||||
output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT).strip()
|
||||
if output == 'bumpversion 0.5.4-lbry':
|
||||
return True
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
@ -284,10 +239,4 @@ def pushd(new_dir):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s",
|
||||
level='INFO'
|
||||
)
|
||||
sys.exit(main())
|
||||
else:
|
||||
log = logging.getLogger('__name__')
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
@ -10,15 +9,13 @@ import github
|
|||
import requests
|
||||
import uritemplate
|
||||
|
||||
from lbrynet.core import log_support
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
current_tag = subprocess.check_output(
|
||||
['git', 'describe', '--exact-match', 'HEAD']).strip()
|
||||
except subprocess.CalledProcessError:
|
||||
log.info('Stopping as we are not currently on a tag')
|
||||
print 'Stopping as we are not currently on a tag'
|
||||
return
|
||||
|
||||
if 'GH_TOKEN' not in os.environ:
|
||||
|
@ -27,20 +24,15 @@ def main():
|
|||
|
||||
gh_token = os.environ['GH_TOKEN']
|
||||
auth = github.Github(gh_token)
|
||||
app_repo = auth.get_repo('lbryio/lbry-app')
|
||||
daemon_repo = auth.get_repo('lbryio/lbry')
|
||||
repo = auth.get_repo('lbryio/lbry-app')
|
||||
|
||||
if not check_repo_has_tag(app_repo, current_tag):
|
||||
log.info('Tag %s is not in repo %s', current_tag, app_repo)
|
||||
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
|
||||
|
||||
# daemon = get_daemon_artifact()
|
||||
# release = get_release(daemon_repo, current_tag)
|
||||
# upload_asset(release, daemon, gh_token)
|
||||
|
||||
app = get_app_artifact()
|
||||
release = get_release(app_repo, current_tag)
|
||||
release = get_release(repo, current_tag)
|
||||
upload_asset(release, app, gh_token)
|
||||
|
||||
|
||||
|
@ -60,21 +52,18 @@ def get_release(current_repo, current_tag):
|
|||
|
||||
|
||||
def get_app_artifact():
|
||||
this_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
system = platform.system()
|
||||
if system == 'Darwin':
|
||||
return glob.glob('dist/mac/LBRY*.dmg')[0]
|
||||
return glob.glob(this_dir + '/../dist/mac/LBRY*.dmg')[0]
|
||||
elif system == 'Linux':
|
||||
return glob.glob('dist/LBRY*.deb')[0]
|
||||
return glob.glob(this_dir + '/../dist/LBRY*.deb')[0]
|
||||
elif system == 'Windows':
|
||||
return glob.glob('dist/LBRY*.exe')[0]
|
||||
return glob.glob(this_dir + '/../dist/LBRY*.exe')[0]
|
||||
else:
|
||||
raise Exception("I don't know about any artifact on {}".format(system))
|
||||
|
||||
|
||||
def get_daemon_artifact():
|
||||
return glob.glob('dist/*.zip')[0]
|
||||
|
||||
|
||||
def upload_asset(release, asset_to_upload, token):
|
||||
basename = os.path.basename(asset_to_upload)
|
||||
if is_asset_already_uploaded(release, basename):
|
||||
|
@ -84,30 +73,26 @@ def upload_asset(release, asset_to_upload, token):
|
|||
try:
|
||||
return _upload_asset(release, asset_to_upload, token, _curl_uploader)
|
||||
except Exception:
|
||||
log.exception('Failed to upload')
|
||||
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}
|
||||
)
|
||||
upload_uri = uritemplate.expand(release.upload_url, {'name': basename})
|
||||
output = uploader(upload_uri, asset_to_upload, token)
|
||||
if 'errors' in output:
|
||||
raise Exception(output)
|
||||
else:
|
||||
log.info('Successfully uploaded to %s', output['browser_download_url'])
|
||||
print 'Successfully uploaded to {}'.format(output['browser_download_url'])
|
||||
|
||||
|
||||
# requests doesn't work on windows / linux / osx.
|
||||
def _requests_uploader(upload_uri, asset_to_upload, token):
|
||||
log.info('Using requests to upload %s to %s', asset_to_upload, upload_uri)
|
||||
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))
|
||||
output = response.json()
|
||||
return output
|
||||
return response.json()
|
||||
|
||||
|
||||
# curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login
|
||||
|
@ -118,7 +103,7 @@ def _curl_uploader(upload_uri, asset_to_upload, token):
|
|||
# half a day trying to debug before deciding to switch to curl.
|
||||
#
|
||||
# TODO: actually set the content type
|
||||
log.info('Using curl to upload %s to %s', asset_to_upload, upload_uri)
|
||||
print 'Using curl to upload {} to {}'.format(asset_to_upload, upload_uri)
|
||||
cmd = [
|
||||
'curl',
|
||||
'-sS',
|
||||
|
@ -141,21 +126,16 @@ def _curl_uploader(upload_uri, asset_to_upload, token):
|
|||
print stderr
|
||||
print 'stdout from curl:'
|
||||
print stdout
|
||||
output = json.loads(stdout)
|
||||
return output
|
||||
return json.loads(stdout)
|
||||
|
||||
|
||||
def is_asset_already_uploaded(release, basename):
|
||||
for asset in release.raw_data['assets']:
|
||||
if asset['name'] == basename:
|
||||
log.info('File %s has already been uploaded to %s', basename, release.tag_name)
|
||||
print 'File {} has already been uploaded to {}'.format(basename, release.tag_name)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log = logging.getLogger('release-on-tag')
|
||||
log_support.configure_console(level='INFO')
|
||||
sys.exit(main())
|
||||
else:
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
|
||||
cd "$ROOT"
|
||||
|
||||
(
|
||||
cd lbry
|
||||
git tag -d $(git describe)
|
||||
git reset --hard origin/master
|
||||
)
|
||||
|
||||
(
|
||||
cd lbryum
|
||||
git tag -d $(git describe)
|
||||
git reset --hard origin/master
|
||||
)
|
||||
|
||||
git tag -d $(git describe)
|
||||
git reset --hard HEAD~1
|
|
@ -1,28 +0,0 @@
|
|||
"""Set the build version to be 'dev', 'qa', 'rc', 'release'"""
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
build = get_build()
|
||||
with open(os.path.join('lbry', 'lbrynet', 'build_type.py'), 'w') as f:
|
||||
f.write('BUILD = "{}"'.format(build))
|
||||
|
||||
|
||||
def get_build():
|
||||
try:
|
||||
tag = subprocess.check_output(['git', 'describe', '--exact-match']).strip()
|
||||
if re.match('v\d+\.\d+\.\d+rc\d+', tag):
|
||||
return 'rc'
|
||||
else:
|
||||
return 'release'
|
||||
except subprocess.CalledProcessError:
|
||||
# if the build doesn't have a tag
|
||||
return 'qa'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
|
@ -1,58 +1,20 @@
|
|||
"""Set the package version to the output of `git describe`"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from __future__ import print_function
|
||||
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import fileinput
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--version', help="defaults to the output of `git describe`")
|
||||
args = parser.parse_args()
|
||||
if args.version:
|
||||
version = args.version
|
||||
else:
|
||||
tag = subprocess.check_output(['git', 'describe']).strip()
|
||||
try:
|
||||
version = get_version_from_tag(tag)
|
||||
except InvalidVersionTag:
|
||||
# this should be an error but its easier to handle here
|
||||
# than in the calling scripts.
|
||||
print 'Tag cannot be converted to a version, Exitting'
|
||||
return
|
||||
set_version(version)
|
||||
|
||||
|
||||
class InvalidVersionTag(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_version_from_tag(tag):
|
||||
match = re.match('v([\d.]+)', tag)
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
raise InvalidVersionTag('Failed to parse version from tag {}'.format(tag))
|
||||
|
||||
|
||||
def set_version(version):
|
||||
package_file = os.path.join('app', 'package.json')
|
||||
with open(package_file) as fp:
|
||||
package_data = json.load(fp)
|
||||
package_data['version'] = version
|
||||
with open(package_file, 'w') as fp:
|
||||
json.dump(package_data, fp, indent=2, separators=(',', ': '))
|
||||
with open(os.path.join('lbry', 'lbrynet', '__init__.py'), 'w') as fp:
|
||||
fp.write(LBRYNET_TEMPLATE.format(version=version))
|
||||
|
||||
|
||||
LBRYNET_TEMPLATE = """
|
||||
__version__ = "{version}"
|
||||
version = tuple(__version__.split('.'))
|
||||
"""
|
||||
filename = os.path.abspath(
|
||||
os.path.join(os.path.abspath(__file__), '..', '..', 'ui', 'js', 'lbryio.js'))
|
||||
for line in fileinput.input(filename, inplace=True):
|
||||
if line.startswith(' enabled: false'):
|
||||
print(' enabled: true')
|
||||
else:
|
||||
print(line, end='')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
|
||||
def main():
|
||||
tag = subprocess.check_output(['git', 'describe']).strip()
|
||||
zipfilename = 'lbrynet-daemon-{}-{}.zip'.format(tag, get_system_label())
|
||||
full_filename = os.path.join('dist', zipfilename)
|
||||
executables = ['lbrynet-daemon', 'lbrynet-cli']
|
||||
ext = '.exe' if platform.system() == 'Windows' else ''
|
||||
with zipfile.ZipFile(full_filename, 'w') as myzip:
|
||||
for executable in executables:
|
||||
myzip.write(os.path.join('app', 'dist', executable + ext), executable + ext)
|
||||
|
||||
|
||||
def get_system_label():
|
||||
system = platform.system()
|
||||
if system == 'Darwin':
|
||||
return 'macos'
|
||||
else:
|
||||
return system.lower()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
|
@ -1,28 +0,0 @@
|
|||
$env:Path += ";C:\MinGW\bin\"
|
||||
|
||||
$env:Path += ";C:\Program Files (x86)\Windows Kits\10\bin\x86\"
|
||||
gcc --version
|
||||
mingw32-make --version
|
||||
|
||||
# build/install miniupnpc manually
|
||||
tar zxf miniupnpc-1.9.tar.gz
|
||||
cd miniupnpc-1.9
|
||||
mingw32-make.exe -f Makefile.mingw
|
||||
python.exe setupmingw32.py build --compiler=mingw32
|
||||
python.exe setupmingw32.py install
|
||||
cd ..\
|
||||
Remove-Item -Recurse -Force miniupnpc-1.9
|
||||
|
||||
# copy requirements from lbry, but remove lbryum (we'll add it back in below) and gmpy and miniupnpc (installed manually)
|
||||
Get-Content ..\lbry\requirements.txt | Select-String -Pattern 'lbryum|gmpy|miniupnpc' -NotMatch | Out-File requirements.txt
|
||||
# add in gmpy wheel
|
||||
Add-Content requirements.txt "./gmpy-1.17-cp27-none-win32.whl"
|
||||
# for electron, we install lbryum and lbry using submodules
|
||||
Add-Content requirements.txt "../lbryum"
|
||||
Add-Content requirements.txt "../lbry"
|
||||
|
||||
pip.exe install pyinstaller
|
||||
pip.exe install -r requirements.txt
|
||||
|
||||
pyinstaller -y daemon.onefile.spec
|
||||
pyinstaller -y cli.onefile.spec
|
|
@ -1,58 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
import platform
|
||||
import os
|
||||
|
||||
|
||||
cwd = os.getcwd()
|
||||
if os.path.basename(cwd) != 'daemon':
|
||||
raise Exception('The build needs to be run from the same directory as the spec file')
|
||||
repo_base = os.path.abspath(os.path.join(cwd, '..'))
|
||||
|
||||
|
||||
system = platform.system()
|
||||
if system == 'Darwin':
|
||||
icns = os.path.join(repo_base, 'build', 'icon.icns')
|
||||
elif system == 'Linux':
|
||||
icns = os.path.join(repo_base, 'build', 'icons', '256x256.png')
|
||||
elif system == 'Windows':
|
||||
icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico')
|
||||
else:
|
||||
print 'Warning: System {} has no icons'.format(system)
|
||||
icns = None
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['cli.py'],
|
||||
pathex=[cwd],
|
||||
binaries=None,
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher
|
||||
)
|
||||
|
||||
pyz = PYZ(
|
||||
a.pure,
|
||||
a.zipped_data,
|
||||
cipher=block_cipher
|
||||
)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='lbrynet-cli',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
icon=icns
|
||||
)
|
|
@ -1,7 +0,0 @@
|
|||
from lbrynet.lbrynet_daemon import DaemonCLI
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
|
||||
if __name__ == '__main__':
|
||||
DaemonCLI.main()
|
|
@ -1,77 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
import platform
|
||||
import os
|
||||
|
||||
import lbryum
|
||||
|
||||
|
||||
cwd = os.getcwd()
|
||||
if os.path.basename(cwd) != 'daemon':
|
||||
raise Exception('The build needs to be run from the same directory as the spec file')
|
||||
repo_base = os.path.abspath(os.path.join(cwd, '..'))
|
||||
|
||||
|
||||
system = platform.system()
|
||||
if system == 'Darwin':
|
||||
icns = os.path.join(repo_base, 'build', 'icon.icns')
|
||||
elif system == 'Linux':
|
||||
icns = os.path.join(repo_base, 'build', 'icons', '256x256.png')
|
||||
elif system == 'Windows':
|
||||
icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico')
|
||||
else:
|
||||
print 'Warning: System {} has no icons'.format(system)
|
||||
icns = None
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
languages = (
|
||||
'chinese_simplified.txt', 'japanese.txt', 'spanish.txt',
|
||||
'english.txt', 'portuguese.txt'
|
||||
)
|
||||
|
||||
|
||||
datas = [
|
||||
(
|
||||
os.path.join(os.path.dirname(lbryum.__file__), 'wordlist', language),
|
||||
'lbryum/wordlist'
|
||||
)
|
||||
for language in languages
|
||||
]
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['daemon.py'],
|
||||
pathex=[cwd],
|
||||
binaries=None,
|
||||
datas=datas,
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher
|
||||
)
|
||||
|
||||
|
||||
pyz = PYZ(
|
||||
a.pure, a.zipped_data,
|
||||
cipher=block_cipher
|
||||
)
|
||||
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='lbrynet-daemon',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
icon=icns
|
||||
)
|
|
@ -1,4 +0,0 @@
|
|||
from lbrynet.lbrynet_daemon import DaemonControl
|
||||
|
||||
if __name__ == '__main__':
|
||||
DaemonControl.start()
|
Binary file not shown.
Binary file not shown.
1
lbry
1
lbry
|
@ -1 +0,0 @@
|
|||
Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1
|
1
lbryum
1
lbryum
|
@ -1 +0,0 @@
|
|||
Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739
|
|
@ -42,5 +42,6 @@
|
|||
"devDependencies": {
|
||||
"electron": "^1.4.15",
|
||||
"electron-builder": "^11.7.0"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
166
ui/js/app.js
166
ui/js/app.js
|
@ -7,16 +7,16 @@ import HelpPage from './page/help.js';
|
|||
import WatchPage from './page/watch.js';
|
||||
import ReportPage from './page/report.js';
|
||||
import StartPage from './page/start.js';
|
||||
import ClaimCodePage from './page/claim_code.js';
|
||||
import ReferralPage from './page/referral.js';
|
||||
import RewardsPage from './page/rewards.js';
|
||||
import RewardPage from './page/reward.js';
|
||||
import WalletPage from './page/wallet.js';
|
||||
import DetailPage from './page/show.js';
|
||||
import ShowPage from './page/show.js';
|
||||
import PublishPage from './page/publish.js';
|
||||
import SearchPage from './page/search.js';
|
||||
import DiscoverPage from './page/discover.js';
|
||||
import SplashScreen from './component/splash.js';
|
||||
import DeveloperPage from './page/developer.js';
|
||||
import lbryuri from './lbryuri.js';
|
||||
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
|
||||
import Drawer from './component/drawer.js';
|
||||
import Header from './component/header.js';
|
||||
import {Modal, ExpandableModal} from './component/modal.js';
|
||||
import {Link} from './component/link.js';
|
||||
|
@ -38,17 +38,12 @@ var App = React.createClass({
|
|||
message: 'Error message',
|
||||
data: 'Error data',
|
||||
},
|
||||
_fullScreenPages: ['watch'],
|
||||
_storeHistoryOfNextRender: false,
|
||||
|
||||
_upgradeDownloadItem: null,
|
||||
_isMounted: false,
|
||||
_version: null,
|
||||
|
||||
// Temporary workaround since electron-dl throws errors when you try to get the filename
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
address: window.location.search
|
||||
};
|
||||
},
|
||||
getUpdateUrl: function() {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
|
@ -80,15 +75,13 @@ var App = React.createClass({
|
|||
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
|
||||
return {
|
||||
viewingPage: viewingPage,
|
||||
pageArgs: pageArgs === undefined ? null : pageArgs
|
||||
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
var match, param, val, viewingPage, pageArgs,
|
||||
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
|
||||
|
||||
return Object.assign(this.getViewingPageAndArgs(this.props.address), {
|
||||
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
|
||||
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
|
||||
viewingPage: 'discover',
|
||||
appUrl: null,
|
||||
errorInfo: null,
|
||||
modal: null,
|
||||
downloadProgress: null,
|
||||
|
@ -96,6 +89,8 @@ var App = React.createClass({
|
|||
});
|
||||
},
|
||||
componentWillMount: function() {
|
||||
window.addEventListener("popstate", this.onHistoryPop);
|
||||
|
||||
document.addEventListener('unhandledError', (event) => {
|
||||
this.alertError(event.detail);
|
||||
});
|
||||
|
@ -112,7 +107,10 @@ var App = React.createClass({
|
|||
if (target.matches('a[href^="?"]')) {
|
||||
event.preventDefault();
|
||||
if (this._isMounted) {
|
||||
this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
|
||||
let appUrl = target.getAttribute('href');
|
||||
this._storeHistoryOfNextRender = true;
|
||||
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
|
||||
document.body.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
target = target.parentNode;
|
||||
|
@ -120,28 +118,16 @@ var App = React.createClass({
|
|||
});
|
||||
|
||||
if (!sessionStorage.getItem('upgradeSkipped')) {
|
||||
lbry.checkNewVersionAvailable(({isAvailable}) => {
|
||||
if (!isAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
lbry.getVersionInfo((versionInfo) => {
|
||||
this._version = versionInfo.lbrynet_version;
|
||||
lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
|
||||
if (upgradeAvailable) {
|
||||
this._version = remoteVersion;
|
||||
this.setState({
|
||||
modal: 'upgrade',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
openDrawer: function() {
|
||||
sessionStorage.setItem('drawerOpen', true);
|
||||
this.setState({ drawerOpen: true });
|
||||
},
|
||||
closeDrawer: function() {
|
||||
sessionStorage.setItem('drawerOpen', false);
|
||||
this.setState({ drawerOpen: false });
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
|
@ -152,6 +138,28 @@ var App = React.createClass({
|
|||
},
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
window.removeEventListener("popstate", this.onHistoryPop);
|
||||
},
|
||||
onHistoryPop: function() {
|
||||
this.setState(this.getViewingPageAndArgs(location.search));
|
||||
},
|
||||
onSearch: function(term) {
|
||||
this._storeHistoryOfNextRender = true;
|
||||
const isShow = term.startsWith('lbry://');
|
||||
this.setState({
|
||||
viewingPage: isShow ? "show" : "search",
|
||||
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
|
||||
pageArgs: term
|
||||
});
|
||||
},
|
||||
onSubmit: function(uri) {
|
||||
this._storeHistoryOfNextRender = true;
|
||||
this.setState({
|
||||
address: uri,
|
||||
appUrl: "?show=" + encodeURIComponent(uri),
|
||||
viewingPage: "show",
|
||||
pageArgs: uri
|
||||
})
|
||||
},
|
||||
handleUpgradeClicked: function() {
|
||||
// Make a new directory within temp directory so the filename is guaranteed to be available
|
||||
|
@ -205,12 +213,6 @@ var App = React.createClass({
|
|||
modal: null,
|
||||
});
|
||||
},
|
||||
onSearch: function(term) {
|
||||
this.setState({
|
||||
viewingPage: 'discover',
|
||||
pageArgs: term
|
||||
});
|
||||
},
|
||||
alertError: function(error) {
|
||||
var errorInfoList = [];
|
||||
for (let key of Object.keys(error)) {
|
||||
|
@ -224,81 +226,57 @@ var App = React.createClass({
|
|||
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
|
||||
});
|
||||
},
|
||||
getHeaderLinks: function()
|
||||
{
|
||||
switch(this.state.viewingPage)
|
||||
{
|
||||
case 'wallet':
|
||||
case 'send':
|
||||
case 'receive':
|
||||
case 'claim':
|
||||
case 'referral':
|
||||
return {
|
||||
'?wallet' : 'Overview',
|
||||
'?send' : 'Send',
|
||||
'?receive' : 'Receive',
|
||||
'?claim' : 'Claim Beta Code',
|
||||
'?referral' : 'Check Referral Credit',
|
||||
};
|
||||
case 'downloaded':
|
||||
case 'published':
|
||||
return {
|
||||
'?downloaded': 'Downloaded',
|
||||
'?published': 'Published',
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getMainContent: function()
|
||||
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 <SettingsPage />;
|
||||
return ["Settings", "icon-gear", <SettingsPage />];
|
||||
case 'help':
|
||||
return <HelpPage />;
|
||||
case 'watch':
|
||||
return <WatchPage name={this.state.pageArgs} />;
|
||||
return ["Help", "icon-question", <HelpPage />];
|
||||
case 'report':
|
||||
return <ReportPage />;
|
||||
return ['Report an Issue', 'icon-file', <ReportPage />];
|
||||
case 'downloaded':
|
||||
return <FileListDownloaded />;
|
||||
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
|
||||
case 'published':
|
||||
return <FileListPublished />;
|
||||
return ["Publishes", "icon-folder", <FileListPublished />];
|
||||
case 'start':
|
||||
return <StartPage />;
|
||||
case 'claim':
|
||||
return <ClaimCodePage />;
|
||||
case 'referral':
|
||||
return <ReferralPage />;
|
||||
return ["Start", "icon-file", <StartPage />];
|
||||
case 'rewards':
|
||||
return ["Rewards", "icon-bank", <RewardsPage />];
|
||||
case 'wallet':
|
||||
case 'send':
|
||||
case 'receive':
|
||||
return <WalletPage viewingPage={this.state.viewingPage} />;
|
||||
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
|
||||
case 'show':
|
||||
return <DetailPage name={this.state.pageArgs} />;
|
||||
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
|
||||
case 'publish':
|
||||
return <PublishPage />;
|
||||
return ["Publish", "icon-upload", <PublishPage />];
|
||||
case 'developer':
|
||||
return <DeveloperPage />;
|
||||
return ["Developer", "icon-file", <DeveloperPage />];
|
||||
case 'discover':
|
||||
default:
|
||||
return <DiscoverPage {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
|
||||
return ["Home", "icon-home", <DiscoverPage />];
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var mainContent = this.getMainContent(),
|
||||
headerLinks = this.getHeaderLinks(),
|
||||
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
|
||||
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
|
||||
|
||||
lbry.setTitle(address);
|
||||
|
||||
if (this._storeHistoryOfNextRender) {
|
||||
this._storeHistoryOfNextRender = false;
|
||||
history.pushState({}, document.title, this.state.appUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
this.state.viewingPage == 'watch' ?
|
||||
this._fullScreenPages.includes(this.state.viewingPage) ?
|
||||
mainContent :
|
||||
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
|
||||
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
|
||||
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
|
||||
<Header onOpenDrawer={this.openDrawer} initialQuery={searchQuery} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} />
|
||||
<div id="window">
|
||||
<Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
|
||||
<div id="main-content">
|
||||
{mainContent}
|
||||
</div>
|
||||
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
|
||||
|
@ -325,7 +303,7 @@ var App = React.createClass({
|
|||
</div>
|
||||
</Modal>
|
||||
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
|
||||
overlayClassName="error-modal-overlay" onConfirmed={this.closeModal}
|
||||
overlayClassName="modal-overlay error-modal-overlay" onConfirmed={this.closeModal}
|
||||
extraContent={this.state.errorInfo}>
|
||||
<h3 className="modal__header">Error</h3>
|
||||
|
||||
|
|
301
ui/js/component/auth.js
Normal file
301
ui/js/component/auth.js
Normal file
|
@ -0,0 +1,301 @@
|
|||
import React from "react";
|
||||
import lbryio from "../lbryio.js";
|
||||
import Modal from "./modal.js";
|
||||
import ModalPage from "./modal-page.js";
|
||||
import {Link, RewardLink} from "../component/link.js";
|
||||
import {FormRow} from "../component/form.js";
|
||||
import {CreditAmount, Address} from "../component/common.js";
|
||||
import {getLocal, getSession, setSession, setLocal} from '../utils.js';
|
||||
|
||||
|
||||
const SubmitEmailStage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
rewardType: null,
|
||||
email: '',
|
||||
submitting: false
|
||||
};
|
||||
},
|
||||
handleEmailChanged: function(event) {
|
||||
this.setState({
|
||||
email: event.target.value,
|
||||
});
|
||||
},
|
||||
onEmailSaved: function(email) {
|
||||
this.props.setStage("confirm", { email: email })
|
||||
},
|
||||
handleSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => {
|
||||
this.onEmailSaved(this.state.email);
|
||||
}, (error) => {
|
||||
if (error.xhr && error.xhr.status == 409) {
|
||||
this.onEmailSaved(this.state.email);
|
||||
return;
|
||||
} else if (this._emailRow) {
|
||||
this._emailRow.showError(error.message)
|
||||
}
|
||||
this.setState({ submitting: false });
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io"
|
||||
name="email" value={this.state.email}
|
||||
onChange={this.handleEmailChanged} />
|
||||
<div className="form-row-submit">
|
||||
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const ConfirmEmailStage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
rewardType: null,
|
||||
code: '',
|
||||
submitting: false,
|
||||
errorMessage: null,
|
||||
};
|
||||
},
|
||||
handleCodeChanged: function(event) {
|
||||
this.setState({
|
||||
code: event.target.value,
|
||||
});
|
||||
},
|
||||
handleSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
|
||||
const onSubmitError = (error) => {
|
||||
if (this._codeRow) {
|
||||
this._codeRow.showError(error.message)
|
||||
}
|
||||
this.setState({ submitting: false });
|
||||
};
|
||||
|
||||
lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => {
|
||||
if (userEmail.IsVerified) {
|
||||
this.props.setStage("welcome")
|
||||
} else {
|
||||
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
|
||||
}
|
||||
}, onSubmitError);
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
|
||||
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
|
||||
helper="A verification code is required to access this version."/>
|
||||
<div className="form-row-submit form-row-submit--with-footer">
|
||||
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
<div className="form-field__helper">
|
||||
No code? <Link onClick={() => { this.props.setStage("nocode")}} label="Click here" />.
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const WelcomeStage = React.createClass({
|
||||
propTypes: {
|
||||
endAuth: React.PropTypes.func,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hasReward: false,
|
||||
rewardAmount: null,
|
||||
}
|
||||
},
|
||||
onRewardClaim: function(reward) {
|
||||
this.setState({
|
||||
hasReward: true,
|
||||
rewardAmount: reward.amount
|
||||
})
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
!this.state.hasReward ?
|
||||
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
|
||||
<section>
|
||||
<h3 className="modal__header">Welcome to LBRY.</h3>
|
||||
<p>Using LBRY is like dating a centaur. Totally normal up top, and <em>way different</em> underneath.</p>
|
||||
<p>Up top, LBRY is similar to popular media sites.</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>
|
||||
<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) }} />
|
||||
</div>
|
||||
</section>
|
||||
</Modal> :
|
||||
<Modal type="alert" overlayClassName="modal-overlay modal-overlay--clear" isOpen={true} contentLabel="Welcome to LBRY" {...this.props} onConfirmed={() => { this.props.setStage(null) }}>
|
||||
<section>
|
||||
<h3 className="modal__header">About Your Reward</h3>
|
||||
<p>You earned a reward of <CreditAmount amount={this.state.rewardAmount} label={false} /> LBRY credits, or <em>LBC</em>.</p>
|
||||
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
|
||||
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
|
||||
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
|
||||
<p>Finally, know that LBRY is a beta and that it earns the name.</p>
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const ErrorStage = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<p>An error was encountered that we cannot continue from.</p>
|
||||
<p>At least we're earning the name beta.</p>
|
||||
{ this.props.errorText ? <p>Message: {this.props.errorText}</p> : '' }
|
||||
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const PendingStage = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<p>Preparing for first access <span className="busy-indicator"></span></p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const CodeRequiredStage = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
getInitialState: function() {
|
||||
return {
|
||||
balance: 0,
|
||||
address: getLocal('wallet_address')
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
||||
this.setState({
|
||||
balance: balance
|
||||
});
|
||||
})
|
||||
|
||||
if (!this.state.address) {
|
||||
lbry.getUnusedAddress((address) => {
|
||||
setLocal('wallet_address', address);
|
||||
this.setState({ address: address });
|
||||
});
|
||||
}
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
if (this._balanceSubscribeId) {
|
||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
const disabled = this.state.balance < 1;
|
||||
return (
|
||||
<div>
|
||||
<section className="section-spaced">
|
||||
<p>Access to LBRY is restricted as we build and scale the network.</p>
|
||||
<p>There are two ways in:</p>
|
||||
<h3>Own LBRY Credits</h3>
|
||||
<p>If you own at least 1 LBC, you can get in right now.</p>
|
||||
<p style={{ textAlign: "center"}}><Link onClick={() => { setLocal('auth_bypassed', true); this.props.setStage(null); }}
|
||||
disabled={disabled} label="Let Me In" button={ disabled ? "alt" : "primary" } /></p>
|
||||
<p>Your balance is <CreditAmount amount={this.state.balance} />. To increase your balance, send credits to this address:</p>
|
||||
<p><Address address={ this.state.address ? this.state.address : "Generating Address..." } /></p>
|
||||
<p>If you don't understand how to send credits, then...</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Wait For A Code</h3>
|
||||
<p>If you provide your email, you'll automatically receive a notification when the system is open.</p>
|
||||
<p><Link onClick={() => { this.props.setStage("email"); }} label="Return" /></p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const AuthOverlay = React.createClass({
|
||||
_stages: {
|
||||
pending: PendingStage,
|
||||
error: ErrorStage,
|
||||
nocode: CodeRequiredStage,
|
||||
email: SubmitEmailStage,
|
||||
confirm: ConfirmEmailStage,
|
||||
welcome: WelcomeStage
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
stage: "pending",
|
||||
stageProps: {}
|
||||
};
|
||||
},
|
||||
setStage: function(stage, stageProps = {}) {
|
||||
this.setState({
|
||||
stage: stage,
|
||||
stageProps: stageProps
|
||||
})
|
||||
},
|
||||
componentWillMount: function() {
|
||||
lbryio.authenticate().then((user) => {
|
||||
if (!user.HasVerifiedEmail) {
|
||||
if (getLocal('auth_bypassed')) {
|
||||
this.setStage(null)
|
||||
} else {
|
||||
this.setStage("email", {})
|
||||
}
|
||||
} else {
|
||||
lbryio.call('reward', 'list', {}).then((userRewards) => {
|
||||
userRewards.filter(function(reward) {
|
||||
return reward.RewardType == "new_user" && reward.TransactionID;
|
||||
}).length ?
|
||||
this.setStage(null) :
|
||||
this.setStage("welcome")
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.setStage("error", { errorText: err.message })
|
||||
document.dispatchEvent(new CustomEvent('unhandledError', {
|
||||
detail: {
|
||||
message: err.message,
|
||||
data: err.stack
|
||||
}
|
||||
}));
|
||||
})
|
||||
},
|
||||
render: function() {
|
||||
if (!this.state.stage) {
|
||||
return null;
|
||||
}
|
||||
const StageContent = this._stages[this.state.stage];
|
||||
return (
|
||||
this.state.stage != "welcome" ?
|
||||
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
|
||||
<h1>LBRY Early Access</h1>
|
||||
<StageContent {...this.state.stageProps} setStage={this.setStage} />
|
||||
</ModalPage> :
|
||||
<StageContent setStage={this.setStage} {...this.state.stageProps} />
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,31 +1,29 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import uri from '../uri.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Icon} from './common.js';
|
||||
|
||||
const ChannelIndicator = React.createClass({
|
||||
const UriIndicator = React.createClass({
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string.isRequired,
|
||||
claimInfo: React.PropTypes.object.isRequired,
|
||||
hasSignature: React.PropTypes.bool.isRequired,
|
||||
signatureIsValid: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
const {name, has_signature, signature_is_valid} = this.props.claimInfo;
|
||||
if (!has_signature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uriObj = uri.parseLbryUri(this.props.uri);
|
||||
if (!uriObj.isChannel) {
|
||||
return null;
|
||||
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;
|
||||
const channelUri = uri.buildLbryUri(channelUriObj, false);
|
||||
delete channelUriObj.contentName;
|
||||
const channelUri = lbryuri.build(channelUriObj, false);
|
||||
|
||||
let icon, modifier;
|
||||
if (!signature_is_valid) {
|
||||
icon = 'icon-check-circle';
|
||||
if (this.props.signatureIsValid) {
|
||||
modifier = 'valid';
|
||||
} else {
|
||||
icon = 'icon-times-circle';
|
||||
|
@ -33,11 +31,13 @@ const ChannelIndicator = React.createClass({
|
|||
}
|
||||
return (
|
||||
<span>
|
||||
by <strong>{channelUri}</strong> {' '}
|
||||
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} />
|
||||
{channelUri} {' '}
|
||||
{ !this.props.signatureIsValid ?
|
||||
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
|
||||
'' }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ChannelIndicator;
|
||||
export default UriIndicator;
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import $clamp from 'clamp-js-main';
|
||||
|
||||
//component/icon.js
|
||||
export let Icon = React.createClass({
|
||||
|
@ -19,29 +18,15 @@ export let Icon = React.createClass({
|
|||
|
||||
export let TruncatedText = React.createClass({
|
||||
propTypes: {
|
||||
lines: React.PropTypes.number,
|
||||
height: React.PropTypes.string,
|
||||
auto: React.PropTypes.bool,
|
||||
lines: React.PropTypes.number
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
lines: null,
|
||||
height: null,
|
||||
auto: true,
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
// Manually round up the line height, because clamp.js doesn't like fractional-pixel line heights.
|
||||
|
||||
// Need to work directly on the style object because setting the style prop doesn't update internal styles right away.
|
||||
this.refs.span.style.lineHeight = Math.ceil(parseFloat(getComputedStyle(this.refs.span).lineHeight)) + 'px';
|
||||
|
||||
$clamp(this.refs.span, {
|
||||
clamp: this.props.lines || this.props.height || 'auto',
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return <span ref="span" className="truncated-text">{this.props.children}</span>;
|
||||
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -54,45 +39,109 @@ export let BusyMessage = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var creditAmountStyle = {
|
||||
color: '#216C2A',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '0.8em'
|
||||
}, estimateStyle = {
|
||||
fontSize: '0.8em',
|
||||
color: '#aaa',
|
||||
};
|
||||
|
||||
export let CurrencySymbol = React.createClass({
|
||||
render: function() { return <span>LBC</span>; }
|
||||
});
|
||||
|
||||
export let CreditAmount = React.createClass({
|
||||
propTypes: {
|
||||
amount: React.PropTypes.number,
|
||||
precision: React.PropTypes.number
|
||||
amount: React.PropTypes.number.isRequired,
|
||||
precision: React.PropTypes.number,
|
||||
isEstimate: React.PropTypes.bool,
|
||||
label: React.PropTypes.bool,
|
||||
showFree: React.PropTypes.bool,
|
||||
look: React.PropTypes.oneOf(['indicator', 'plain']),
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
precision: 1,
|
||||
label: true,
|
||||
showFree: false,
|
||||
look: 'indicator',
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision ? this.props.precision : 1);
|
||||
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
|
||||
let amountText;
|
||||
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
|
||||
amountText = 'free';
|
||||
} else if (this.props.label) {
|
||||
amountText = formattedAmount + (parseFloat(formattedAmount) == 1 ? ' credit' : ' credits');
|
||||
} else {
|
||||
amountText = formattedAmount;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="credit-amount">
|
||||
<span style={creditAmountStyle}>{formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'}</span>
|
||||
{ this.props.isEstimate ? <span style={estimateStyle}> (est)</span> : null }
|
||||
<span className={`credit-amount credit-amount--${this.props.look}`}>
|
||||
<span>
|
||||
{amountText}
|
||||
</span>
|
||||
{ this.props.isEstimate ? <span className="credit-amount__estimate" title="This is an estimate and does not include data fees">*</span> : null }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export let FilePrice = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
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',
|
||||
};
|
||||
export let Address = React.createClass({
|
||||
_inputElem: null,
|
||||
propTypes: {
|
||||
address: React.PropTypes.string,
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<span style={addressStyle}>{this.props.address}</span>
|
||||
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
|
||||
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -131,6 +180,9 @@ export let Thumbnail = React.createClass({
|
|||
this._isMounted = false;
|
||||
},
|
||||
render: function() {
|
||||
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} />
|
||||
const className = this.props.className ? this.props.className : '',
|
||||
otherProps = Object.assign({}, this.props)
|
||||
delete otherProps.className;
|
||||
return <img ref="img" onError={this.handleError} {...otherProps} className={className} src={this.state.imageUri} />
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import lbry from '../lbry.js';
|
||||
import React from 'react';
|
||||
import {Link} from './link.js';
|
||||
|
||||
var DrawerItem = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
subPages: [],
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
|
||||
this.props.subPages.indexOf(this.props.viewingPage) != -1);
|
||||
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
|
||||
}
|
||||
});
|
||||
|
||||
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
|
||||
height: '36px'
|
||||
};
|
||||
|
||||
var Drawer = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
|
||||
handleLogoClicked: function(event) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||||
window.location.href = '?developer'
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
balance: 0,
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
|
||||
this.setState({
|
||||
balance: balance
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
if (this._balanceSubscribeId) {
|
||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<nav id="drawer">
|
||||
<div id="drawer-handle">
|
||||
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
|
||||
<a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
|
||||
</div>
|
||||
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
|
||||
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
|
||||
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
|
||||
<DrawerItem href="?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
|
||||
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
|
||||
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default Drawer;
|
|
@ -1,76 +1,24 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {Icon} from '../component/common.js';
|
||||
import Modal from './modal.js';
|
||||
import FormField from './form.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 WatchLink = React.createClass({
|
||||
propTypes: {
|
||||
streamName: React.PropTypes.string,
|
||||
downloadStarted: React.PropTypes.bool,
|
||||
},
|
||||
startVideo: function() {
|
||||
window.location = '?watch=' + this.props.streamName;
|
||||
},
|
||||
handleClick: function() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
if (this.props.downloadStarted) {
|
||||
this.startVideo();
|
||||
} else {
|
||||
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
|
||||
lbry.getBalance((balance) => {
|
||||
if (cost > balance) {
|
||||
this.setState({
|
||||
modal: 'notEnoughCredits',
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
this.startVideo();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
modal: null,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div className="button-set-item">
|
||||
<Link button="primary" disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} />
|
||||
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
|
||||
You don't have enough LBRY credits to pay for this stream.
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let FileActionsRow = React.createClass({
|
||||
_isMounted: false,
|
||||
_fileInfoSubscribeId: null,
|
||||
|
||||
propTypes: {
|
||||
streamName: React.PropTypes.string,
|
||||
uri: React.PropTypes.string,
|
||||
outpoint: React.PropTypes.string.isRequired,
|
||||
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
|
||||
contentType: React.PropTypes.string,
|
||||
contentType: React.PropTypes.string.isRequired,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
|
@ -79,7 +27,7 @@ let FileActionsRow = React.createClass({
|
|||
menuOpen: false,
|
||||
deleteChecked: false,
|
||||
attemptingDownload: false,
|
||||
attemptingRemove: false
|
||||
attemptingRemove: false,
|
||||
}
|
||||
},
|
||||
onFileInfoUpdate: function(fileInfo) {
|
||||
|
@ -95,15 +43,15 @@ let FileActionsRow = React.createClass({
|
|||
attemptingDownload: true,
|
||||
attemptingRemove: false
|
||||
});
|
||||
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
|
||||
lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
||||
lbry.getBalance((balance) => {
|
||||
if (cost > balance) {
|
||||
this.setState({
|
||||
modal: 'notEnoughCredits',
|
||||
attemptingDownload: false,
|
||||
});
|
||||
} else {
|
||||
lbry.getStream(this.props.streamName, (streamInfo) => {
|
||||
} else if (this.state.affirmedPurchase) {
|
||||
lbry.get({uri: this.props.uri}).then((streamInfo) => {
|
||||
if (streamInfo === null || typeof streamInfo !== 'object') {
|
||||
this.setState({
|
||||
modal: 'timedOut',
|
||||
|
@ -111,6 +59,11 @@ let FileActionsRow = React.createClass({
|
|||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
attemptingDownload: false,
|
||||
modal: 'affirmPurchase'
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -153,6 +106,13 @@ let FileActionsRow = React.createClass({
|
|||
attemptingDownload: false
|
||||
});
|
||||
},
|
||||
onAffirmPurchase: function() {
|
||||
this.setState({
|
||||
affirmedPurchase: true,
|
||||
modal: null
|
||||
});
|
||||
this.tryDownload();
|
||||
},
|
||||
openMenu: function() {
|
||||
this.setState({
|
||||
menuOpen: !this.state.menuOpen,
|
||||
|
@ -196,11 +156,10 @@ let FileActionsRow = React.createClass({
|
|||
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.props.contentType && this.props.contentType.startsWith('video/')
|
||||
? <WatchLink streamName={this.props.streamName} downloadStarted={!!this.state.fileInfo} />
|
||||
: null}
|
||||
{this.state.fileInfo !== null || this.state.fileInfo.isMine
|
||||
? linkBlock
|
||||
: null}
|
||||
|
@ -209,18 +168,22 @@ let FileActionsRow = React.createClass({
|
|||
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
|
||||
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
|
||||
</DropDownMenu> : '' }
|
||||
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
|
||||
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
|
||||
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>lbry://{this.props.streamName}</strong>.
|
||||
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>{this.props.metadata.title}</cite> from LBRY?</p>
|
||||
<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>
|
||||
|
@ -234,7 +197,7 @@ export let FileActions = React.createClass({
|
|||
_fileInfoSubscribeId: null,
|
||||
|
||||
propTypes: {
|
||||
streamName: React.PropTypes.string,
|
||||
uri: React.PropTypes.string,
|
||||
outpoint: React.PropTypes.string.isRequired,
|
||||
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
|
||||
contentType: React.PropTypes.string,
|
||||
|
@ -261,7 +224,8 @@ export let FileActions = React.createClass({
|
|||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
|
||||
lbry.getStreamAvailability(this.props.uri, (availability) => {
|
||||
|
||||
lbry.get_availability({uri: this.props.uri}, (availability) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
available: availability > 0,
|
||||
|
@ -291,10 +255,10 @@ export let FileActions = React.createClass({
|
|||
return (<section className="file-actions">
|
||||
{
|
||||
fileInfo || this.state.available || this.state.forceShowActions
|
||||
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} streamName={this.props.streamName}
|
||||
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
|
||||
contentType={this.props.contentType} />
|
||||
: <div>
|
||||
<div className="button-set-item empty">This file is not currently available.</div>
|
||||
<div className="button-set-item empty">Content unavailable.</div>
|
||||
<ToolTip label="Why?"
|
||||
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
|
||||
className="button-set-item" />
|
||||
|
|
|
@ -1,67 +1,23 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {FileActions} from '../component/file-actions.js';
|
||||
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js';
|
||||
import ChannelIndicator from '../component/channel-indicator.js';
|
||||
|
||||
let FilePrice = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
|
||||
lbry.getCostInfoForName(this.props.uri, ({cost, includesData}) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
});
|
||||
}
|
||||
}, () => {
|
||||
// If we get an error looking up cost information, do nothing
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.cost === null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="file-tile__cost">
|
||||
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
import {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,
|
||||
_metadata: null,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
claimInfo: React.PropTypes.object,
|
||||
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
|
||||
|
@ -70,20 +26,15 @@ export let FileTileStream = React.createClass({
|
|||
return {
|
||||
showNsfwHelp: false,
|
||||
isHidden: false,
|
||||
available: null,
|
||||
}
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
obscureNsfw: !lbry.getClientSetting('showNsfw'),
|
||||
hidePrice: false
|
||||
hidePrice: false,
|
||||
hasSignature: false,
|
||||
}
|
||||
},
|
||||
componentWillMount: function() {
|
||||
const {value: {stream: {metadata, source: {contentType}}}} = this.props.claimInfo;
|
||||
this._metadata = metadata;
|
||||
this._contentType = contentType;
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
if (this.props.hideOnRemove) {
|
||||
|
@ -103,7 +54,7 @@ export let FileTileStream = React.createClass({
|
|||
}
|
||||
},
|
||||
handleMouseOver: function() {
|
||||
if (this.props.obscureNsfw && this.props.metadata && this._metadata.nsfw) {
|
||||
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
|
||||
this.setState({
|
||||
showNsfwHelp: true,
|
||||
});
|
||||
|
@ -121,39 +72,37 @@ export let FileTileStream = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const metadata = this._metadata;
|
||||
const isConfirmed = typeof metadata == 'object';
|
||||
const title = isConfirmed ? metadata.title : ('lbry://' + this.props.uri);
|
||||
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}>
|
||||
<div className={"row-fluid card-content file-tile__row"}>
|
||||
<div className="span3">
|
||||
<a href={'?show=' + this.props.uri}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a>
|
||||
</div>
|
||||
<div className="span9">
|
||||
{ !this.props.hidePrice
|
||||
? <FilePrice uri={this.props.uri} />
|
||||
: null}
|
||||
<div className="meta"><a href={'?show=' + this.props.uri}>{'lbry://' + this.props.uri}</a></div>
|
||||
<h3 className="file-tile__title">
|
||||
<a href={'?show=' + this.props.uri}>
|
||||
<TruncatedText lines={1}>
|
||||
{title}
|
||||
<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>
|
||||
</a>
|
||||
</h3>
|
||||
<ChannelIndicator uri={this.props.uri} claimInfo={this.props.claimInfo} />
|
||||
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this._contentType} />
|
||||
<p className="file-tile__description">
|
||||
<TruncatedText lines={3}>
|
||||
{isConfirmed
|
||||
? metadata.description
|
||||
: <span className="empty">This file is pending confirmation.</span>}
|
||||
</TruncatedText>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{this.state.showNsfwHelp
|
||||
? <div className='card-overlay'>
|
||||
<p>
|
||||
|
@ -167,12 +116,113 @@ export let FileTileStream = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
export let FileCardStream = React.createClass({
|
||||
_fileInfoSubscribeId: null,
|
||||
_isMounted: null,
|
||||
_metadata: null,
|
||||
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
claimInfo: React.PropTypes.object,
|
||||
outpoint: React.PropTypes.string,
|
||||
hideOnRemove: React.PropTypes.bool,
|
||||
hidePrice: React.PropTypes.bool,
|
||||
obscureNsfw: React.PropTypes.bool
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
showNsfwHelp: false,
|
||||
isHidden: false,
|
||||
}
|
||||
},
|
||||
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,
|
||||
available: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -181,30 +231,54 @@ export let FileTile = React.createClass({
|
|||
claimInfo: null
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
|
||||
lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => {
|
||||
const {value: {stream: {metadata}}, txid, nout} = claimInfo;
|
||||
if (this._isMounted && claimInfo.value.stream.metadata) {
|
||||
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({
|
||||
outpoint: txid + ':' + nout,
|
||||
claimInfo: claimInfo,
|
||||
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 || !this.state.outpoint) {
|
||||
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;
|
||||
}
|
||||
|
||||
return <FileTileStream outpoint={this.state.outpoint} claimInfo={this.state.claimInfo}
|
||||
{... this.props} uri={this.props.uri}/>;
|
||||
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} />;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
import React from 'react';
|
||||
import {Icon} from './common.js';
|
||||
|
||||
var requiredFieldWarningStyle = {
|
||||
color: '#cc0000',
|
||||
transition: 'opacity 400ms ease-in',
|
||||
};
|
||||
var formFieldCounter = 0,
|
||||
formFieldNestedLabelTypes = ['radio', 'checkbox'];
|
||||
|
||||
var FormField = React.createClass({
|
||||
function formFieldId() {
|
||||
return "form-field-" + (++formFieldCounter);
|
||||
}
|
||||
|
||||
export let FormField = React.createClass({
|
||||
_fieldRequiredText: 'This field is required',
|
||||
_type: null,
|
||||
_element: null,
|
||||
|
||||
propTypes: {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
hidden: React.PropTypes.bool,
|
||||
prefix: React.PropTypes.string,
|
||||
postfix: React.PropTypes.string,
|
||||
hasError: React.PropTypes.bool
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
adviceState: 'hidden',
|
||||
adviceText: null,
|
||||
isError: null,
|
||||
errorMessage: null,
|
||||
}
|
||||
},
|
||||
componentWillMount: function() {
|
||||
if (['text', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
|
||||
if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
|
||||
this._element = 'input';
|
||||
this._type = this.props.type;
|
||||
} else if (this.props.type == 'text-number') {
|
||||
|
@ -33,25 +37,11 @@ var FormField = React.createClass({
|
|||
this._element = this.props.type;
|
||||
}
|
||||
},
|
||||
showAdvice: function(text) {
|
||||
showError: function(text) {
|
||||
this.setState({
|
||||
adviceState: 'shown',
|
||||
adviceText: text,
|
||||
isError: true,
|
||||
errorMessage: text,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
adviceState: 'fading',
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
adviceState: 'hidden',
|
||||
});
|
||||
}, 450);
|
||||
}, 5000);
|
||||
},
|
||||
warnRequired: function() {
|
||||
this.showAdvice(this._fieldRequiredText);
|
||||
},
|
||||
focus: function() {
|
||||
this.refs.field.focus();
|
||||
|
@ -60,7 +50,8 @@ var FormField = React.createClass({
|
|||
if (this.props.type == 'checkbox') {
|
||||
return this.refs.field.checked;
|
||||
} else if (this.props.type == 'file') {
|
||||
return this.refs.field.files[0].path;
|
||||
return this.refs.field.files.length && this.refs.field.files[0].path ?
|
||||
this.refs.field.files[0].path : null;
|
||||
} else {
|
||||
return this.refs.field.value;
|
||||
}
|
||||
|
@ -70,45 +61,94 @@ var FormField = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
// Pass all unhandled props to the field element
|
||||
const otherProps = Object.assign({}, this.props);
|
||||
const otherProps = Object.assign({}, this.props),
|
||||
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
|
||||
elementId = this.props.id ? this.props.id : formFieldId(),
|
||||
renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
|
||||
|
||||
delete otherProps.type;
|
||||
delete otherProps.hidden;
|
||||
delete otherProps.label;
|
||||
delete otherProps.hasError;
|
||||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
|
||||
return (
|
||||
!this.props.hidden
|
||||
? <div className="form-field-container">
|
||||
<this._element type={this._type} className="form-field" name={this.props.name} ref="field" placeholder={this.props.placeholder}
|
||||
className={'form-field--' + this.props.type + ' ' + (this.props.className || '')}
|
||||
{...otherProps}>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
<FormFieldAdvice field={this.refs.field} state={this.state.adviceState}>{this.state.adviceText}</FormFieldAdvice>
|
||||
</div>
|
||||
: null
|
||||
);
|
||||
const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
|
||||
className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
|
||||
{...otherProps}>
|
||||
{this.props.children}
|
||||
</this._element>;
|
||||
|
||||
return <div className="form-field">
|
||||
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
|
||||
{ renderElementInsideLabel ?
|
||||
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
|
||||
{element}
|
||||
{this.props.label}
|
||||
</label> :
|
||||
element }
|
||||
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
|
||||
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
||||
</div>
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
var FormFieldAdvice = React.createClass({
|
||||
export let FormRow = React.createClass({
|
||||
_fieldRequiredText: 'This field is required',
|
||||
propTypes: {
|
||||
state: React.PropTypes.string.isRequired,
|
||||
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
|
||||
// helper: React.PropTypes.html,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
isError: false,
|
||||
errorMessage: null,
|
||||
}
|
||||
},
|
||||
showError: function(text) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorMessage: text,
|
||||
});
|
||||
},
|
||||
showRequiredError: function() {
|
||||
this.showError(this._fieldRequiredText);
|
||||
},
|
||||
clearError: function(text) {
|
||||
this.setState({
|
||||
isError: false,
|
||||
errorMessage: ''
|
||||
});
|
||||
},
|
||||
getValue: function() {
|
||||
return this.refs.field.getValue();
|
||||
},
|
||||
getSelectedElement: function() {
|
||||
return this.refs.field.getSelectedElement();
|
||||
},
|
||||
focus: function() {
|
||||
this.refs.field.focus();
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
this.props.state != 'hidden'
|
||||
? <div className="form-field-advice-container">
|
||||
<div className={'form-field-advice' + (this.props.state == 'fading' ? ' form-field-advice--fading' : '')}>
|
||||
<Icon icon="icon-caret-up" className="form-field-advice__arrow" />
|
||||
<div className="form-field-advice__content-container">
|
||||
<span className="form-field-advice__content">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: null
|
||||
);
|
||||
}
|
||||
});
|
||||
const fieldProps = Object.assign({}, this.props),
|
||||
elementId = formFieldId(),
|
||||
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
|
||||
|
||||
export default FormField;
|
||||
if (!renderLabelInFormField) {
|
||||
delete fieldProps.label;
|
||||
}
|
||||
delete fieldProps.helper;
|
||||
|
||||
return <div className="form-row">
|
||||
{ this.props.label && !renderLabelInFormField ?
|
||||
<div className={"form-row__label-row " + (this.props.labelPrefix ? "form-row__label-row--prefix" : "") }>
|
||||
<label htmlFor={elementId} className={"form-field__label " + (this.state.isError ? 'form-field__label--error' : '')}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div> : '' }
|
||||
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
|
||||
{ !this.state.isError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
|
||||
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,74 +1,198 @@
|
|||
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 {
|
||||
title: "LBRY",
|
||||
isScrolled: false
|
||||
balance: 0
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
new MutationObserver(function(mutations) {
|
||||
this.setState({ title: mutations[0].target.textContent });
|
||||
}.bind(this)).observe(
|
||||
document.querySelector('title'),
|
||||
{ subtree: true, characterData: true, childList: true }
|
||||
);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('scroll', this.handleScroll);
|
||||
if (this.userTypingTimer)
|
||||
{
|
||||
clearTimeout(this.userTypingTimer);
|
||||
}
|
||||
},
|
||||
handleScroll: function() {
|
||||
this.setState({
|
||||
isScrolled: document.body.scrollTop > 0
|
||||
this._isMounted = true;
|
||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({balance: balance});
|
||||
}
|
||||
});
|
||||
},
|
||||
onQueryChange: function(event) {
|
||||
|
||||
if (this.userTypingTimer)
|
||||
{
|
||||
clearTimeout(this.userTypingTimer);
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
if (this._balanceSubscribeId) {
|
||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||
}
|
||||
|
||||
//@TODO: Switch to React.js timing
|
||||
var searchTerm = event.target.value;
|
||||
this.userTypingTimer = setTimeout(() => {
|
||||
this.props.onSearch(searchTerm);
|
||||
}, 800); // 800ms delay, tweak for faster/slower
|
||||
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }>
|
||||
<div className="header-top-bar">
|
||||
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
|
||||
<h1>{ this.state.title }</h1>
|
||||
<div className="header-search">
|
||||
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
|
||||
placeholder="Find movies, music, games, and more"/>
|
||||
</div>
|
||||
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>
|
||||
{
|
||||
this.props.links ?
|
||||
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
|
||||
''
|
||||
}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SubHeader = React.createClass({
|
||||
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() {
|
||||
var links = [],
|
||||
let links = [],
|
||||
viewingUrl = '?' + this.props.viewingPage;
|
||||
|
||||
for (let link of Object.keys(this.props.links)) {
|
||||
|
@ -79,7 +203,7 @@ var SubHeader = React.createClass({
|
|||
);
|
||||
}
|
||||
return (
|
||||
<nav className="sub-header">
|
||||
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
|
||||
{links}
|
||||
</nav>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
import {Icon} from './common.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import rewards from '../rewards.js';
|
||||
|
||||
export let Link = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -39,7 +41,7 @@ export let Link = React.createClass({
|
|||
content = (
|
||||
<span {... 'button' in this.props ? {className: 'button__content'} : {}}>
|
||||
{'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
|
||||
{<span className="link-label">{this.props.label}</span>}
|
||||
{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>
|
||||
);
|
||||
|
@ -52,4 +54,80 @@ export let Link = React.createClass({
|
|||
</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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -9,9 +9,6 @@ var LoadScreen = React.createClass({
|
|||
details: React.PropTypes.string,
|
||||
isWarning: React.PropTypes.bool,
|
||||
},
|
||||
handleCancelClick: function() {
|
||||
history.back();
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
isWarning: false,
|
||||
|
@ -34,9 +31,6 @@ var LoadScreen = React.createClass({
|
|||
<BusyMessage message={this.props.message} />
|
||||
</h3>
|
||||
{this.props.isWarning ? <Icon icon="icon-warning" /> : null} <span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span>
|
||||
{window.history.length > 1
|
||||
? <div><Link label="Cancel" onClick={this.handleCancelClick} className='load-screen__cancel-link button-text' /></div>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
18
ui/js/component/modal-page.js
Normal file
18
ui/js/component/modal-page.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
export const ModalPage = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
||||
className={(this.props.className || '') + ' modal-page'}
|
||||
overlayClassName="modal-overlay">
|
||||
<div className="modal-page__content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ModalPage;
|
|
@ -6,6 +6,7 @@ import {Link} from './link.js';
|
|||
export const Modal = React.createClass({
|
||||
propTypes: {
|
||||
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
|
||||
overlay: React.PropTypes.bool,
|
||||
onConfirmed: React.PropTypes.func,
|
||||
onAborted: React.PropTypes.func,
|
||||
confirmButtonLabel: React.PropTypes.string,
|
||||
|
@ -16,6 +17,7 @@ export const Modal = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
type: 'alert',
|
||||
overlay: true,
|
||||
confirmButtonLabel: 'OK',
|
||||
abortButtonLabel: 'Cancel',
|
||||
confirmButtonDisabled: false,
|
||||
|
@ -26,7 +28,7 @@ export const Modal = React.createClass({
|
|||
return (
|
||||
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
|
||||
className={(this.props.className || '') + ' modal'}
|
||||
overlayClassName={(this.props.overlayClassName || '') + ' modal-overlay'}>
|
||||
overlayClassName={![null, undefined, ""].includes(this.props.overlayClassName) ? this.props.overlayClassName : 'modal-overlay'}>
|
||||
<div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
|
21
ui/js/component/notice.js
Normal file
21
ui/js/component/notice.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Notice = React.createClass({
|
||||
propTypes: {
|
||||
isError: React.PropTypes.bool,
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
isError: false,
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
|
||||
{this.props.children}
|
||||
</section>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default Notice;
|
57
ui/js/component/snack-bar.js
Normal file
57
ui/js/component/snack-bar.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
|
||||
export const SnackBar = React.createClass({
|
||||
|
||||
_displayTime: 5, // in seconds
|
||||
|
||||
_hideTimeout: null,
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
snacks: []
|
||||
}
|
||||
},
|
||||
handleSnackReceived: function(event) {
|
||||
// if (this._hideTimeout) {
|
||||
// clearTimeout(this._hideTimeout);
|
||||
// }
|
||||
|
||||
let snacks = this.state.snacks;
|
||||
snacks.push(event.detail);
|
||||
this.setState({ snacks: snacks});
|
||||
},
|
||||
componentWillMount: function() {
|
||||
document.addEventListener('globalNotice', this.handleSnackReceived);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('globalNotice', this.handleSnackReceived);
|
||||
},
|
||||
render: function() {
|
||||
if (!this.state.snacks.length) {
|
||||
this._hideTimeout = null; //should be unmounting anyway, but be safe?
|
||||
return null;
|
||||
}
|
||||
|
||||
let snack = this.state.snacks[0];
|
||||
|
||||
if (this._hideTimeout === null) {
|
||||
this._hideTimeout = setTimeout(() => {
|
||||
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;
|
|
@ -13,11 +13,12 @@ var SplashScreen = React.createClass({
|
|||
isLagging: false,
|
||||
}
|
||||
},
|
||||
updateStatus: function(was_lagging=false) {
|
||||
lbry.getDaemonStatus(this._updateStatusCallback);
|
||||
updateStatus: function() {
|
||||
lbry.status().then(this._updateStatusCallback);
|
||||
},
|
||||
_updateStatusCallback: function(status) {
|
||||
if (status.code == 'started') {
|
||||
const startupStatus = status.startup_status
|
||||
if (startupStatus.code == 'started') {
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
|
@ -27,27 +28,35 @@ var SplashScreen = React.createClass({
|
|||
isLagging: false
|
||||
});
|
||||
|
||||
lbry.resolveName('one', () => {
|
||||
window.sessionStorage.setItem('loaded', 'y')
|
||||
lbry.resolve({uri: 'lbry://one'}).then(() => {
|
||||
window.sessionStorage.setItem('loaded', 'y')
|
||||
this.props.onLoadDone();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
details: status.message + (status.is_lagging ? '' : '...'),
|
||||
isLagging: status.is_lagging,
|
||||
details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.updateStatus(status.is_lagging);
|
||||
this.updateStatus();
|
||||
}, 500);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
lbry.connect((connected) => {
|
||||
this.updateStatus();
|
||||
});
|
||||
lbry.connect().then((isConnected) => {
|
||||
if (isConnected) {
|
||||
this.updateStatus();
|
||||
} else {
|
||||
this.setState({
|
||||
isLagging: true,
|
||||
message: "Failed to connect to LBRY",
|
||||
details: "LBRY was unable to start and connect properly."
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
render: function() {
|
||||
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />;
|
||||
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
|
||||
}
|
||||
});
|
||||
|
||||
|
|
480
ui/js/lbry.js
480
ui/js/lbry.js
|
@ -1,32 +1,46 @@
|
|||
import lbryio from './lbryio.js';
|
||||
import lighthouse from './lighthouse.js';
|
||||
import jsonrpc from './jsonrpc.js';
|
||||
import {getLocal, setLocal} from './utils.js';
|
||||
import lbryuri from './lbryuri.js';
|
||||
import {getLocal, getSession, setSession, setLocal} from './utils.js';
|
||||
|
||||
const {remote} = require('electron');
|
||||
const {remote, ipcRenderer} = require('electron');
|
||||
const menu = remote.require('./menu/main-menu');
|
||||
|
||||
/**
|
||||
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
|
||||
* needed to make a dummy claim or file info object.
|
||||
*/
|
||||
function savePendingPublish(name) {
|
||||
function savePendingPublish({name, channel_name}) {
|
||||
let uri;
|
||||
if (channel_name) {
|
||||
uri = lbryuri.build({name: channel_name, path: name}, false);
|
||||
} else {
|
||||
uri = lbryuri.build({name: name}, false);
|
||||
}
|
||||
const pendingPublishes = getLocal('pendingPublishes') || [];
|
||||
const newPendingPublish = {
|
||||
claim_id: 'pending_claim_' + name,
|
||||
txid: 'pending_' + name,
|
||||
name, channel_name,
|
||||
claim_id: 'pending_claim_' + uri,
|
||||
txid: 'pending_' + uri,
|
||||
nout: 0,
|
||||
outpoint: 'pending_' + name + ':0',
|
||||
name: name,
|
||||
outpoint: 'pending_' + uri + ':0',
|
||||
time: Date.now(),
|
||||
};
|
||||
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
|
||||
return newPendingPublish;
|
||||
}
|
||||
|
||||
function removePendingPublish({name, outpoint}) {
|
||||
setLocal('pendingPublishes', getPendingPublishes().filter(
|
||||
(pub) => pub.name != name && pub.outpoint != outpoint
|
||||
));
|
||||
/**
|
||||
* If there is a pending publish with the given name or outpoint, remove it.
|
||||
* A channel name may also be provided along with name.
|
||||
*/
|
||||
function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
|
||||
function pubMatches(pub) {
|
||||
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
|
||||
}
|
||||
|
||||
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,61 +49,30 @@ function removePendingPublish({name, outpoint}) {
|
|||
*/
|
||||
function getPendingPublishes() {
|
||||
const pendingPublishes = getLocal('pendingPublishes') || [];
|
||||
|
||||
const newPendingPublishes = [];
|
||||
for (let pendingPublish of pendingPublishes) {
|
||||
if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) {
|
||||
newPendingPublishes.push(pendingPublish);
|
||||
}
|
||||
}
|
||||
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
|
||||
setLocal('pendingPublishes', newPendingPublishes);
|
||||
return newPendingPublishes
|
||||
return newPendingPublishes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found
|
||||
* but it has timed out), returns null.
|
||||
* Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
|
||||
* provided along withe the name. If no pending publish is found, returns null.
|
||||
*/
|
||||
function getPendingPublish({name, outpoint}) {
|
||||
function getPendingPublish({name, channel_name, outpoint}) {
|
||||
const pendingPublishes = getPendingPublishes();
|
||||
const pendingPublishIndex = pendingPublishes.findIndex(
|
||||
({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint
|
||||
);
|
||||
const pendingPublish = pendingPublishes[pendingPublishIndex];
|
||||
|
||||
if (pendingPublishIndex == -1) {
|
||||
return null;
|
||||
} else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) {
|
||||
// Pending publish timed out, so remove it from the stored list and don't match
|
||||
|
||||
const newPendingPublishes = pendingPublishes.slice();
|
||||
newPendingPublishes.splice(pendingPublishIndex, 1);
|
||||
setLocal('pendingPublishes', newPendingPublishes);
|
||||
return null;
|
||||
} else {
|
||||
return pendingPublish;
|
||||
}
|
||||
return pendingPublishes.find(
|
||||
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
|
||||
) || null;
|
||||
}
|
||||
|
||||
function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) {
|
||||
return {
|
||||
name: name,
|
||||
outpoint: outpoint,
|
||||
claim_id: claim_id,
|
||||
txid: txid,
|
||||
nout: nout,
|
||||
};
|
||||
function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) {
|
||||
return {name, outpoint, claim_id, txid, nout, channel_name};
|
||||
}
|
||||
|
||||
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
|
||||
return {
|
||||
name: name,
|
||||
outpoint: outpoint,
|
||||
claim_id: claim_id,
|
||||
metadata: "Attempting publication",
|
||||
};
|
||||
return {name, outpoint, claim_id, metadata: null};
|
||||
}
|
||||
|
||||
window.pptdfi = pendingPublishToDummyFileInfo;
|
||||
|
||||
let lbry = {
|
||||
isConnected: false,
|
||||
|
@ -112,33 +95,55 @@ let lbry = {
|
|||
};
|
||||
|
||||
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
|
||||
jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
|
||||
jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
|
||||
}
|
||||
|
||||
//core
|
||||
lbry._connectPromise = null;
|
||||
lbry.connect = function() {
|
||||
if (lbry._connectPromise === null) {
|
||||
|
||||
lbry._connectPromise = new Promise((resolve, reject) => {
|
||||
|
||||
// Check every half second to see if the daemon is accepting connections
|
||||
function checkDaemonStarted(tryNum = 0) {
|
||||
lbry.isDaemonAcceptingConnections(function (runningStatus) {
|
||||
if (runningStatus) {
|
||||
resolve(true);
|
||||
}
|
||||
else {
|
||||
if (tryNum <= 600) { // Move # of tries into constant or config option
|
||||
setTimeout(function () {
|
||||
checkDaemonStarted(tryNum + 1);
|
||||
}, tryNum < 100 ? 200 : 1000);
|
||||
}
|
||||
else {
|
||||
reject(new Error("Unable to connect to LBRY"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkDaemonStarted();
|
||||
});
|
||||
}
|
||||
|
||||
return lbry._connectPromise;
|
||||
}
|
||||
|
||||
|
||||
//core
|
||||
lbry.connect = function(callback)
|
||||
{
|
||||
// Check every half second to see if the daemon is accepting connections
|
||||
// Once this returns True, can call getDaemonStatus to see where
|
||||
// we are in the startup process
|
||||
function checkDaemonStarted(tryNum=0) {
|
||||
lbry.isDaemonAcceptingConnections(function (runningStatus) {
|
||||
if (runningStatus) {
|
||||
lbry.isConnected = true;
|
||||
callback(true);
|
||||
} else {
|
||||
if (tryNum <= 600) { // Move # of tries into constant or config option
|
||||
setTimeout(function () {
|
||||
checkDaemonStarted(tryNum + 1);
|
||||
}, 500);
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
//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";
|
||||
}
|
||||
checkDaemonStarted();
|
||||
}
|
||||
|
||||
lbry.isDaemonAcceptingConnections = function (callback) {
|
||||
|
@ -146,10 +151,6 @@ lbry.isDaemonAcceptingConnections = function (callback) {
|
|||
lbry.call('status', {}, () => callback(true), null, () => callback(false))
|
||||
};
|
||||
|
||||
lbry.getDaemonStatus = function (callback) {
|
||||
lbry.call('daemon_status', {}, callback);
|
||||
};
|
||||
|
||||
lbry.checkFirstRun = function(callback) {
|
||||
lbry.call('is_first_run', {}, callback);
|
||||
}
|
||||
|
@ -189,23 +190,6 @@ lbry.sendToAddress = function(amount, address, callback, errorCallback) {
|
|||
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
|
||||
}
|
||||
|
||||
lbry.resolveName = function(name, callback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('resolve_name', { 'name': name }, callback, () => {
|
||||
// For now, assume any error means the name was not resolved
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
lbry.getStream = function(name, callback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('get', { 'name': name }, callback);
|
||||
};
|
||||
|
||||
lbry.getClaimInfo = function(name, callback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
|
@ -219,23 +203,6 @@ lbry.getMyClaim = function(name, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
lbry.getKeyFee = function(name, callback, errorCallback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('stream_cost_estimate', { name: name }, callback, errorCallback);
|
||||
}
|
||||
|
||||
lbry.getTotalCost = function(name, size, callback, errorCallback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('stream_cost_estimate', {
|
||||
name: name,
|
||||
size: size,
|
||||
}, callback, errorCallback);
|
||||
}
|
||||
|
||||
lbry.getPeersForBlobHash = function(blobHash, callback) {
|
||||
let timedOut = false;
|
||||
const timeout = setTimeout(() => {
|
||||
|
@ -251,88 +218,87 @@ lbry.getPeersForBlobHash = function(blobHash, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
lbry.getStreamAvailability = function(name, callback, errorCallback) {
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
lbry.call('get_availability', {name: name}, callback, errorCallback);
|
||||
}
|
||||
/**
|
||||
* Takes a LBRY URI; will first try and calculate a total cost using
|
||||
* Lighthouse. If Lighthouse can't be reached, it just retrives the
|
||||
* key fee.
|
||||
*
|
||||
* Returns an object with members:
|
||||
* - cost: Number; the calculated cost of the name
|
||||
* - includes_data: Boolean; indicates whether or not the data fee info
|
||||
* from Lighthouse is included.
|
||||
*/
|
||||
lbry.costPromiseCache = {}
|
||||
lbry.getCostInfo = function(uri) {
|
||||
if (lbry.costPromiseCache[uri] === undefined) {
|
||||
lbry.costPromiseCache[uri] = new Promise((resolve, reject) => {
|
||||
const COST_INFO_CACHE_KEY = 'cost_info_cache';
|
||||
let costInfoCache = getSession(COST_INFO_CACHE_KEY, {})
|
||||
|
||||
lbry.getCostInfoForName = function(name, callback, errorCallback) {
|
||||
/**
|
||||
* Takes a LBRY name; will first try and calculate a total cost using
|
||||
* Lighthouse. If Lighthouse can't be reached, it just retrives the
|
||||
* key fee.
|
||||
*
|
||||
* Returns an object with members:
|
||||
* - cost: Number; the calculated cost of the name
|
||||
* - includes_data: Boolean; indicates whether or not the data fee info
|
||||
* from Lighthouse is included.
|
||||
*/
|
||||
if (!name) {
|
||||
throw new Error(`Name required.`);
|
||||
}
|
||||
|
||||
function getCostWithData(name, size, callback, errorCallback) {
|
||||
lbry.getTotalCost(name, size, (cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: true,
|
||||
});
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
function getCostNoData(name, callback, errorCallback) {
|
||||
lbry.getKeyFee(name, (cost) => {
|
||||
callback({
|
||||
cost: cost,
|
||||
includesData: false,
|
||||
});
|
||||
}, errorCallback);
|
||||
}
|
||||
|
||||
lighthouse.get_size_for_name(name).then((size) => {
|
||||
getCostWithData(name, size, callback, errorCallback);
|
||||
}, () => {
|
||||
getCostNoData(name, callback, errorCallback);
|
||||
});
|
||||
}
|
||||
|
||||
lbry.getFeaturedDiscoverNames = function(callback) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open('GET', 'https://api.lbry.io/discover/list', true);
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
var responseData = JSON.parse(xhr.responseText);
|
||||
if (responseData.data) //new signature, once api.lbry.io is updated
|
||||
{
|
||||
resolve(responseData.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolve(responseData);
|
||||
}
|
||||
} else {
|
||||
reject(Error('Failed to fetch featured names.'));
|
||||
function cacheAndResolve(cost, includesData) {
|
||||
costInfoCache[uri] = {cost, includesData};
|
||||
setSession(COST_INFO_CACHE_KEY, costInfoCache);
|
||||
resolve({cost, includesData});
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
if (!uri) {
|
||||
return reject(new Error(`URI required.`));
|
||||
}
|
||||
|
||||
if (costInfoCache[uri] && costInfoCache[uri].cost) {
|
||||
return resolve(costInfoCache[uri])
|
||||
}
|
||||
|
||||
function getCost(uri, size) {
|
||||
lbry.stream_cost_estimate({uri, ... size !== null ? {size} : {}}).then((cost) => {
|
||||
cacheAndResolve(cost, size !== null);
|
||||
}, 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 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) => {
|
||||
if (size) {
|
||||
getCost(name, size);
|
||||
}
|
||||
else {
|
||||
getCost(name, null);
|
||||
}
|
||||
}, () => {
|
||||
getCost(name, null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
return lbry.costPromiseCache[uri];
|
||||
}
|
||||
|
||||
lbry.getMyClaims = function(callback) {
|
||||
lbry.call('get_name_claims', {}, callback);
|
||||
}
|
||||
|
||||
lbry.startFile = function(name, callback) {
|
||||
lbry.call('start_lbry_file', { name: name }, callback);
|
||||
}
|
||||
|
||||
lbry.stopFile = function(name, callback) {
|
||||
lbry.call('stop_lbry_file', { name: name }, callback);
|
||||
}
|
||||
|
||||
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
|
||||
this._removedFiles.push(outpoint);
|
||||
this._updateFileInfoSubscribers(outpoint);
|
||||
|
@ -393,12 +359,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
|
|||
returnedPending = true;
|
||||
|
||||
if (publishedCallback) {
|
||||
savePendingPublish(params.name);
|
||||
savePendingPublish({name: params.name, channel_name: params.channel_name});
|
||||
publishedCallback(true);
|
||||
}
|
||||
|
||||
if (fileListedCallback) {
|
||||
savePendingPublish(params.name);
|
||||
const {name, channel_name} = params;
|
||||
savePendingPublish({name: params.name, channel_name: params.channel_name});
|
||||
fileListedCallback(true);
|
||||
}
|
||||
}, 2000);
|
||||
|
@ -408,44 +375,6 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
|
|||
//});
|
||||
}
|
||||
|
||||
lbry.getVersionInfo = function(callback) {
|
||||
lbry.call('version', {}, callback);
|
||||
};
|
||||
|
||||
lbry.checkNewVersionAvailable = function(callback) {
|
||||
lbry.call('version', {}, function(versionInfo) {
|
||||
var ver = versionInfo.lbrynet_version.split('.');
|
||||
|
||||
var maj = parseInt(ver[0]),
|
||||
min = parseInt(ver[1]),
|
||||
patch = parseInt(ver[2]);
|
||||
|
||||
var remoteVer = versionInfo.remote_lbrynet.split('.');
|
||||
var remoteMaj = parseInt(remoteVer[0]),
|
||||
remoteMin = parseInt(remoteVer[1]),
|
||||
remotePatch = parseInt(remoteVer[2]);
|
||||
|
||||
if (maj < remoteMaj) {
|
||||
var newVersionAvailable = true;
|
||||
} else if (maj == remoteMaj) {
|
||||
if (min < remoteMin) {
|
||||
var newVersionAvailable = true;
|
||||
} else if (min == remoteMin) {
|
||||
var newVersionAvailable = (patch < remotePatch);
|
||||
} else {
|
||||
var newVersionAvailable = false;
|
||||
}
|
||||
} else {
|
||||
var newVersionAvailable = false;
|
||||
}
|
||||
callback(newVersionAvailable);
|
||||
}, function(err) {
|
||||
if (err.fault == 'NoSuchFunction') {
|
||||
// Really old daemon that can't report a version
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lbry.getClientSettings = function() {
|
||||
var outSettings = {};
|
||||
|
@ -458,6 +387,10 @@ lbry.getClientSettings = function() {
|
|||
|
||||
lbry.getClientSetting = function(setting) {
|
||||
var localStorageVal = localStorage.getItem('setting_' + setting);
|
||||
if (setting == 'showDeveloperMenu')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
|
||||
}
|
||||
|
||||
|
@ -550,7 +483,7 @@ lbry.stop = function(callback) {
|
|||
lbry.fileInfo = {};
|
||||
lbry._subscribeIdCount = 0;
|
||||
lbry._fileInfoSubscribeCallbacks = {};
|
||||
lbry._fileInfoSubscribeInterval = 5000;
|
||||
lbry._fileInfoSubscribeInterval = 500000;
|
||||
lbry._balanceSubscribeCallbacks = {};
|
||||
lbry._balanceSubscribeInterval = 5000;
|
||||
lbry._removedFiles = [];
|
||||
|
@ -560,8 +493,9 @@ 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) {
|
||||
|
@ -612,6 +546,7 @@ lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) {
|
|||
delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId];
|
||||
}
|
||||
|
||||
lbry._balanceUpdateInterval = null;
|
||||
lbry._updateBalanceSubscribers = function() {
|
||||
lbry.get_balance().then(function(balance) {
|
||||
for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) {
|
||||
|
@ -619,8 +554,8 @@ lbry._updateBalanceSubscribers = function() {
|
|||
}
|
||||
});
|
||||
|
||||
if (Object.keys(lbry._balanceSubscribeCallbacks).length) {
|
||||
setTimeout(() => {
|
||||
if (!lbry._balanceUpdateInterval && Object.keys(lbry._balanceSubscribeCallbacks).length) {
|
||||
lbry._balanceUpdateInterval = setInterval(() => {
|
||||
lbry._updateBalanceSubscribers();
|
||||
}, lbry._balanceSubscribeInterval);
|
||||
}
|
||||
|
@ -635,6 +570,9 @@ lbry.balanceSubscribe = function(callback) {
|
|||
|
||||
lbry.balanceUnsubscribe = function(subscribeId) {
|
||||
delete lbry._balanceSubscribeCallbacks[subscribeId];
|
||||
if (lbry._balanceUpdateInterval && !Object.keys(lbry._balanceSubscribeCallbacks).length) {
|
||||
clearInterval(lbry._balanceUpdateInterval)
|
||||
}
|
||||
}
|
||||
|
||||
lbry.showMenuIfNeeded = function() {
|
||||
|
@ -646,6 +584,14 @@ lbry.showMenuIfNeeded = function() {
|
|||
sessionStorage.setItem('menuShown', chosenMenu);
|
||||
};
|
||||
|
||||
lbry.getVersionInfo = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) });
|
||||
ipcRenderer.send('version-info-requested');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs,
|
||||
* these are designed to be transparent wrappers around the corresponding API methods.
|
||||
|
@ -657,14 +603,14 @@ lbry.showMenuIfNeeded = function() {
|
|||
*/
|
||||
lbry.file_list = function(params={}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {name, outpoint} = params;
|
||||
const {name, channel_name, outpoint} = params;
|
||||
|
||||
/**
|
||||
* If we're searching by outpoint, check first to see if there's a matching pending publish.
|
||||
* Pending publishes use their own faux outpoints that are always unique, so we don't need
|
||||
* to check if there's a real file.
|
||||
*/
|
||||
if (outpoint !== undefined) {
|
||||
if (outpoint) {
|
||||
const pendingPublish = getPendingPublish({outpoint});
|
||||
if (pendingPublish) {
|
||||
resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
|
||||
|
@ -673,14 +619,8 @@ lbry.file_list = function(params={}) {
|
|||
}
|
||||
|
||||
lbry.call('file_list', params, (fileInfos) => {
|
||||
// Remove any pending publications that are now listed in the file manager
|
||||
removePendingPublishIfNeeded({name, channel_name, outpoint});
|
||||
|
||||
const pendingPublishes = getPendingPublishes();
|
||||
for (let {name: itemName} of fileInfos) {
|
||||
if (pendingPublishes.find(() => name == itemName)) {
|
||||
removePendingPublish({name: name});
|
||||
}
|
||||
}
|
||||
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
|
||||
resolve([...fileInfos, ...dummyFileInfos]);
|
||||
}, reject, reject);
|
||||
|
@ -690,16 +630,60 @@ lbry.file_list = function(params={}) {
|
|||
lbry.claim_list_mine = function(params={}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry.call('claim_list_mine', params, (claims) => {
|
||||
// Filter out pending publishes when the name is already in the file manager
|
||||
const dummyClaims = getPendingPublishes().filter(
|
||||
(pub) => !claims.find(({name}) => name == pub.name)
|
||||
).map(pendingPublishToDummyClaim);
|
||||
for (let {name, channel_name, txid, nout} of claims) {
|
||||
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
|
||||
}
|
||||
|
||||
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
|
||||
resolve([...claims, ...dummyClaims]);
|
||||
}, reject, reject);
|
||||
}, reject, reject)
|
||||
});
|
||||
}
|
||||
|
||||
lbry.resolve = function(params={}) {
|
||||
const claimCacheKey = 'resolve_claim_cache',
|
||||
claimCache = getSession(claimCacheKey, {})
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!params.uri) {
|
||||
throw "Resolve has hacked cache on top of it that requires a URI"
|
||||
}
|
||||
if (params.uri && claimCache[params.uri] !== undefined) {
|
||||
resolve(claimCache[params.uri]);
|
||||
} else {
|
||||
lbry.call('resolve', params, function(data) {
|
||||
claimCache[params.uri] = data;
|
||||
setSession(claimCacheKey, claimCache)
|
||||
resolve(data)
|
||||
}, reject)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Adds caching.
|
||||
lbry.settings_get = function(params={}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (params.allow_cached) {
|
||||
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, {
|
||||
get: function(target, name) {
|
||||
if (name in target) {
|
||||
|
@ -708,7 +692,7 @@ lbry = new Proxy(lbry, {
|
|||
|
||||
return function(params={}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
jsonrpc.call(lbry.daemonConnectionString, name, [params], resolve, reject, reject);
|
||||
jsonrpc.call(lbry.daemonConnectionString, name, params, resolve, reject, reject);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
159
ui/js/lbryio.js
Normal file
159
ui/js/lbryio.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import {getLocal, getSession, setSession, setLocal} from './utils.js';
|
||||
import lbry from './lbry.js';
|
||||
|
||||
const querystring = require('querystring');
|
||||
|
||||
const lbryio = {
|
||||
_accessToken: getLocal('accessToken'),
|
||||
_authenticationPromise: null,
|
||||
_user : null,
|
||||
enabled: false
|
||||
};
|
||||
|
||||
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
|
||||
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
||||
|
||||
lbryio.getExchangeRates = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cached = getSession('exchangeRateCache');
|
||||
if (!cached || Date.now() - cached.time > EXCHANGE_RATE_TIMEOUT) {
|
||||
lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => {
|
||||
const rates = {lbc_usd, lbc_btc, btc_usd};
|
||||
setSession('exchangeRateCache', {
|
||||
rates: rates,
|
||||
time: Date.now(),
|
||||
});
|
||||
resolve(rates);
|
||||
});
|
||||
} else {
|
||||
resolve(cached.rates);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!lbryio.enabled && !evenIfDisabled && (resource != 'discover' || action != 'list')) {
|
||||
reject(new Error("LBRY internal API is disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest;
|
||||
|
||||
xhr.addEventListener('error', function (event) {
|
||||
reject(new Error("Something went wrong making an internal API call."));
|
||||
});
|
||||
|
||||
|
||||
xhr.addEventListener('timeout', function() {
|
||||
reject(new Error('XMLHttpRequest connection timed out'));
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function() {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (!response.success) {
|
||||
if (reject) {
|
||||
let error = new Error(response.error);
|
||||
error.xhr = xhr;
|
||||
reject(error);
|
||||
} else {
|
||||
document.dispatchEvent(new CustomEvent('unhandledError', {
|
||||
detail: {
|
||||
connectionString: connectionString,
|
||||
method: action,
|
||||
params: params,
|
||||
message: response.error.message,
|
||||
... response.error.data ? {data: response.error.data} : {},
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
resolve(response.data);
|
||||
}
|
||||
});
|
||||
|
||||
// For social media auth:
|
||||
//const accessToken = localStorage.getItem('accessToken');
|
||||
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
|
||||
|
||||
// Temp app ID based auth:
|
||||
const fullParams = {app_id: lbryio.getAccessToken(), ...params};
|
||||
|
||||
if (method == 'get') {
|
||||
xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true);
|
||||
xhr.send();
|
||||
} else if (method == 'post') {
|
||||
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
xhr.send(querystring.stringify(fullParams));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
lbryio.getAccessToken = () => {
|
||||
return getLocal('accessToken');
|
||||
}
|
||||
|
||||
lbryio.setAccessToken = (token) => {
|
||||
setLocal('accessToken', token)
|
||||
}
|
||||
|
||||
lbryio.authenticate = function() {
|
||||
if (!lbryio.enabled) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
ID: 1,
|
||||
HasVerifiedEmail: true
|
||||
})
|
||||
})
|
||||
}
|
||||
if (lbryio._authenticationPromise === null) {
|
||||
lbryio._authenticationPromise = new Promise((resolve, reject) => {
|
||||
lbry.status().then((response) => {
|
||||
|
||||
let installation_id = response.installation_id;
|
||||
|
||||
function setCurrentUser() {
|
||||
lbryio.call('user', 'me').then((data) => {
|
||||
lbryio.user = data
|
||||
resolve(data)
|
||||
}).catch(function(err) {
|
||||
lbryio.setAccessToken(null);
|
||||
if (!getSession('reloadedOnFailedAuth')) {
|
||||
setSession('reloadedOnFailedAuth', true)
|
||||
window.location.reload();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!lbryio.getAccessToken()) {
|
||||
lbryio.call('user', 'new', {
|
||||
language: 'en',
|
||||
app_id: installation_id,
|
||||
}, 'post').then(function(responseData) {
|
||||
if (!responseData.ID) {
|
||||
reject(new Error("Received invalid authentication response."));
|
||||
}
|
||||
lbryio.setAccessToken(installation_id)
|
||||
setCurrentUser()
|
||||
}).catch(function(error) {
|
||||
/*
|
||||
until we have better error code format, assume all errors are duplicate application id
|
||||
if we're wrong, this will be caught by later attempts to make a valid call
|
||||
*/
|
||||
lbryio.setAccessToken(installation_id)
|
||||
setCurrentUser()
|
||||
})
|
||||
} else {
|
||||
setCurrentUser()
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
return lbryio._authenticationPromise;
|
||||
}
|
||||
|
||||
export default lbryio;
|
169
ui/js/lbryuri.js
Normal file
169
ui/js/lbryuri.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
const CHANNEL_NAME_MIN_LEN = 4;
|
||||
const CLAIM_ID_MAX_LEN = 40;
|
||||
|
||||
const lbryuri = {};
|
||||
|
||||
/**
|
||||
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||
* messages for invalid names.
|
||||
*
|
||||
* N.B. that "name" indicates the value in the name position of the URI. For
|
||||
* claims for channel content, this will actually be the channel name, and
|
||||
* the content name is in the path (e.g. lbry://@channel/content)
|
||||
*
|
||||
* In most situations, you'll want to use the contentName and channelName keys
|
||||
* and ignore the name key.
|
||||
*
|
||||
* Returns a dictionary with keys:
|
||||
* - name (string): The value in the "name" position in the URI. Note that this
|
||||
* could be either content name or channel name; see above.
|
||||
* - path (string, if persent)
|
||||
* - claimSequence (int, if present)
|
||||
* - bidPosition (int, if present)
|
||||
* - claimId (string, if present)
|
||||
* - isChannel (boolean)
|
||||
* - contentName (string): For anon claims, the name; for channel claims, the path
|
||||
* - channelName (string, if present): Channel name without @
|
||||
*/
|
||||
lbryuri.parse = function(uri, requireProto=false) {
|
||||
// Break into components. Empty sub-matches are converted to null
|
||||
const componentsRegex = new RegExp(
|
||||
'^((?:lbry:\/\/)?)' + // protocol
|
||||
'([^:$#/]*)' + // name (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
|
||||
'(/?)(.*)' // path separator, path
|
||||
);
|
||||
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null);
|
||||
|
||||
let contentName;
|
||||
|
||||
// Validate protocol
|
||||
if (requireProto && !proto) {
|
||||
throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
|
||||
}
|
||||
|
||||
// Validate and process name
|
||||
if (!name) {
|
||||
throw new Error('URI does not include name.');
|
||||
}
|
||||
|
||||
const isChannel = name.startsWith('@');
|
||||
const channelName = isChannel ? name.slice(1) : name;
|
||||
|
||||
if (isChannel) {
|
||||
if (!channelName) {
|
||||
throw new Error('No channel name after @.');
|
||||
}
|
||||
|
||||
if (channelName.length < CHANNEL_NAME_MIN_LEN) {
|
||||
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
|
||||
}
|
||||
|
||||
contentName = path;
|
||||
}
|
||||
|
||||
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
|
||||
}
|
||||
|
||||
// Validate and process modifier (claim ID, bid position or claim sequence)
|
||||
let claimId, claimSequence, bidPosition;
|
||||
if (modSep) {
|
||||
if (!modVal) {
|
||||
throw new Error(`No modifier provided after separator ${modSep}.`);
|
||||
}
|
||||
|
||||
if (modSep == '#') {
|
||||
claimId = modVal;
|
||||
} else if (modSep == ':') {
|
||||
claimSequence = modVal;
|
||||
} else if (modSep == '$') {
|
||||
bidPosition = modVal;
|
||||
}
|
||||
}
|
||||
|
||||
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) {
|
||||
throw new Error(`Invalid claim ID ${claimId}.`);
|
||||
}
|
||||
|
||||
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
|
||||
throw new Error('Claim sequence must be a number.');
|
||||
}
|
||||
|
||||
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
|
||||
throw new Error('Bid position must be a number.');
|
||||
}
|
||||
|
||||
// Validate and process path
|
||||
if (path) {
|
||||
if (!isChannel) {
|
||||
throw new Error('Only channel URIs may have a path.');
|
||||
}
|
||||
|
||||
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
|
||||
if (pathBadChars) {
|
||||
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
|
||||
}
|
||||
|
||||
contentName = path;
|
||||
} else if (pathSep) {
|
||||
throw new Error('No path provided after /');
|
||||
}
|
||||
|
||||
return {
|
||||
name, path, isChannel,
|
||||
... contentName ? {contentName} : {},
|
||||
... channelName ? {channelName} : {},
|
||||
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
|
||||
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
|
||||
... claimId ? {claimId} : {},
|
||||
... path ? {path} : {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an object in the same format returned by lbryuri.parse() and builds a URI.
|
||||
*
|
||||
* The channelName key will accept names with or without the @ prefix.
|
||||
*/
|
||||
lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) {
|
||||
let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj;
|
||||
|
||||
if (channelName) {
|
||||
const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName;
|
||||
if (!name) {
|
||||
name = channelNameFormatted;
|
||||
} else if (name !== channelNameFormatted) {
|
||||
throw new Error('Received a channel content URI, but name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.');
|
||||
}
|
||||
}
|
||||
|
||||
if (contentName) {
|
||||
if (!name) {
|
||||
name = contentName;
|
||||
} else if (!path) {
|
||||
path = contentName;
|
||||
}
|
||||
if (path && path !== contentName) {
|
||||
throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.');
|
||||
}
|
||||
}
|
||||
|
||||
return (includeProto ? 'lbry://' : '') + name +
|
||||
(claimId ? `#${claimId}` : '') +
|
||||
(claimSequence ? `:${claimSequence}` : '') +
|
||||
(bidPosition ? `\$${bidPosition}` : '') +
|
||||
(path ? `/${path}` : '');
|
||||
|
||||
}
|
||||
|
||||
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
|
||||
* consists of adding the lbry:// prefix if needed) */
|
||||
lbryuri.normalize= function(uri) {
|
||||
const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri);
|
||||
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
|
||||
}
|
||||
|
||||
window.lbryuri = lbryuri;
|
||||
export default lbryuri;
|
|
@ -1,12 +1,12 @@
|
|||
import lbry from './lbry.js';
|
||||
import jsonrpc from './jsonrpc.js';
|
||||
|
||||
const queryTimeout = 5000;
|
||||
const maxQueryTries = 5;
|
||||
const queryTimeout = 3000;
|
||||
const maxQueryTries = 2;
|
||||
const defaultServers = [
|
||||
'http://lighthouse4.lbry.io:50005',
|
||||
'http://lighthouse5.lbry.io:50005',
|
||||
'http://lighthouse6.lbry.io:50005',
|
||||
'http://lighthouse7.lbry.io:50005',
|
||||
'http://lighthouse8.lbry.io:50005',
|
||||
'http://lighthouse9.lbry.io:50005',
|
||||
];
|
||||
const path = '/';
|
||||
|
||||
|
@ -20,12 +20,9 @@ function getServers() {
|
|||
}
|
||||
|
||||
function call(method, params, callback, errorCallback) {
|
||||
if (connectTryNum > maxQueryTries) {
|
||||
if (connectFailedCallback) {
|
||||
connectFailedCallback();
|
||||
} else {
|
||||
throw new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`);
|
||||
}
|
||||
if (connectTryNum >= maxQueryTries) {
|
||||
errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,7 +45,7 @@ function call(method, params, callback, errorCallback) {
|
|||
}, () => {
|
||||
connectTryNum++;
|
||||
call(method, params, callback, errorCallback);
|
||||
});
|
||||
}, queryTimeout);
|
||||
}
|
||||
|
||||
const lighthouse = new Proxy({}, {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import lbry from './lbry.js';
|
||||
import lbryio from './lbryio.js';
|
||||
import lighthouse from './lighthouse.js';
|
||||
import App from './app.js';
|
||||
import SplashScreen from './component/splash.js';
|
||||
import SnackBar from './component/snack-bar.js';
|
||||
import {AuthOverlay} from './component/auth.js';
|
||||
|
||||
const {remote} = require('electron');
|
||||
const contextMenu = remote.require('./menu/context-menu');
|
||||
|
@ -16,31 +19,24 @@ window.addEventListener('contextmenu', (event) => {
|
|||
event.preventDefault();
|
||||
});
|
||||
|
||||
var init = function() {
|
||||
let init = function() {
|
||||
window.lbry = lbry;
|
||||
window.lighthouse = lighthouse;
|
||||
let canvas = document.getElementById('canvas');
|
||||
|
||||
lbry.connect().then(function(isConnected) {
|
||||
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
|
||||
})
|
||||
|
||||
function onDaemonReady() {
|
||||
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
|
||||
ReactDOM.render(<div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div>, canvas)
|
||||
}
|
||||
|
||||
var canvas = document.getElementById('canvas');
|
||||
if (window.sessionStorage.getItem('loaded') == 'y') {
|
||||
ReactDOM.render(<App/>, canvas)
|
||||
onDaemonReady();
|
||||
} else {
|
||||
ReactDOM.render(
|
||||
<SplashScreen message="Connecting" onLoadDone={function() {
|
||||
// Redirect to the claim code page if needed. Find somewhere better for this logic
|
||||
if (!localStorage.getItem('claimCodeDone') && window.location.search == '' || window.location.search == '?' || window.location.search == 'discover') {
|
||||
lbry.getBalance((balance) => {
|
||||
if (balance <= 0) {
|
||||
window.location.href = '?claim';
|
||||
} else {
|
||||
ReactDOM.render(<App/>, canvas);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ReactDOM.render(<App/>, canvas);
|
||||
}
|
||||
}}/>,
|
||||
canvas
|
||||
);
|
||||
ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import {Link} from '../component/link.js';
|
||||
|
||||
var claimCodeContentStyle = {
|
||||
display: 'inline-block',
|
||||
textAlign: 'left',
|
||||
width: '600px',
|
||||
}, claimCodeLabelStyle = {
|
||||
display: 'inline-block',
|
||||
cursor: 'default',
|
||||
width: '130px',
|
||||
textAlign: 'right',
|
||||
marginRight: '6px',
|
||||
};
|
||||
|
||||
var ClaimCodePage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
submitting: false,
|
||||
modal: null,
|
||||
referralCredits: null,
|
||||
activationCredits: null,
|
||||
failureReason: null,
|
||||
}
|
||||
},
|
||||
handleSubmit: function(event) {
|
||||
if (typeof event !== 'undefined') {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (!this.refs.code.value) {
|
||||
this.setState({
|
||||
modal: 'missingCode',
|
||||
});
|
||||
return;
|
||||
} else if (!this.refs.email.value) {
|
||||
this.setState({
|
||||
modal: 'missingEmail',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
|
||||
lbry.getUnusedAddress((address) => {
|
||||
var code = this.refs.code.value;
|
||||
var email = this.refs.email.value;
|
||||
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.addEventListener('load', () => {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
modal: 'codeRedeemed',
|
||||
referralCredits: response.referralCredits,
|
||||
activationCredits: response.activationCredits,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
modal: 'codeRedeemFailed',
|
||||
failureReason: response.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
modal: 'couldNotConnect',
|
||||
});
|
||||
});
|
||||
|
||||
xhr.open('POST', 'https://invites.lbry.io', true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
|
||||
'&email=' + encodeURIComponent(email));
|
||||
});
|
||||
},
|
||||
handleSkip: function() {
|
||||
this.setState({
|
||||
modal: 'skipped',
|
||||
});
|
||||
},
|
||||
handleFinished: function() {
|
||||
localStorage.setItem('claimCodeDone', true);
|
||||
window.location = '?home';
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="card">
|
||||
<h2>Claim your beta invitation code</h2>
|
||||
<section style={claimCodeContentStyle}>
|
||||
<p>Thanks for beta testing LBRY! Enter your invitation code and email address below to receive your initial
|
||||
LBRY credits.</p>
|
||||
<p>You will be added to our mailing list (if you're not already on it) and will be eligible for future rewards for beta testers.</p>
|
||||
</section>
|
||||
<section>
|
||||
<section><label style={claimCodeLabelStyle} htmlFor="code">Invitation code</label><input name="code" ref="code" /></section>
|
||||
<section><label style={claimCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
|
||||
</section>
|
||||
<section>
|
||||
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
|
||||
disabled={this.state.submitting} onClick={this.handleSubmit} />
|
||||
<Link button="alt" label="Skip" disabled={this.state.submitting} onClick={this.handleSkip} />
|
||||
<input type='submit' className='hidden' />
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
<Modal isOpen={this.state.modal == 'missingCode'} contentLabel="Invitation code required"
|
||||
onConfirmed={this.closeModal}>
|
||||
Please enter an invitation code or choose "Skip."
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'missingEmail'} contentLabel="Email required"
|
||||
onConfirmed={this.closeModal}>
|
||||
Please enter an email address or choose "Skip."
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'codeRedeemFailed'} contentLabel="Failed to redeem code"
|
||||
onConfirmed={this.closeModal}>
|
||||
{this.state.failureReason}
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
|
||||
onConfirmed={this.handleFinished}>
|
||||
Your invite code has been redeemed. { ' ' }
|
||||
{this.state.referralCredits > 0
|
||||
? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits}
|
||||
will be added to your balance shortly.`
|
||||
: (this.state.activationCredits > 0
|
||||
? `${this.state.activationCredits} credits will be added to your balance shortly.`
|
||||
: 'The credits will be added to your balance shortly.')}
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'skipped'} contentLabel="Welcome to LBRY"
|
||||
onConfirmed={this.handleFinished}>
|
||||
Welcome to LBRY! You can visit the Wallet page to redeem an invite code at any time.
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Could not connect"
|
||||
onConfirmed={this.closeModal}>
|
||||
<p>LBRY couldn't connect to our servers to confirm your invitation code. Please check your internet connection.</p>
|
||||
If you continue to have problems, you can still browse LBRY and visit the Settings page to redeem your code later.
|
||||
</Modal>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ClaimCodePage;
|
|
@ -1,6 +1,6 @@
|
|||
import lbry from '../lbry.js';
|
||||
import React from 'react';
|
||||
import FormField from '../component/form.js';
|
||||
import {FormField} from '../component/form.js';
|
||||
import {Link} from '../component/link.js';
|
||||
|
||||
const fs = require('fs');
|
||||
|
|
|
@ -1,168 +1,62 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {FileTile} from '../component/file-tile.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {BusyMessage} from '../component/common.js';
|
||||
|
||||
var fetchResultsStyle = {
|
||||
color: '#888',
|
||||
textAlign: 'center',
|
||||
fontSize: '1.2em'
|
||||
};
|
||||
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
|
||||
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
|
||||
'"five" to put your content here!');
|
||||
|
||||
var SearchActive = React.createClass({
|
||||
let FeaturedCategory = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div style={fetchResultsStyle}>
|
||||
<BusyMessage message="Looking up the Dewey Decimals" />
|
||||
</div>
|
||||
);
|
||||
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>)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
var searchNoResultsStyle = {
|
||||
textAlign: 'center'
|
||||
}, searchNoResultsMessageStyle = {
|
||||
fontStyle: 'italic',
|
||||
marginRight: '5px'
|
||||
};
|
||||
|
||||
var SearchNoResults = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<section style={searchNoResultsStyle}>
|
||||
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {this.props.query} yet.</span>
|
||||
<Link label="Be the first" href="?publish" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResults = React.createClass({
|
||||
render: function() {
|
||||
var rows = [],
|
||||
seenNames = {}; //fix this when the search API returns claim IDs
|
||||
this.props.results.forEach(function({name, value}) {
|
||||
if (!seenNames[name]) {
|
||||
seenNames[name] = name;
|
||||
rows.push(
|
||||
<FileTile key={name} uri={name} sdHash={value.sources.lbry_sd_hash} />
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div>{rows}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var featuredContentLegendStyle = {
|
||||
fontSize: '12px',
|
||||
color: '#aaa',
|
||||
verticalAlign: '15%',
|
||||
};
|
||||
|
||||
var FeaturedContent = React.createClass({
|
||||
let DiscoverPage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
featuredNames: [],
|
||||
featuredUris: {},
|
||||
failed: false
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
lbry.getFeaturedDiscoverNames().then((featuredNames) => {
|
||||
this.setState({ featuredNames: featuredNames });
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
const toolTipText = ('Community Content is a public space where anyone can share content with the ' +
|
||||
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
|
||||
'"five" to put your content here!');
|
||||
|
||||
return (
|
||||
<div className="row-fluid">
|
||||
<div className="span6">
|
||||
<h3>Featured Content</h3>
|
||||
{ this.state.featuredNames.map((name) => { return <FileTile key={name} uri={name} /> }) }
|
||||
</div>
|
||||
<div className="span6">
|
||||
<h3>
|
||||
Community Content
|
||||
<ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/>
|
||||
</h3>
|
||||
<FileTile uri="one" />
|
||||
<FileTile uri="two" />
|
||||
<FileTile uri="three" />
|
||||
<FileTile uri="four" />
|
||||
<FileTile uri="five" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DiscoverPage = React.createClass({
|
||||
userTypingTimer: null,
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.props.query != this.state.query)
|
||||
{
|
||||
this.handleSearchChanged(this.props.query);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps, nextState) {
|
||||
if (nextProps.query != nextState.query)
|
||||
{
|
||||
this.handleSearchChanged(nextProps.query);
|
||||
}
|
||||
},
|
||||
|
||||
handleSearchChanged: function(query) {
|
||||
this.setState({
|
||||
searching: true,
|
||||
query: query,
|
||||
});
|
||||
|
||||
lighthouse.search(query).then(this.searchCallback);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
document.title = "Discover";
|
||||
if (this.props.query) {
|
||||
// Rendering with a query already typed
|
||||
this.handleSearchChanged(this.props.query);
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
results: [],
|
||||
query: this.props.query,
|
||||
searching: ('query' in this.props) && (this.props.query.length > 0)
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function(results) {
|
||||
if (this.state.searching) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
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({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
failed: true
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<main>
|
||||
{ this.state.searching ? <SearchActive /> : null }
|
||||
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null }
|
||||
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null }
|
||||
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
|
||||
</main>
|
||||
);
|
||||
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>;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import FormField from '../component/form.js';
|
||||
import {FormField} from '../component/form.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import {FileTileStream} from '../component/file-tile.js';
|
||||
import rewards from '../rewards.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import {BusyMessage, Thumbnail} from '../component/common.js';
|
||||
|
||||
|
||||
export let FileListNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?downloaded': 'Downloaded',
|
||||
'?published': 'Published',
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
export let FileListDownloaded = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
|
@ -16,7 +29,6 @@ export let FileListDownloaded = React.createClass({
|
|||
},
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
document.title = "Downloaded Files";
|
||||
|
||||
lbry.claim_list_mine().then((myClaimInfos) => {
|
||||
if (!this._isMounted) { return; }
|
||||
|
@ -31,26 +43,24 @@ export let FileListDownloaded = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
render: function() {
|
||||
let content = "";
|
||||
if (this.state.fileInfos === null) {
|
||||
return (
|
||||
<main className="page">
|
||||
<BusyMessage message="Loading" />
|
||||
</main>
|
||||
);
|
||||
content = <BusyMessage message="Loading" />;
|
||||
} else if (!this.state.fileInfos.length) {
|
||||
return (
|
||||
<main className="page">
|
||||
<span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
|
||||
</main>
|
||||
);
|
||||
content = <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>;
|
||||
} else {
|
||||
return (
|
||||
<main className="page">
|
||||
<FileList fileInfos={this.state.fileInfos} hidePrices={true} />
|
||||
</main>
|
||||
);
|
||||
content = <FileList fileInfos={this.state.fileInfos} hidePrices={true} />;
|
||||
}
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<FileListNav viewingPage="downloaded" />
|
||||
{content}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -62,9 +72,22 @@ export let FileListPublished = React.createClass({
|
|||
fileInfos: null,
|
||||
};
|
||||
},
|
||||
_requestPublishReward: function() {
|
||||
lbryio.call('reward', 'list', {}).then(function(userRewards) {
|
||||
//already rewarded
|
||||
if (userRewards.filter(function (reward) {
|
||||
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
|
||||
}).length) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
|
||||
}
|
||||
}, () => {});
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this._isMounted = true;
|
||||
document.title = "Published Files";
|
||||
this._requestPublishReward();
|
||||
|
||||
lbry.claim_list_mine().then((claimInfos) => {
|
||||
if (!this._isMounted) { return; }
|
||||
|
@ -79,28 +102,26 @@ export let FileListPublished = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
render: function () {
|
||||
let content = null;
|
||||
if (this.state.fileInfos === null) {
|
||||
return (
|
||||
<main className="page">
|
||||
<BusyMessage message="Loading" />
|
||||
</main>
|
||||
);
|
||||
content = <BusyMessage message="Loading" />;
|
||||
}
|
||||
else if (!this.state.fileInfos.length) {
|
||||
return (
|
||||
<main className="page">
|
||||
<span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
|
||||
</main>
|
||||
);
|
||||
content = <span>You haven't published anything to LBRY yet. Try <Link href="?publish" label="publishing" />!</span>;
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<main className="page">
|
||||
<FileList fileInfos={this.state.fileInfos} />
|
||||
</main>
|
||||
);
|
||||
content = <FileList fileInfos={this.state.fileInfos} />;
|
||||
}
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<FileListNav viewingPage="published" />
|
||||
{content}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -160,14 +181,24 @@ export let FileList = React.createClass({
|
|||
seenUris = {};
|
||||
|
||||
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
|
||||
for (let {name, outpoint, metadata} of fileInfosSorted) {
|
||||
if (!metadata || seenUris[name]) {
|
||||
for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
|
||||
if (seenUris[name] || !claim_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let streamMetadata;
|
||||
if (metadata) {
|
||||
streamMetadata = metadata.stream.metadata;
|
||||
} else {
|
||||
streamMetadata = null;
|
||||
}
|
||||
|
||||
|
||||
const uri = lbryuri.build({contentName: name, channelName: channel_name});
|
||||
seenUris[name] = true;
|
||||
content.push(<FileTileStream key={outpoint} outpoint={outpoint} name={name} hideOnRemove={true}
|
||||
hidePrice={this.props.hidePrices} metadata={metadata} />);
|
||||
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true}
|
||||
hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
|
||||
hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {SettingsNav} from './settings.js';
|
||||
import {version as uiVersion} from 'json!../../package.json';
|
||||
|
||||
var HelpPage = React.createClass({
|
||||
|
@ -24,9 +25,6 @@ var HelpPage = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Help";
|
||||
},
|
||||
render: function() {
|
||||
let ver, osName, platform, newVerLink;
|
||||
if (this.state.versionInfo) {
|
||||
|
@ -49,58 +47,71 @@ var HelpPage = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<SettingsNav viewingPage="help" />
|
||||
<section className="card">
|
||||
<h3>Read the FAQ</h3>
|
||||
<p>Our FAQ answers many common questions.</p>
|
||||
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p>
|
||||
<div className="card__title-primary">
|
||||
<h3>Read the FAQ</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p>Our FAQ answers many common questions.</p>
|
||||
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Get Live Help</h3>
|
||||
<p>
|
||||
Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room.
|
||||
</p>
|
||||
<p>
|
||||
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" />
|
||||
</p>
|
||||
<div className="card__title-primary">
|
||||
<h3>Get Live Help</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p>
|
||||
Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room.
|
||||
</p>
|
||||
<p>
|
||||
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" />
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Report a Bug</h3>
|
||||
<p>Did you find something wrong?</p>
|
||||
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
|
||||
<div className="meta">Thanks! LBRY is made by its users.</div>
|
||||
<div className="card__title-primary"><h3>Report a Bug</h3></div>
|
||||
<div className="card__content">
|
||||
<p>Did you find something wrong?</p>
|
||||
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
|
||||
<div className="meta">Thanks! LBRY is made by its users.</div>
|
||||
</div>
|
||||
</section>
|
||||
{!ver ? null :
|
||||
<section className="card">
|
||||
<h3>About</h3>
|
||||
{ver.lbrynet_update_available || ver.lbryum_update_available ?
|
||||
<p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p>
|
||||
: <p>Your copy of LBRY is up to date.</p>
|
||||
}
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>daemon (lbrynet)</th>
|
||||
<td>{ver.lbrynet_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>wallet (lbryum)</th>
|
||||
<td>{ver.lbryum_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>interface</th>
|
||||
<td>{uiVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>{platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Installation ID</th>
|
||||
<td>{this.state.lbryId}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="card__title-primary"><h3>About</h3></div>
|
||||
<div className="card__content">
|
||||
{ver.lbrynet_update_available || ver.lbryum_update_available ?
|
||||
<p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p>
|
||||
: <p>Your copy of LBRY is up to date.</p>
|
||||
}
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>daemon (lbrynet)</th>
|
||||
<td>{ver.lbrynet_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>wallet (lbryum)</th>
|
||||
<td>{ver.lbryum_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>interface</th>
|
||||
<td>{uiVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>{platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Installation ID</th>
|
||||
<td>{this.state.lbryId}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import FormField from '../component/form.js';
|
||||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import rewards from '../rewards.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import Modal from '../component/modal.js';
|
||||
|
||||
var PublishPage = React.createClass({
|
||||
_requiredFields: ['name', 'bid', 'meta_title', 'meta_author', 'meta_license', 'meta_description'],
|
||||
_requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'],
|
||||
|
||||
_updateChannelList: function(channel) {
|
||||
// Calls API to update displayed list of channels. If a channel name is provided, will select
|
||||
// that channel at the same time (used immediately after creating a channel)
|
||||
lbry.channel_list_mine().then((channels) => {
|
||||
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {})
|
||||
this.setState({
|
||||
channels: channels,
|
||||
... channel ? {channel} : {}
|
||||
|
@ -26,19 +29,23 @@ var PublishPage = React.createClass({
|
|||
submitting: true,
|
||||
});
|
||||
|
||||
var checkFields = this._requiredFields.slice();
|
||||
let checkFields = this._requiredFields;
|
||||
if (!this.state.myClaimExists) {
|
||||
checkFields.push('file');
|
||||
checkFields.unshift('file');
|
||||
}
|
||||
|
||||
var missingFieldFound = false;
|
||||
let missingFieldFound = false;
|
||||
for (let fieldName of checkFields) {
|
||||
var field = this.refs[fieldName];
|
||||
if (field.getValue() === '') {
|
||||
field.warnRequired();
|
||||
if (!missingFieldFound) {
|
||||
field.focus();
|
||||
missingFieldFound = true;
|
||||
const field = this.refs[fieldName];
|
||||
if (field) {
|
||||
if (field.getValue() === '' || field.getValue() === false) {
|
||||
field.showRequiredError();
|
||||
if (!missingFieldFound) {
|
||||
field.focus();
|
||||
missingFieldFound = true;
|
||||
}
|
||||
} else {
|
||||
field.clearError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,14 +67,16 @@ var PublishPage = React.createClass({
|
|||
var metadata = {};
|
||||
}
|
||||
|
||||
for (let metaField of ['title', 'author', 'description', 'thumbnail', 'license', 'license_url', 'language', 'nsfw']) {
|
||||
for (let metaField of ['title', 'description', 'thumbnail', 'license', 'license_url', 'language']) {
|
||||
var value = this.refs['meta_' + metaField].getValue();
|
||||
if (value !== '') {
|
||||
metadata[metaField] = value;
|
||||
}
|
||||
}
|
||||
|
||||
var licenseUrl = this.refs.meta_license_url.getValue();
|
||||
metadata.nsfw = Boolean(parseInt(!!this.refs.meta_nsfw.getValue()));
|
||||
|
||||
const licenseUrl = this.refs.meta_license_url.getValue();
|
||||
if (licenseUrl) {
|
||||
metadata.license_url = licenseUrl;
|
||||
}
|
||||
|
@ -81,9 +90,9 @@ var PublishPage = React.createClass({
|
|||
};
|
||||
|
||||
if (this.refs.file.getValue() !== '') {
|
||||
publishArgs.file_path = this.refs.file.getValue();
|
||||
publishArgs.file_path = this.refs.file.getValue();
|
||||
}
|
||||
|
||||
|
||||
lbry.publish(publishArgs, (message) => {
|
||||
this.handlePublishStarted();
|
||||
}, null, (error) => {
|
||||
|
@ -110,17 +119,18 @@ var PublishPage = React.createClass({
|
|||
channels: null,
|
||||
rawName: '',
|
||||
name: '',
|
||||
bid: '',
|
||||
bid: 10,
|
||||
hasFile: false,
|
||||
feeAmount: '',
|
||||
feeCurrency: 'USD',
|
||||
channel: 'anonymous',
|
||||
newChannelName: '@',
|
||||
newChannelBid: '',
|
||||
nameResolved: false,
|
||||
newChannelBid: 10,
|
||||
nameResolved: null,
|
||||
myClaimExists: null,
|
||||
topClaimValue: 0.0,
|
||||
myClaimValue: 0.0,
|
||||
myClaimMetadata: null,
|
||||
myClaimExists: null,
|
||||
copyrightNotice: '',
|
||||
otherLicenseDescription: '',
|
||||
otherLicenseUrl: '',
|
||||
|
@ -138,7 +148,7 @@ var PublishPage = React.createClass({
|
|||
});
|
||||
},
|
||||
handlePublishStartedConfirmed: function() {
|
||||
window.location = "?published";
|
||||
window.location.href = "?published";
|
||||
},
|
||||
handlePublishError: function(error) {
|
||||
this.setState({
|
||||
|
@ -161,57 +171,63 @@ var PublishPage = React.createClass({
|
|||
}
|
||||
|
||||
if (!lbry.nameIsValid(rawName, false)) {
|
||||
this.refs.name.showAdvice('LBRY names must contain only letters, numbers and dashes.');
|
||||
this.refs.name.showError('LBRY names must contain only letters, numbers and dashes.');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = rawName.toLowerCase();
|
||||
this.setState({
|
||||
rawName: rawName,
|
||||
name: name,
|
||||
nameResolved: null,
|
||||
myClaimExists: null,
|
||||
});
|
||||
|
||||
var name = rawName.toLowerCase();
|
||||
|
||||
lbry.resolveName(name, (info) => {
|
||||
if (name != this.refs.name.getValue().toLowerCase()) {
|
||||
lbry.getMyClaim(name, (myClaimInfo) => {
|
||||
if (name != this.state.name) {
|
||||
// A new name has been typed already, so bail
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
this.setState({
|
||||
myClaimExists: !!myClaimInfo,
|
||||
});
|
||||
lbry.resolve({uri: name}).then((claimInfo) => {
|
||||
if (name != this.state.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!claimInfo) {
|
||||
this.setState({
|
||||
nameResolved: false,
|
||||
});
|
||||
} else {
|
||||
const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount);
|
||||
const newState = {
|
||||
nameResolved: true,
|
||||
topClaimValue: parseFloat(claimInfo.claim.amount),
|
||||
myClaimExists: !!myClaimInfo,
|
||||
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.claim.amount) : null,
|
||||
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
|
||||
topClaimIsMine: topClaimIsMine,
|
||||
};
|
||||
|
||||
if (topClaimIsMine) {
|
||||
newState.bid = myClaimInfo.claim.amount;
|
||||
} else if (this.state.myClaimMetadata) {
|
||||
// Just changed away from a name we have a claim on, so clear pre-fill
|
||||
newState.bid = '';
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
}, () => { // Assume an error means the name is available
|
||||
this.setState({
|
||||
name: name,
|
||||
nameResolved: false,
|
||||
myClaimExists: false,
|
||||
});
|
||||
} else {
|
||||
lbry.getMyClaim(name, (myClaimInfo) => {
|
||||
lbry.getClaimInfo(name, (claimInfo) => {
|
||||
if (name != this.refs.name.getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount);
|
||||
const newState = {
|
||||
name: name,
|
||||
nameResolved: true,
|
||||
topClaimValue: parseFloat(claimInfo.amount),
|
||||
myClaimExists: !!myClaimInfo,
|
||||
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null,
|
||||
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
|
||||
topClaimIsMine: topClaimIsMine,
|
||||
};
|
||||
|
||||
if (topClaimIsMine) {
|
||||
newState.bid = myClaimInfo.amount;
|
||||
} else if (this.state.myClaimMetadata) {
|
||||
// Just changed away from a name we have a claim on, so clear pre-fill
|
||||
newState.bid = '';
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
handleBidChange: function(event) {
|
||||
|
@ -234,7 +250,7 @@ var PublishPage = React.createClass({
|
|||
isFee: feeEnabled
|
||||
});
|
||||
},
|
||||
handeLicenseChange: function(event) {
|
||||
handleLicenseChange: function(event) {
|
||||
var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type');
|
||||
var newState = {
|
||||
copyrightChosen: licenseType == 'copyright',
|
||||
|
@ -242,8 +258,7 @@ var PublishPage = React.createClass({
|
|||
};
|
||||
|
||||
if (licenseType == 'copyright') {
|
||||
var author = this.refs.meta_author.getValue();
|
||||
newState.copyrightNotice = 'Copyright ' + (new Date().getFullYear()) + (author ? ' ' + author : '');
|
||||
newState.copyrightNotice = 'All rights reserved.'
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
@ -274,8 +289,10 @@ var PublishPage = React.createClass({
|
|||
const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value);
|
||||
|
||||
if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) {
|
||||
this.refs.newChannelName.showAdvice('LBRY channel names must contain only letters, numbers and dashes.');
|
||||
this.refs.newChannelName.showError('LBRY channel names must contain only letters, numbers and dashes.');
|
||||
return;
|
||||
} else {
|
||||
this.refs.newChannelName.clearError()
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -287,9 +304,14 @@ var PublishPage = React.createClass({
|
|||
newChannelBid: event.target.value,
|
||||
});
|
||||
},
|
||||
handleTOSChange: function(event) {
|
||||
this.setState({
|
||||
TOSAgreed: event.target.checked,
|
||||
});
|
||||
},
|
||||
handleCreateChannelClick: function (event) {
|
||||
if (this.state.newChannelName.length < 5) {
|
||||
this.refs.newChannelName.showAdvice('LBRY channel names must be at least 4 characters in length.');
|
||||
this.refs.newChannelName.showError('LBRY channel names must be at least 4 characters in length.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -308,7 +330,7 @@ var PublishPage = React.createClass({
|
|||
}, 5000);
|
||||
}, (error) => {
|
||||
// TODO: better error handling
|
||||
this.refs.newChannelName.showAdvice('Unable to create channel due to an internal error.');
|
||||
this.refs.newChannelName.showError('Unable to create channel due to an internal error.');
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
|
@ -326,167 +348,207 @@ var PublishPage = React.createClass({
|
|||
componentWillMount: function() {
|
||||
this._updateChannelList();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Publish";
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
},
|
||||
// Also getting a type warning here too
|
||||
onFileChange: function() {
|
||||
if (this.refs.file.getValue()) {
|
||||
this.setState({ hasFile: true })
|
||||
} else {
|
||||
this.setState({ hasFile: false })
|
||||
}
|
||||
},
|
||||
getNameBidHelpText: function() {
|
||||
if (!this.state.name) {
|
||||
return "Select a URL for this publish.";
|
||||
} else if (this.state.nameResolved === false) {
|
||||
return "This URL is unused.";
|
||||
} else if (this.state.myClaimExists) {
|
||||
return "You have already used this URL. Publishing to it again will update your previous publish."
|
||||
} else if (this.state.topClaimValue) {
|
||||
return <span>A deposit of at least <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit ' : 'credits '}
|
||||
is required to win <strong>{this.state.name}</strong>. However, you can still get a permanent URL for any amount.</span>
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
if (this.state.channels === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
|
||||
|
||||
return (
|
||||
<main ref="page">
|
||||
<main className="main--single-column">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<section className="card">
|
||||
<h4>LBRY Name</h4>
|
||||
<div className="form-row">
|
||||
lbry://<FormField type="text" ref="name" value={this.state.rawName} onChange={this.handleNameChange} />
|
||||
{
|
||||
(!this.state.name ? '' :
|
||||
(! this.state.nameResolved ? <em> The name <strong>{this.state.name}</strong> is available.</em>
|
||||
: (this.state.myClaimExists ? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em>
|
||||
: <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>)))
|
||||
}
|
||||
<div className="help">What LBRY name would you like to claim for this file?</div>
|
||||
<div className="card__title-primary">
|
||||
<h4>Content</h4>
|
||||
<div className="card__subtitle">
|
||||
What are you publishing?
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow name="file" label="File" ref="file" type="file" onChange={this.onFileChange}
|
||||
helper={this.state.myClaimExists ? "If you don't choose a file, the file from your existing claim will be used." : null}/>
|
||||
</div>
|
||||
{ !this.state.hasFile ? '' :
|
||||
<div>
|
||||
<div className="card__content">
|
||||
<FormRow label="Title" type="text" ref="meta_title" name="title" placeholder="Titular Title" />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow type="text" label="Thumbnail URL" ref="meta_thumbnail" name="thumbnail" placeholder="http://spee.ch/mylogo" />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow label="Description" type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow label="Language" type="select" defaultValue="en" ref="meta_language" name="language">
|
||||
<option value="en">English</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow type="select" label="Maturity" defaultValue="en" ref="meta_nsfw" name="nsfw">
|
||||
{/* <option value=""></option> */}
|
||||
<option value="0">All Ages</option>
|
||||
<option value="1">Adults Only</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>Access</h4>
|
||||
<div className="card__subtitle">
|
||||
How much does this content cost?
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="form-row__label-row">
|
||||
<label className="form-row__label">Price</label>
|
||||
</div>
|
||||
<FormRow label="Free" type="radio" name="isFree" value="1" onChange={ () => { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} />
|
||||
<FormField type="radio" name="isFree" label={!this.state.isFee ? 'Choose price...' : 'Price ' }
|
||||
onChange={ () => { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} />
|
||||
<span className={!this.state.isFee ? 'hidden' : ''}>
|
||||
<FormField type="number" className="form-field__input--inline" step="0.01" placeholder="1.00" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
|
||||
<option value="USD">US Dollars</option>
|
||||
<option value="LBC">LBRY credits</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{ this.state.isFee ?
|
||||
<div className="form-field__helper">
|
||||
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
|
||||
</div> : '' }
|
||||
<FormRow label="License" type="select" ref="meta_license" name="license" onChange={this.handleLicenseChange}>
|
||||
<option></option>
|
||||
<option>Public Domain</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
|
||||
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
|
||||
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
|
||||
</FormRow>
|
||||
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
|
||||
{this.state.copyrightChosen
|
||||
? <FormRow label="Copyright notice" type="text" name="copyright-notice"
|
||||
value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
|
||||
: null}
|
||||
{this.state.otherLicenseChosen ?
|
||||
<FormRow label="License description" type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
|
||||
: null}
|
||||
{this.state.otherLicenseChosen ?
|
||||
<FormRow label="License URL" type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
|
||||
: null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h4>Channel</h4>
|
||||
<div className="form-row">
|
||||
<FormField type="select" onChange={this.handleChannelChange} value={this.state.channel}>
|
||||
<div className="card__title-primary">
|
||||
<h4>Identity</h4>
|
||||
<div className="card__subtitle">
|
||||
Who created this content?
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow type="select" tabIndex="1" onChange={this.handleChannelChange} value={this.state.channel}>
|
||||
<option key="anonymous" value="anonymous">Anonymous</option>
|
||||
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
|
||||
<option key="new" value="new">New channel...</option>
|
||||
</FormField>
|
||||
{this.state.channel == 'new'
|
||||
? <section>
|
||||
<label>Name <FormField type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
|
||||
value={this.state.newChannelName} /></label>
|
||||
<label>Bid amount <FormField type="text-number" onChange={this.handleNewChannelBidChange} value={this.state.newChannelBid} /> LBC</label>
|
||||
<Link button="primary" label={!this.state.creatingChannel ? 'Create channel' : 'Creating channel...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
|
||||
</section>
|
||||
: null}
|
||||
<div className="help">What channel would you like to publish this file under?</div>
|
||||
<option key="new" value="new">New identity...</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h4>Choose File</h4>
|
||||
<FormField name="file" ref="file" type="file" />
|
||||
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null }
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h4>Bid Amount</h4>
|
||||
<div className="form-row">
|
||||
Credits <FormField ref="bid" type="text-number" onChange={this.handleBidChange} value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} />
|
||||
<div className="help">How much would you like to bid for this name?
|
||||
{ !this.state.nameResolved ? <span> Since this name is not currently resolved, you may bid as low as you want, but higher bids help prevent others from claiming your name.</span>
|
||||
: (this.state.topClaimIsMine ? <span> You currently control this name with a bid of <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.</span>
|
||||
: (this.state.myClaimExists ? <span> You have a non-winning bid on this name for <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.
|
||||
To control this name, you'll need to increase your bid to more than <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</span>
|
||||
: <span> You must bid over <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'} to claim this name.</span>)) }
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h4>Fee</h4>
|
||||
<div className="form-row">
|
||||
<label>
|
||||
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> No fee
|
||||
</label>
|
||||
<label>
|
||||
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(true) } } checked={this.state.isFee} /> { !this.state.isFee ? 'Choose fee...' : 'Fee ' }
|
||||
<span className={!this.state.isFee ? 'hidden' : ''}>
|
||||
<FormField type="text-number" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
|
||||
<option value="USD">US Dollars</option>
|
||||
<option value="LBC">LBRY credits</option>
|
||||
</FormField>
|
||||
</span>
|
||||
</label>
|
||||
<div className="help">
|
||||
<p>How much would you like to charge for this file?</p>
|
||||
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section className="card">
|
||||
<h4>Your Content</h4>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="title">Title</label><FormField type="text" ref="meta_title" name="title" placeholder="My Show, Episode 1" />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="author">Author</label><FormField type="text" ref="meta_author" name="author" placeholder="My Company, Inc." />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="license">License</label><FormField type="select" ref="meta_license" name="license" onChange={this.handeLicenseChange}>
|
||||
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
|
||||
<option>Public Domain</option>
|
||||
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
|
||||
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
|
||||
</FormField>
|
||||
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
|
||||
</div>
|
||||
{this.state.copyrightChosen
|
||||
? <div className="form-row">
|
||||
<label htmlFor="copyright-notice" value={this.state.copyrightNotice}>Copyright notice</label><FormField type="text" name="copyright-notice" value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
|
||||
{this.state.channel == 'new' ?
|
||||
<div className="card__content">
|
||||
<FormRow label="Name" type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
|
||||
value={this.state.newChannelName} />
|
||||
<FormRow label="Deposit"
|
||||
postfix="LBC"
|
||||
step="0.01"
|
||||
type="number"
|
||||
helper={lbcInputHelp}
|
||||
onChange={this.handleNewChannelBidChange}
|
||||
value={this.state.newChannelBid} />
|
||||
<div className="form-row-submit">
|
||||
<Link button="primary" label={!this.state.creatingChannel ? 'Create identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
{this.state.otherLicenseChosen
|
||||
? <div className="form-row">
|
||||
<label htmlFor="other-license-description">License description</label><FormField type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
|
||||
</div>
|
||||
: null}
|
||||
{this.state.otherLicenseChosen
|
||||
? <div className="form-row">
|
||||
<label htmlFor="other-license-url">License URL</label> <FormField type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="language">Language</label> <FormField type="select" defaultValue="en" ref="meta_language" name="language">
|
||||
<option value="en">English</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="de">German</option>
|
||||
<option value="jp">Japanese</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="es">Spanish</option>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="description">Description</label> <FormField type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label><FormField type="checkbox" ref="meta_nsfw" name="nsfw" placeholder="Description of your content" /> Not Safe For Work</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>Address</h4>
|
||||
<div className="card__subtitle">Where should this content permanently reside? <Link label="Read more" href="https://lbry.io/faq/naming" />.</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow prefix="lbry://" type="text" ref="name" placeholder="myname" value={this.state.rawName} onChange={this.handleNameChange}
|
||||
helper={this.getNameBidHelpText()} />
|
||||
</div>
|
||||
{ this.state.rawName ?
|
||||
<div className="card__content">
|
||||
<FormRow ref="bid"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="Deposit"
|
||||
postfix="LBC"
|
||||
onChange={this.handleBidChange}
|
||||
value={this.state.bid}
|
||||
placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100}
|
||||
helper={lbcInputHelp} />
|
||||
</div> : '' }
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h4>Additional Content Information (Optional)</h4>
|
||||
<div className="form-row">
|
||||
<label htmlFor="meta_thumbnail">Thumbnail URL</label> <FormField type="text" ref="meta_thumbnail" name="thumbnail" placeholder="http://mycompany.com/images/ep_1.jpg" />
|
||||
<div className="card__title-primary">
|
||||
<h4>Terms of Service</h4>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow label={
|
||||
<span>I agree to the <Link href="https://www.lbry.io/termsofservice" label="LBRY terms of service" checked={this.state.TOSAgreed} /></span>
|
||||
} type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card-series-submit">
|
||||
<Link button="primary" label={!this.state.submitting ? 'Publish' : 'Publishing...'} onClick={this.handleSubmit} disabled={this.state.submitting} />
|
||||
<Link button="cancel" onClick={window.history.back} label="Cancel" />
|
||||
<Link button="cancel" onClick={lbry.back} label="Cancel" />
|
||||
<input type="submit" className="hidden" />
|
||||
</div>
|
||||
</form>
|
||||
|
@ -494,7 +556,7 @@ var PublishPage = React.createClass({
|
|||
<Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published"
|
||||
onConfirmed={this.handlePublishStartedConfirmed}>
|
||||
<p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p>
|
||||
You will now be taken to your My Files page, where your newly published file will be listed. The file will take a few minutes to appear for other LBRY users; until then it will be listed as "pending."
|
||||
<p>The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.</p>
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file"
|
||||
onConfirmed={this.closeModal}>
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import Modal from '../component/modal.js';
|
||||
|
||||
var referralCodeContentStyle = {
|
||||
display: 'inline-block',
|
||||
textAlign: 'left',
|
||||
width: '600px',
|
||||
}, referralCodeLabelStyle = {
|
||||
display: 'inline-block',
|
||||
cursor: 'default',
|
||||
width: '130px',
|
||||
textAlign: 'right',
|
||||
marginRight: '6px',
|
||||
};
|
||||
|
||||
var ReferralPage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
submitting: false,
|
||||
modal: null,
|
||||
referralCredits: null,
|
||||
failureReason: null,
|
||||
}
|
||||
},
|
||||
handleSubmit: function(event) {
|
||||
if (typeof event !== 'undefined') {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (!this.refs.code.value) {
|
||||
this.setState({
|
||||
modal: 'missingCode',
|
||||
});
|
||||
} else if (!this.refs.email.value) {
|
||||
this.setState({
|
||||
modal: 'missingEmail',
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
|
||||
lbry.getUnusedAddress((address) => {
|
||||
var code = this.refs.code.value;
|
||||
var email = this.refs.email.value;
|
||||
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.addEventListener('load', () => {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
modal: 'referralInfo',
|
||||
referralCredits: response.referralCredits,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
modal: 'lookupFailed',
|
||||
failureReason: response.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
modal: 'couldNotConnect',
|
||||
});
|
||||
});
|
||||
|
||||
xhr.open('POST', 'https://invites.lbry.io/check', true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
|
||||
'&email=' + encodeURIComponent(email));
|
||||
});
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
},
|
||||
handleFinished: function() {
|
||||
localStorage.setItem('claimCodeDone', true);
|
||||
window.location = '?home';
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="card">
|
||||
<h2>Check your referral credits</h2>
|
||||
<section style={referralCodeContentStyle}>
|
||||
<p>Have you referred others to LBRY? Enter your referral code and email address below to check how many credits you've earned!</p>
|
||||
<p>As a reminder, your referral code is the same as your LBRY invitation code.</p>
|
||||
</section>
|
||||
<section>
|
||||
<section><label style={referralCodeLabelStyle} htmlFor="code">Referral code</label><input name="code" ref="code" /></section>
|
||||
<section><label style={referralCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
|
||||
</section>
|
||||
<section>
|
||||
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
|
||||
disabled={this.state.submitting} onClick={this.handleSubmit} />
|
||||
<input type='submit' className='hidden' />
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
<Modal isOpen={this.state.modal == 'referralInfo'} contentLabel="Credit earnings"
|
||||
onConfirmed={this.handleFinished}>
|
||||
{this.state.referralCredits > 0
|
||||
? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!`
|
||||
: 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
|
||||
onConfirmed={this.closeModal}>
|
||||
{this.state.failureReason}
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Couldn't confirm referral code"
|
||||
onConfirmed={this.closeModal}>
|
||||
LBRY couldn't connect to our servers to confirm your referral code. Please check your internet connection.
|
||||
</Modal>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ReferralPage;
|
|
@ -18,9 +18,6 @@ var ReportPage = React.createClass({
|
|||
this._messageArea.value = '';
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Report an Issue";
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
|
@ -34,7 +31,7 @@ var ReportPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<section className="card">
|
||||
<h3>Report an Issue</h3>
|
||||
<p>Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!</p>
|
||||
|
|
126
ui/js/page/reward.js
Normal file
126
ui/js/page/reward.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
import React from 'react';
|
||||
import lbryio from '../lbryio.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import Notice from '../component/notice.js';
|
||||
import {CreditAmount} from '../component/common.js';
|
||||
//
|
||||
// const {shell} = require('electron');
|
||||
// const querystring = require('querystring');
|
||||
//
|
||||
// const GITHUB_CLIENT_ID = '6baf581d32bad60519';
|
||||
//
|
||||
// const LinkGithubReward = React.createClass({
|
||||
// propTypes: {
|
||||
// onClaimed: React.PropTypes.func,
|
||||
// },
|
||||
// _launchLinkPage: function() {
|
||||
// /* const githubAuthParams = {
|
||||
// client_id: GITHUB_CLIENT_ID,
|
||||
// redirect_uri: 'https://lbry.io/',
|
||||
// scope: 'user:email,public_repo',
|
||||
// allow_signup: false,
|
||||
// }
|
||||
// shell.openExternal('https://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */
|
||||
// shell.openExternal('https://lbry.io');
|
||||
// },
|
||||
// handleConfirmClicked: function() {
|
||||
// this.setState({
|
||||
// confirming: true,
|
||||
// });
|
||||
//
|
||||
// lbry.get_new_address().then((address) => {
|
||||
// lbryio.call('reward', 'new', {
|
||||
// reward_type: 'new_developer',
|
||||
// access_token: '**access token here**',
|
||||
// wallet_address: address,
|
||||
// }, 'post').then((response) => {
|
||||
// console.log('response:', response);
|
||||
//
|
||||
// this.props.onClaimed(); // This will trigger another API call to show that we succeeded
|
||||
//
|
||||
// this.setState({
|
||||
// confirming: false,
|
||||
// error: null,
|
||||
// });
|
||||
// }, (error) => {
|
||||
// console.log('failed with error:', error);
|
||||
// this.setState({
|
||||
// confirming: false,
|
||||
// error: error,
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// getInitialState: function() {
|
||||
// return {
|
||||
// confirming: false,
|
||||
// error: null,
|
||||
// };
|
||||
// },
|
||||
// render: function() {
|
||||
// return (
|
||||
// <section>
|
||||
// <p><Link button="alt" label="Link with GitHub" onClick={this._launchLinkPage} /></p>
|
||||
// <section className="reward-page__details">
|
||||
// <p>This will open a browser window where you can authorize GitHub to link your account to LBRY. This will record your email (no spam) and star the LBRY repo.</p>
|
||||
// <p>Once you're finished, you may confirm you've linked the account to receive your reward.</p>
|
||||
// </section>
|
||||
// {this.state.error
|
||||
// ? <Notice isError>
|
||||
// {this.state.error.message}
|
||||
// </Notice>
|
||||
// : null}
|
||||
//
|
||||
// <Link button="primary" label={this.state.confirming ? 'Confirming...' : 'Confirm'}
|
||||
// onClick={this.handleConfirmClicked} />
|
||||
// </section>
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// const RewardPage = React.createClass({
|
||||
// propTypes: {
|
||||
// name: React.PropTypes.string.isRequired,
|
||||
// },
|
||||
// _getRewardType: function() {
|
||||
// lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => {
|
||||
// this.setState({
|
||||
// rewardType: rewardType,
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// getInitialState: function() {
|
||||
// return {
|
||||
// rewardType: null,
|
||||
// };
|
||||
// },
|
||||
// componentWillMount: function() {
|
||||
// this._getRewardType();
|
||||
// },
|
||||
// render: function() {
|
||||
// if (!this.state.rewardType) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// let Reward;
|
||||
// if (this.props.name == 'link_github') {
|
||||
// Reward = LinkGithubReward;
|
||||
// }
|
||||
//
|
||||
// const {title, description, value} = this.state.rewardType;
|
||||
// return (
|
||||
// <main>
|
||||
// <section className="card">
|
||||
// <h2>{title}</h2>
|
||||
// <CreditAmount amount={value} />
|
||||
// <p>{this.state.rewardType.claimed
|
||||
// ? <span class="empty">This reward has been claimed.</span>
|
||||
// : description}</p>
|
||||
// <Reward onClaimed={this._getRewardType} />
|
||||
// </section>
|
||||
// </main>
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// export default RewardPage;
|
74
ui/js/page/rewards.js
Normal file
74
ui/js/page/rewards.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import {CreditAmount, Icon} from '../component/common.js';
|
||||
import rewards from '../rewards.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import {WalletNav} from './wallet.js';
|
||||
import {RewardLink} from '../component/link.js';
|
||||
|
||||
const RewardTile = React.createClass({
|
||||
propTypes: {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
description: React.PropTypes.string.isRequired,
|
||||
claimed: React.PropTypes.bool.isRequired,
|
||||
value: React.PropTypes.number.isRequired,
|
||||
onRewardClaim: React.PropTypes.func
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-primary">
|
||||
<CreditAmount amount={this.props.value} />
|
||||
<h3>{this.props.title}</h3>
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
{this.props.claimed
|
||||
? <span><Icon icon="icon-check" /> Reward claimed.</span>
|
||||
: <RewardLink {...this.props} />}
|
||||
</div>
|
||||
<div className="card__content">{this.props.description}</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RewardsPage = React.createClass({
|
||||
componentWillMount: function() {
|
||||
this.loadRewards()
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
userRewards: null,
|
||||
failed: null
|
||||
};
|
||||
},
|
||||
loadRewards: function() {
|
||||
lbryio.call('reward', 'list', {}).then((userRewards) => {
|
||||
this.setState({
|
||||
userRewards: userRewards,
|
||||
});
|
||||
}, () => {
|
||||
this.setState({failed: true })
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<WalletNav viewingPage="rewards"/>
|
||||
<div>
|
||||
{!this.state.userRewards
|
||||
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
|
||||
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
|
||||
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default RewardsPage;
|
165
ui/js/page/search.js
Normal file
165
ui/js/page/search.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {BusyMessage} from '../component/common.js';
|
||||
|
||||
var SearchNoResults = React.createClass({
|
||||
render: function() {
|
||||
return <section>
|
||||
<span className="empty">
|
||||
No one has checked anything in for {this.props.query} yet.
|
||||
<Link label="Be the first" href="?publish" />
|
||||
</span>
|
||||
</section>;
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResultList = React.createClass({
|
||||
render: function() {
|
||||
var rows = [],
|
||||
seenNames = {}; //fix this when the search API returns claim IDs
|
||||
|
||||
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
|
||||
const uri = lbryuri.build({
|
||||
channelName: channel_name,
|
||||
contentName: name,
|
||||
claimId: channel_id || claim_id,
|
||||
});
|
||||
|
||||
rows.push(
|
||||
<FileTileStream key={uri} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>{rows}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let SearchResults = React.createClass({
|
||||
propTypes: {
|
||||
query: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
_isMounted: false,
|
||||
|
||||
search: function(term) {
|
||||
lighthouse.search(term).then(this.searchCallback);
|
||||
if (!this.state.searching) {
|
||||
this.setState({ searching: true })
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function () {
|
||||
this._isMounted = true;
|
||||
this.search(this.props.query);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (nextProps.query != this.props.query) {
|
||||
this.search(nextProps.query);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function () {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
results: [],
|
||||
searching: true
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function (results) {
|
||||
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
this.setState({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this.state.searching ?
|
||||
<BusyMessage message="Looking up the Dewey Decimals" /> :
|
||||
(this.state.results && this.state.results.length ?
|
||||
<SearchResultList results={this.state.results} /> :
|
||||
<SearchNoResults query={this.props.query} />);
|
||||
}
|
||||
});
|
||||
|
||||
let SearchPage = React.createClass({
|
||||
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
query: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
isValidUri: function(query) {
|
||||
try {
|
||||
lbryuri.parse(query);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._isMounted = true;
|
||||
lighthouse.search(this.props.query).then(this.searchCallback);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
results: [],
|
||||
searching: true
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function(results) {
|
||||
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
this.setState({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
{ this.isValidUri(this.props.query) ?
|
||||
<section className="section-spaced">
|
||||
<h3 className="card-row__header">
|
||||
Exact URL
|
||||
<ToolTip label="?" body="This is the resolution of a LBRY URL and not controlled by LBRY Inc." className="tooltip--header"/>
|
||||
</h3>
|
||||
<FileTile uri={this.props.query} showEmpty={true} />
|
||||
</section> : '' }
|
||||
<section className="section-spaced">
|
||||
<h3 className="card-row__header">
|
||||
Search Results for {this.props.query}
|
||||
<ToolTip label="?" body="These search results are provided by LBRY Inc." className="tooltip--header"/>
|
||||
</h3>
|
||||
<SearchResults query={this.props.query} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchPage;
|
|
@ -1,52 +1,63 @@
|
|||
import React from 'react';
|
||||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import lbry from '../lbry.js';
|
||||
|
||||
var settingsRadioOptionStyles = {
|
||||
display: 'block',
|
||||
marginLeft: '13px'
|
||||
}, settingsCheckBoxOptionStyles = {
|
||||
display: 'block',
|
||||
marginLeft: '13px'
|
||||
}, settingsNumberFieldStyles = {
|
||||
width: '40px'
|
||||
}, downloadDirectoryLabelStyles = {
|
||||
fontSize: '.9em',
|
||||
marginLeft: '13px'
|
||||
}, downloadDirectoryFieldStyles= {
|
||||
width: '300px'
|
||||
};
|
||||
export let SettingsNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?settings': 'Settings',
|
||||
'?help' : 'Help'
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
var SettingsPage = React.createClass({
|
||||
_onSettingSaveSuccess: function() {
|
||||
// This is bad.
|
||||
// document.dispatchEvent(new CustomEvent('globalNotice', {
|
||||
// detail: {
|
||||
// message: "Settings saved",
|
||||
// },
|
||||
// }))
|
||||
},
|
||||
setDaemonSetting: function(name, value) {
|
||||
lbry.setDaemonSetting(name, value, this._onSettingSaveSuccess)
|
||||
},
|
||||
setClientSetting: function(name, value) {
|
||||
lbry.setClientSetting(name, value)
|
||||
this._onSettingSaveSuccess()
|
||||
},
|
||||
onRunOnStartChange: function (event) {
|
||||
lbry.setDaemonSetting('run_on_startup', event.target.checked);
|
||||
this.setDaemonSetting('run_on_startup', event.target.checked);
|
||||
},
|
||||
onShareDataChange: function (event) {
|
||||
lbry.setDaemonSetting('share_debug_info', event.target.checked);
|
||||
this.setDaemonSetting('share_usage_data', event.target.checked);
|
||||
},
|
||||
onDownloadDirChange: function(event) {
|
||||
lbry.setDaemonSetting('download_directory', event.target.value);
|
||||
this.setDaemonSetting('download_directory', event.target.value);
|
||||
},
|
||||
onMaxUploadPrefChange: function(isLimited) {
|
||||
if (!isLimited) {
|
||||
lbry.setDaemonSetting('max_upload', 0.0);
|
||||
this.setDaemonSetting('max_upload', 0.0);
|
||||
}
|
||||
this.setState({
|
||||
isMaxUpload: isLimited
|
||||
});
|
||||
},
|
||||
onMaxUploadFieldChange: function(event) {
|
||||
lbry.setDaemonSetting('max_upload', Number(event.target.value));
|
||||
this.setDaemonSetting('max_upload', Number(event.target.value));
|
||||
},
|
||||
onMaxDownloadPrefChange: function(isLimited) {
|
||||
if (!isLimited) {
|
||||
lbry.setDaemonSetting('max_download', 0.0);
|
||||
this.setDaemonSetting('max_download', 0.0);
|
||||
}
|
||||
this.setState({
|
||||
isMaxDownload: isLimited
|
||||
});
|
||||
},
|
||||
onMaxDownloadFieldChange: function(event) {
|
||||
lbry.setDaemonSetting('max_download', Number(event.target.value));
|
||||
this.setDaemonSetting('max_download', Number(event.target.value));
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
|
@ -55,100 +66,144 @@ var SettingsPage = React.createClass({
|
|||
showUnavailable: lbry.getClientSetting('showUnavailable'),
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Settings";
|
||||
},
|
||||
componentWillMount: function() {
|
||||
lbry.getDaemonSettings(function(settings) {
|
||||
lbry.getDaemonSettings((settings) => {
|
||||
this.setState({
|
||||
daemonSettings: settings,
|
||||
isMaxUpload: settings.max_upload != 0,
|
||||
isMaxDownload: settings.max_download != 0
|
||||
});
|
||||
}.bind(this));
|
||||
});
|
||||
},
|
||||
onShowNsfwChange: function(event) {
|
||||
lbry.setClientSetting('showNsfw', event.target.checked);
|
||||
},
|
||||
onShowUnavailableChange: function(event) {
|
||||
lbry.setClientSetting('showUnavailable', event.target.checked);
|
||||
|
||||
},
|
||||
render: function() {
|
||||
if (!this.state.daemonSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>Run on Startup</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow type="checkbox"
|
||||
onChange={this.onRunOnStartChange}
|
||||
defaultChecked={this.state.daemonSettings.run_on_startup}
|
||||
label="Run LBRY automatically when I start my computer" />
|
||||
</div>
|
||||
</section>
|
||||
*/
|
||||
return (
|
||||
<main>
|
||||
<main className="main--single-column">
|
||||
<SettingsNav viewingPage="settings" />
|
||||
<section className="card">
|
||||
<h3>Run on Startup</h3>
|
||||
<label style={settingsCheckBoxOptionStyles}>
|
||||
<input type="checkbox" onChange={this.onRunOnStartChange} defaultChecked={this.state.daemonSettings.run_on_startup} /> Run LBRY automatically when I start my computer
|
||||
</label>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Download Directory</h3>
|
||||
<div className="help">Where would you like the files you download from LBRY to be saved?</div>
|
||||
<input style={downloadDirectoryFieldStyles} type="text" name="download_directory" defaultValue={this.state.daemonSettings.download_directory} onChange={this.onDownloadDirChange}/>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Bandwidth Limits</h3>
|
||||
<div className="form-row">
|
||||
<h4>Max Upload</h4>
|
||||
<label style={settingsRadioOptionStyles}>
|
||||
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxUpload}/> Unlimited
|
||||
</label>
|
||||
<label style={settingsRadioOptionStyles}>
|
||||
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxUpload}/> { this.state.isMaxUpload ? 'Up to' : 'Choose limit...' }
|
||||
<span className={ this.state.isMaxUpload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_upload} style={settingsNumberFieldStyles} onChange={this.onMaxUploadFieldChange}/> MB/s</span>
|
||||
</label>
|
||||
<div className="card__content">
|
||||
<h3>Download Directory</h3>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<h4>Max Download</h4>
|
||||
<label style={settingsRadioOptionStyles}>
|
||||
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited
|
||||
</label>
|
||||
<label style={settingsRadioOptionStyles}>
|
||||
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxDownload}/> { this.state.isMaxDownload ? 'Up to' : 'Choose limit...' }
|
||||
<span className={ this.state.isMaxDownload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_download} style={settingsNumberFieldStyles} onChange={this.onMaxDownloadFieldChange}/> MB/s</span>
|
||||
</label>
|
||||
<div className="card__content">
|
||||
<FormRow type="text"
|
||||
name="download_directory"
|
||||
defaultValue={this.state.daemonSettings.download_directory}
|
||||
helper="LBRY downloads will be saved here."
|
||||
onChange={this.onDownloadDirChange} />
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Content</h3>
|
||||
<div className="form-row">
|
||||
<label style={settingsCheckBoxOptionStyles}>
|
||||
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content
|
||||
</label>
|
||||
<div className="help">
|
||||
NSFW content may include nudity, intense sexuality, profanity, or other adult content.
|
||||
By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction.
|
||||
<div className="card__content">
|
||||
<h3>Bandwidth Limits</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="form-row__label-row"><div className="form-field__label">Max Upload</div></div>
|
||||
<FormRow type="radio"
|
||||
name="max_upload_pref"
|
||||
onChange={() => { this.onMaxUploadPrefChange(false) }}
|
||||
defaultChecked={!this.state.isMaxUpload}
|
||||
label="Unlimited" />
|
||||
<div className="form-row">
|
||||
<FormField type="radio"
|
||||
name="max_upload_pref"
|
||||
onChange={() => { this.onMaxUploadPrefChange(true) }}
|
||||
defaultChecked={this.state.isMaxUpload}
|
||||
label={ this.state.isMaxUpload ? 'Up to' : 'Choose limit...' } />
|
||||
{ this.state.isMaxUpload ?
|
||||
<FormField type="number"
|
||||
min="0"
|
||||
step=".5"
|
||||
defaultValue={this.state.daemonSettings.max_upload}
|
||||
placeholder="10"
|
||||
className="form-field__input--inline"
|
||||
onChange={this.onMaxUploadFieldChange}
|
||||
/>
|
||||
: ''
|
||||
|
||||
}
|
||||
{ this.state.isMaxUpload ? <span className="form-field__label">MB/s</span> : '' }
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="form-row__label-row"><div className="form-field__label">Max Download</div></div>
|
||||
<FormRow label="Unlimited"
|
||||
type="radio"
|
||||
name="max_download_pref"
|
||||
onChange={() => { this.onMaxDownloadPrefChange(false) }}
|
||||
defaultChecked={!this.state.isMaxDownload} />
|
||||
<div className="form-row">
|
||||
<FormField type="radio"
|
||||
name="max_download_pref"
|
||||
onChange={() => { this.onMaxDownloadPrefChange(true) }}
|
||||
defaultChecked={this.state.isMaxDownload}
|
||||
label={ this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } />
|
||||
{ this.state.isMaxDownload ?
|
||||
<FormField type="number"
|
||||
min="0"
|
||||
step=".5"
|
||||
defaultValue={this.state.daemonSettings.max_download}
|
||||
placeholder="10"
|
||||
className="form-field__input--inline"
|
||||
onChange={this.onMaxDownloadFieldChange}
|
||||
/>
|
||||
: ''
|
||||
|
||||
}
|
||||
{ this.state.isMaxDownload ? <span className="form-field__label">MB/s</span> : '' }
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Search</h3>
|
||||
<div className="form-row">
|
||||
<div className="help">
|
||||
Would you like search results to include items that are not currently available for download?
|
||||
<div className="card__content">
|
||||
<h3>Content</h3>
|
||||
</div>
|
||||
<label style={settingsCheckBoxOptionStyles}>
|
||||
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} />
|
||||
Show unavailable content in search results
|
||||
</label>
|
||||
<div className="card__content">
|
||||
<FormRow type="checkbox"
|
||||
onChange={this.onShowUnavailableChange}
|
||||
defaultChecked={this.state.showUnavailable}
|
||||
label="Show unavailable content in search results" />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow label="Show NSFW content" type="checkbox"
|
||||
onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw}
|
||||
helper="NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. " />
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h3>Share Diagnostic Data</h3>
|
||||
<label style={settingsCheckBoxOptionStyles}>
|
||||
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.share_debug_info} />
|
||||
Help make LBRY better by contributing diagnostic data about my usage
|
||||
</label>
|
||||
<div className="card__content">
|
||||
<h3>Share Diagnostic Data</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow type="checkbox"
|
||||
onChange={this.onShareDataChange}
|
||||
defaultChecked={this.state.daemonSettings.share_usage_data}
|
||||
label="Help make LBRY better by contributing diagnostic data about my usage" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default SettingsPage;
|
||||
|
|
|
@ -1,166 +1,284 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {CreditAmount, Thumbnail} from '../component/common.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Video} from '../page/watch.js'
|
||||
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
|
||||
import {FileActions} from '../component/file-actions.js';
|
||||
import {Link} from '../component/link.js';
|
||||
|
||||
var formatItemImgStyle = {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
display: 'block',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: '5px',
|
||||
};
|
||||
import UriIndicator from '../component/channel-indicator.js';
|
||||
|
||||
var FormatItem = React.createClass({
|
||||
propTypes: {
|
||||
claimInfo: React.PropTypes.object,
|
||||
cost: React.PropTypes.number,
|
||||
name: React.PropTypes.string,
|
||||
metadata: React.PropTypes.object,
|
||||
contentType: React.PropTypes.string,
|
||||
uri: React.PropTypes.string,
|
||||
outpoint: React.PropTypes.string,
|
||||
costIncludesData: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
var claimInfo = this.props.claimInfo;
|
||||
var thumbnail = claimInfo.thumbnail;
|
||||
var title = claimInfo.title;
|
||||
var description = claimInfo.description;
|
||||
var author = claimInfo.author;
|
||||
var language = claimInfo.language;
|
||||
var license = claimInfo.license;
|
||||
var fileContentType = (claimInfo.content_type || claimInfo['content-type']);
|
||||
var mediaType = lbry.getMediaType(fileContentType);
|
||||
var costIncludesData = this.props.costIncludesData;
|
||||
var cost = this.props.cost || 0.0;
|
||||
const {author, language, license} = this.props.metadata;
|
||||
|
||||
return (
|
||||
<div className="row-fluid">
|
||||
<div className="span4">
|
||||
<Thumbnail src={thumbnail} alt={'Photo for ' + title} style={formatItemImgStyle} />
|
||||
</div>
|
||||
<div className="span8">
|
||||
<p>{description}</p>
|
||||
<section>
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Content-Type</td><td>{fileContentType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cost</td><td><CreditAmount amount={cost} isEstimate={!costIncludesData}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td><td>{author}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Language</td><td>{language}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>License</td><td>{license}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<FileActions streamName={this.props.name} outpoint={this.props.outpoint} metadata={claimInfo} />
|
||||
<section>
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var FormatsSection = React.createClass({
|
||||
propTypes: {
|
||||
claimInfo: React.PropTypes.object,
|
||||
cost: React.PropTypes.number,
|
||||
name: React.PropTypes.string,
|
||||
costIncludesData: React.PropTypes.bool,
|
||||
},
|
||||
render: function() {
|
||||
var name = this.props.name;
|
||||
var format = this.props.claimInfo;
|
||||
var title = format.title;
|
||||
|
||||
if(format == null)
|
||||
{
|
||||
return (
|
||||
<div>
|
||||
<h2>Sorry, no results found for "{name}".</h2>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="meta">lbry://{name}</div>
|
||||
<h2>{title}</h2>
|
||||
{/* In future, anticipate multiple formats, just a guess at what it could look like
|
||||
// var formats = this.props.claimInfo.formats
|
||||
// return (<tbody>{formats.map(function(format,i){ */}
|
||||
<FormatItem claimInfo={format} cost={this.props.cost} name={this.props.name} costIncludesData={this.props.costIncludesData} />
|
||||
{/* })}</tbody>); */}
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
|
||||
var DetailPage = React.createClass({
|
||||
propTypes: {
|
||||
name: React.PropTypes.string,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
metadata: null,
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
nameLookupComplete: null,
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
document.title = 'lbry://' + this.props.name;
|
||||
|
||||
lbry.claim_show({name: this.props.name}, ({name, txid, nout, value}) => {
|
||||
this.setState({
|
||||
outpoint: txid + ':' + nout,
|
||||
metadata: value,
|
||||
nameLookupComplete: true,
|
||||
});
|
||||
});
|
||||
|
||||
lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
});
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
if (this.state.metadata == null) {
|
||||
if (!this.props.contentType && [author, language, license].filter((val) => {return !!val; }).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = this.props.name;
|
||||
const costIncludesData = this.state.costIncludesData;
|
||||
const metadata = this.state.metadata;
|
||||
const cost = this.state.cost;
|
||||
const outpoint = this.state.outpoint;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="card">
|
||||
{this.state.nameLookupComplete ? (
|
||||
<FormatsSection name={name} outpoint={outpoint} claimInfo={metadata} cost={cost} costIncludesData={costIncludesData} />
|
||||
) : (
|
||||
<div>
|
||||
<h2>No content</h2>
|
||||
There is no content available at the name <strong>lbry://{this.props.name}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="?report" label="report a bug" />. Thanks!
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>);
|
||||
<table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Content-Type</td><td>{this.props.contentType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td><td>{author}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Language</td><td>{language}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>License</td><td>{license}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default DetailPage;
|
||||
let ChannelPage = React.createClass({
|
||||
render: function() {
|
||||
return <main className="main--single-column">
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity"><h1>{this.props.title}</h1></div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p>
|
||||
This channel page is a stub.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
});
|
||||
|
||||
let FilePage = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
isDownloaded: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.outpoint != this.props.outpoint || nextProps.uri != this.props.uri) {
|
||||
this.loadCostAndFileState(nextProps.uri, nextProps.outpoint);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._isMounted = true;
|
||||
this.loadCostAndFileState(this.props.uri, this.props.outpoint);
|
||||
},
|
||||
|
||||
loadCostAndFileState: function(uri, outpoint) {
|
||||
lbry.file_list({outpoint: outpoint}).then((fileInfo) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
isDownloaded: fileInfo.length > 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
lbry.getCostInfo(uri).then(({cost, includesData}) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const metadata = this.props.metadata,
|
||||
title = metadata ? this.props.metadata.title : this.props.uri,
|
||||
uriIndicator = <UriIndicator uri={this.props.uri} hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />;
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<section className="show-page-media">
|
||||
{ this.props.contentType && this.props.contentType.startsWith('video/') ?
|
||||
<Video className="video-embedded" uri={this.props.uri} metadata={metadata} outpoint={this.props.outpoint} /> :
|
||||
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
|
||||
</section>
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity">
|
||||
{this.state.isDownloaded === false
|
||||
? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span>
|
||||
: null}
|
||||
<h1>{title}</h1>
|
||||
<div className="card__subtitle">
|
||||
{ this.props.channelUri ?
|
||||
<Link href={"?show=" + this.props.channelUri }>{uriIndicator}</Link> :
|
||||
uriIndicator}
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||
{metadata.description}
|
||||
</div>
|
||||
</div>
|
||||
{ metadata ?
|
||||
<div className="card__content">
|
||||
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.state.costIncludesData} />
|
||||
</div> : '' }
|
||||
<div className="card__content">
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let ShowPage = React.createClass({
|
||||
_uri: null,
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
outpoint: null,
|
||||
metadata: null,
|
||||
contentType: null,
|
||||
hasSignature: false,
|
||||
claimType: null,
|
||||
signatureIsValid: false,
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
uriLookupComplete: null,
|
||||
isFailed: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.uri != this.props.uri) {
|
||||
this.setState(this.getInitialState());
|
||||
this.loadUri(nextProps.uri);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._isMounted = true;
|
||||
this.loadUri(this.props.uri);
|
||||
},
|
||||
|
||||
loadUri: function(uri) {
|
||||
this._uri = lbryuri.normalize(uri);
|
||||
|
||||
lbry.resolve({uri: this._uri}).then((resolveData) => {
|
||||
const isChannel = resolveData && resolveData.claims_in_channel;
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
if (resolveData) {
|
||||
let newState = { uriLookupComplete: true }
|
||||
if (!isChannel) {
|
||||
let {claim: {txid: txid, nout: nout, has_signature: has_signature, signature_is_valid: signature_is_valid, value: {stream: {metadata: metadata, source: {contentType: contentType}}}}} = resolveData;
|
||||
|
||||
Object.assign(newState, {
|
||||
claimType: "file",
|
||||
metadata: metadata,
|
||||
outpoint: txid + ':' + nout,
|
||||
hasSignature: has_signature,
|
||||
signatureIsValid: signature_is_valid,
|
||||
contentType: contentType
|
||||
});
|
||||
|
||||
|
||||
lbry.setTitle(metadata.title ? metadata.title : this._uri)
|
||||
|
||||
} else {
|
||||
let {certificate: {txid: txid, nout: nout, has_signature: has_signature}} = resolveData;
|
||||
Object.assign(newState, {
|
||||
claimType: "channel",
|
||||
outpoint: txid + ':' + nout,
|
||||
txid: txid,
|
||||
metadata: {
|
||||
title:resolveData.certificate.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
} else {
|
||||
this.setState(Object.assign({}, this.getInitialState(), {
|
||||
uriLookupComplete: true,
|
||||
isFailed: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const metadata = this.state.metadata,
|
||||
title = metadata ? this.state.metadata.title : this._uri;
|
||||
|
||||
let innerContent = "";
|
||||
|
||||
if (!this.state.uriLookupComplete || this.state.isFailed) {
|
||||
innerContent = <section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity"><h1>{title}</h1></div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{ this.state.uriLookupComplete ?
|
||||
<p>This location is not yet in use. { ' ' }<Link href="?publish" label="Put something here" />.</p> :
|
||||
<BusyMessage message="Loading magic decentralized data..." />
|
||||
}
|
||||
</div>
|
||||
</section>;
|
||||
} else if (this.state.claimType == "channel") {
|
||||
innerContent = <ChannelPage title={this._uri} />
|
||||
} else {
|
||||
let channelUriObj = lbryuri.parse(this._uri)
|
||||
delete channelUriObj.path;
|
||||
delete channelUriObj.contentName;
|
||||
const channelUri = this.state.signatureIsValid && this.state.hasSignature && channelUriObj.isChannel ? lbryuri.build(channelUriObj, false) : null;
|
||||
innerContent = <FilePage
|
||||
uri={this._uri}
|
||||
channelUri={channelUri}
|
||||
outpoint={this.state.outpoint}
|
||||
metadata={metadata}
|
||||
contentType={this.state.contentType}
|
||||
hasSignature={this.state.hasSignature}
|
||||
signatureIsValid={this.state.signatureIsValid}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <main className="main--single-column">{innerContent}</main>;
|
||||
}
|
||||
});
|
||||
|
||||
export default ShowPage;
|
||||
|
|
|
@ -5,12 +5,9 @@ var StartPage = React.createClass({
|
|||
componentWillMount: function() {
|
||||
lbry.stop();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "LBRY is Closed";
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<h3>LBRY is Closed</h3>
|
||||
<Link href="lbry://lbry" label="Click here to start LBRY" />
|
||||
</main>
|
||||
|
|
|
@ -2,12 +2,10 @@ import React from 'react';
|
|||
import lbry from '../lbry.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
|
||||
|
||||
|
||||
var addressRefreshButtonStyle = {
|
||||
fontSize: '11pt',
|
||||
};
|
||||
var AddressSection = React.createClass({
|
||||
_refreshAddress: function(event) {
|
||||
if (typeof event !== 'undefined') {
|
||||
|
@ -60,12 +58,20 @@ var AddressSection = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<section className="card">
|
||||
<h3>Wallet Address</h3>
|
||||
<Address address={this.state.address} /> <Link text="Get new address" icon='icon-refresh' onClick={this._getNewAddress} style={addressRefreshButtonStyle} />
|
||||
<input type='submit' className='hidden' />
|
||||
<div className="help">
|
||||
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
|
||||
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.
|
||||
<div className="card__title-primary">
|
||||
<h3>Wallet Address</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<Address address={this.state.address} />
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="help">
|
||||
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
|
||||
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -143,27 +149,26 @@ var SendToAddressSection = React.createClass({
|
|||
return (
|
||||
<section className="card">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<h3>Send Credits</h3>
|
||||
<div className="form-row">
|
||||
<label htmlFor="amount">Amount</label>
|
||||
<input id="amount" type="text" size="10" onChange={this.setAmount}></input>
|
||||
<div className="card__title-primary">
|
||||
<h3>Send Credits</h3>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="address">Recipient address</label>
|
||||
<input id="address" type="text" size="60" onChange={this.setAddress}></input>
|
||||
<div className="card__content">
|
||||
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
|
||||
</div>
|
||||
<div className="form-row form-row-submit">
|
||||
<div className="card__content">
|
||||
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
|
||||
</div>
|
||||
<div className="card__actions card__actions--form-submit">
|
||||
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
|
||||
<input type='submit' className='hidden' />
|
||||
</div>
|
||||
{
|
||||
this.state.results ?
|
||||
<div className="form-row">
|
||||
<h4>Results</h4>
|
||||
{this.state.results}
|
||||
</div>
|
||||
: ''
|
||||
}
|
||||
{
|
||||
this.state.results ?
|
||||
<div className="card__content">
|
||||
<h4>Results</h4>
|
||||
{this.state.results}
|
||||
</div> : ''
|
||||
}
|
||||
</form>
|
||||
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
|
||||
onConfirmed={this.closeModal}>
|
||||
|
@ -231,30 +236,44 @@ var TransactionList = React.createClass({
|
|||
}
|
||||
return (
|
||||
<section className="card">
|
||||
<h3>Transaction History</h3>
|
||||
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
|
||||
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
|
||||
{ this.state.transactionItems && rows.length > 0 ?
|
||||
<table className="table-standard table-stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Transaction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="card__title-primary">
|
||||
<h3>Transaction History</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
|
||||
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
|
||||
{ this.state.transactionItems && rows.length > 0 ?
|
||||
<table className="table-standard table-stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Transaction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
: ''
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export let WalletNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?wallet': 'Overview',
|
||||
'?send': 'Send',
|
||||
'?receive': 'Receive',
|
||||
'?rewards': 'Rewards'
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
var WalletPage = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
|
@ -262,9 +281,6 @@ var WalletPage = React.createClass({
|
|||
propTypes: {
|
||||
viewingPage: React.PropTypes.string,
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "My Wallet";
|
||||
},
|
||||
/*
|
||||
Below should be refactored so that balance is shared all of wallet page. Or even broader?
|
||||
What is the proper React pattern for sharing a global state like balance?
|
||||
|
@ -288,11 +304,16 @@ var WalletPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<WalletNav viewingPage={this.props.viewingPage} />
|
||||
<section className="card">
|
||||
<h3>Balance</h3>
|
||||
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
|
||||
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
|
||||
<div className="card__title-primary">
|
||||
<h3>Balance</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
|
||||
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
|
||||
</div>
|
||||
</section>
|
||||
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
|
||||
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }
|
||||
|
|
|
@ -1,39 +1,136 @@
|
|||
import React from 'react';
|
||||
import {Icon} from '../component/common.js';
|
||||
import {Icon, Thumbnail, FilePrice} from '../component/common.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import lbry from '../lbry.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import rewards from '../rewards.js';
|
||||
import LoadScreen from '../component/load_screen.js'
|
||||
|
||||
const fs = require('fs');
|
||||
const VideoStream = require('videostream');
|
||||
|
||||
export let WatchLink = React.createClass({
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
metadata: React.PropTypes.object,
|
||||
downloadStarted: React.PropTypes.bool,
|
||||
onGet: React.PropTypes.func,
|
||||
},
|
||||
getInitialState: function() {
|
||||
affirmedPurchase: false
|
||||
},
|
||||
play: function() {
|
||||
lbry.get({uri: this.props.uri}).then((streamInfo) => {
|
||||
if (streamInfo === null || typeof streamInfo !== 'object') {
|
||||
this.setState({
|
||||
modal: 'timedOut',
|
||||
attemptingDownload: false,
|
||||
});
|
||||
}
|
||||
|
||||
var WatchPage = React.createClass({
|
||||
lbryio.call('file', 'view', {
|
||||
uri: this.props.uri,
|
||||
outpoint: streamInfo.outpoint,
|
||||
claimId: streamInfo.claim_id
|
||||
}).catch(() => {})
|
||||
});
|
||||
if (this.props.onGet) {
|
||||
this.props.onGet()
|
||||
}
|
||||
},
|
||||
onWatchClick: function() {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
lbry.getCostInfo(this.props.uri).then(({cost}) => {
|
||||
lbry.getBalance((balance) => {
|
||||
if (cost > balance) {
|
||||
this.setState({
|
||||
modal: 'notEnoughCredits',
|
||||
attemptingDownload: false,
|
||||
});
|
||||
} else if (cost <= 0.01) {
|
||||
this.play()
|
||||
} else {
|
||||
lbry.file_list({outpoint: this.props.outpoint}).then((fileInfo) => {
|
||||
if (fileInfo) { // Already downloaded
|
||||
this.play();
|
||||
} else {
|
||||
this.setState({
|
||||
modal: 'affirmPurchase'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
modal: null,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
loading: false,
|
||||
modal: null,
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return (<div>
|
||||
<Link button={ this.props.button ? this.props.button : null }
|
||||
disabled={this.state.loading}
|
||||
label={this.props.label ? this.props.label : ""}
|
||||
className={this.props.className}
|
||||
icon="icon-play"
|
||||
onClick={this.onWatchClick} />
|
||||
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
|
||||
You don't have enough LBRY credits to pay for this stream.
|
||||
</Modal>
|
||||
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
|
||||
contentLabel="Confirm Purchase" onConfirmed={this.play} onAborted={this.closeModal}>
|
||||
Are you sure you'd like to buy <strong>{this.props.metadata.title}</strong> for <strong><FilePrice uri={this.props.uri} metadata={this.props.metadata} label={false} look="plain" /></strong> credits?
|
||||
</Modal>
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export let Video = React.createClass({
|
||||
_isMounted: false,
|
||||
_controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
|
||||
_controlsHideTimeout: null,
|
||||
_outpoint: null,
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string,
|
||||
uri: React.PropTypes.string.isRequired,
|
||||
metadata: React.PropTypes.object,
|
||||
outpoint: React.PropTypes.string,
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
downloadStarted: false,
|
||||
readyToPlay: false,
|
||||
loadStatusMessage: "Requesting stream",
|
||||
isPlaying: false,
|
||||
isPurchased: false,
|
||||
loadStatusMessage: "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it",
|
||||
mimeType: null,
|
||||
controlsShown: false,
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
lbry.get({name: this.props.name}).then((fileInfo) => {
|
||||
this._outpoint = fileInfo.outpoint;
|
||||
onGet: function() {
|
||||
lbry.get({uri: this.props.uri}).then((fileInfo) => {
|
||||
this.updateLoadStatus();
|
||||
});
|
||||
this.setState({
|
||||
isPlaying: true
|
||||
})
|
||||
},
|
||||
handleBackClicked: function() {
|
||||
history.back();
|
||||
componentDidMount: function() {
|
||||
if (this.props.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
handleMouseMove: function() {
|
||||
if (this._controlsTimeout) {
|
||||
|
@ -68,7 +165,7 @@ var WatchPage = React.createClass({
|
|||
},
|
||||
updateLoadStatus: function() {
|
||||
lbry.file_list({
|
||||
outpoint: this._outpoint,
|
||||
outpoint: this.props.outpoint,
|
||||
full_status: true,
|
||||
}).then(([status]) => {
|
||||
if (!status || status.written_bytes == 0) {
|
||||
|
@ -93,6 +190,9 @@ var WatchPage = React.createClass({
|
|||
return fs.createReadStream(status.download_path, opts)
|
||||
}
|
||||
};
|
||||
|
||||
rewards.claimNextPurchaseReward()
|
||||
|
||||
var elem = this.refs.video;
|
||||
var videostream = VideoStream(mediaFile, elem);
|
||||
elem.play();
|
||||
|
@ -101,26 +201,15 @@ var WatchPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
!this.state.readyToPlay
|
||||
? <LoadScreen message={'Loading video...'} details={this.state.loadStatusMessage} />
|
||||
: <main className="video full-screen" onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
|
||||
<video controls width="100%" height="100%" id="video" ref="video"></video>
|
||||
{this.state.controlsShown
|
||||
? <div className="video__overlay">
|
||||
<div className="video__back">
|
||||
<Link icon="icon-arrow-circle-o-left" className="video__back-link" onClick={this.handleBackClicked}/>
|
||||
<div className="video__back-label">
|
||||
<Icon icon="icon-caret-left" className="video__back-label-arrow" />
|
||||
<div className="video__back-label-content">
|
||||
Back to LBRY
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</main>
|
||||
<div className={"video " + this.props.className + (this.state.isPlaying && this.state.readyToPlay ? " video--active" : " video--hidden")}>{
|
||||
this.state.isPlaying ?
|
||||
!this.state.readyToPlay ?
|
||||
<span>this is the world's worst loading screen and we shipped our software with it anyway... <br/><br/>{this.state.loadStatusMessage}</span> :
|
||||
<video controls id="video" ref="video"></video> :
|
||||
<div className="video__cover" style={{backgroundImage: 'url("' + this.props.metadata.thumbnail + '")'}}>
|
||||
<WatchLink className="video__play-button" uri={this.props.uri} metadata={this.props.metadata} outpoint={this.props.outpoint} onGet={this.onGet} icon="icon-play"></WatchLink>
|
||||
</div>
|
||||
}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default WatchPage;
|
||||
})
|
||||
|
|
124
ui/js/rewards.js
Normal file
124
ui/js/rewards.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
import lbry from './lbry.js';
|
||||
import lbryio from './lbryio.js';
|
||||
|
||||
function rewardMessage(type, amount) {
|
||||
return {
|
||||
new_developer: `You earned ${amount} for registering as a new developer.`,
|
||||
new_user: `You earned ${amount} LBC new user reward.`,
|
||||
confirm_email: `You earned ${amount} LBC for verifying your email address.`,
|
||||
new_channel: `You earned ${amount} LBC for creating a publisher identity.`,
|
||||
first_stream: `You earned ${amount} LBC for streaming your first video.`,
|
||||
many_downloads: `You earned ${amount} LBC for downloading some of the things.`,
|
||||
first_publish: `You earned ${amount} LBC for making your first publication.`,
|
||||
}[type];
|
||||
}
|
||||
|
||||
const rewards = {};
|
||||
|
||||
rewards.TYPE_NEW_DEVELOPER = "new_developer",
|
||||
rewards.TYPE_NEW_USER = "new_user",
|
||||
rewards.TYPE_CONFIRM_EMAIL = "confirm_email",
|
||||
rewards.TYPE_FIRST_CHANNEL = "new_channel",
|
||||
rewards.TYPE_FIRST_STREAM = "first_stream",
|
||||
rewards.TYPE_MANY_DOWNLOADS = "many_downloads",
|
||||
rewards.TYPE_FIRST_PUBLISH = "first_publish";
|
||||
|
||||
rewards.claimReward = function (type) {
|
||||
|
||||
function requestReward(resolve, reject, params) {
|
||||
if (!lbryio.enabled) {
|
||||
reject(new Error("Rewards are not enabled."))
|
||||
return;
|
||||
}
|
||||
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
|
||||
const
|
||||
message = rewardMessage(type, RewardAmount),
|
||||
result = {
|
||||
type: type,
|
||||
amount: RewardAmount,
|
||||
message: message
|
||||
};
|
||||
|
||||
// Display global notice
|
||||
document.dispatchEvent(new CustomEvent('globalNotice', {
|
||||
detail: {
|
||||
message: message,
|
||||
linkText: "Show All",
|
||||
linkTarget: "?rewards",
|
||||
isError: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add more events here to display other places
|
||||
|
||||
resolve(result);
|
||||
}, reject);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry.wallet_new_address().then((address) => {
|
||||
const params = {
|
||||
reward_type: type,
|
||||
wallet_address: address,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case rewards.TYPE_FIRST_CHANNEL:
|
||||
lbry.claim_list_mine().then(function(claims) {
|
||||
let claim = claims.find(function(claim) {
|
||||
return claim.name.length && claim.name[0] == '@' && claim.txid.length
|
||||
})
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid;
|
||||
requestReward(resolve, reject, params)
|
||||
} else {
|
||||
reject(new Error("Please create a channel identity first."))
|
||||
}
|
||||
}).catch(reject)
|
||||
break;
|
||||
|
||||
case rewards.TYPE_FIRST_PUBLISH:
|
||||
lbry.claim_list_mine().then((claims) => {
|
||||
let claim = claims.find(function(claim) {
|
||||
return claim.name.length && claim.name[0] != '@' && claim.txid.length
|
||||
})
|
||||
if (claim) {
|
||||
params.transaction_id = claim.txid
|
||||
requestReward(resolve, reject, params)
|
||||
} else {
|
||||
reject(claims.length ?
|
||||
new Error("Please publish something and wait for confirmation by the network to claim this reward.") :
|
||||
new Error("Please publish something to claim this reward."))
|
||||
}
|
||||
}).catch(reject)
|
||||
break;
|
||||
|
||||
case rewards.TYPE_FIRST_STREAM:
|
||||
case rewards.TYPE_NEW_USER:
|
||||
default:
|
||||
requestReward(resolve, reject, params);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
rewards.claimNextPurchaseReward = function() {
|
||||
let types = {}
|
||||
types[rewards.TYPE_FIRST_STREAM] = false
|
||||
types[rewards.TYPE_MANY_DOWNLOADS] = false
|
||||
lbryio.call('reward', 'list', {}).then((userRewards) => {
|
||||
userRewards.forEach((reward) => {
|
||||
if (types[reward.RewardType] === false && reward.TransactionID) {
|
||||
types[reward.RewardType] = true
|
||||
}
|
||||
})
|
||||
let unclaimedType = Object.keys(types).find((type) => {
|
||||
return types[type] === false;
|
||||
})
|
||||
if (unclaimedType) {
|
||||
rewards.claimReward(unclaimedType);
|
||||
}
|
||||
}, () => { });
|
||||
}
|
||||
|
||||
export default rewards;
|
118
ui/js/uri.js
118
ui/js/uri.js
|
@ -1,118 +0,0 @@
|
|||
const CHANNEL_NAME_MIN_LEN = 4;
|
||||
const CLAIM_ID_MAX_LEN = 40;
|
||||
|
||||
const uri = {};
|
||||
|
||||
/**
|
||||
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||
* messages for invalid names.
|
||||
*
|
||||
* Returns a dictionary with keys:
|
||||
* - name (string)
|
||||
* - properName (string; strips off @ for channels)
|
||||
* - isChannel (boolean)
|
||||
* - claimSequence (int, if present)
|
||||
* - bidPosition (int, if present)
|
||||
* - claimId (string, if present)
|
||||
* - path (string, if persent)
|
||||
*/
|
||||
uri.parseLbryUri = function(lbryUri, requireProto=false) {
|
||||
// Break into components. Empty sub-matches are converted to null
|
||||
const componentsRegex = new RegExp(
|
||||
'^((?:lbry:\/\/)?)' + // protocol
|
||||
'([^:$#/]*)' + // name (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
|
||||
'(/?)(.*)' // path separator, path
|
||||
);
|
||||
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(lbryUri).slice(1).map(match => match || null);
|
||||
|
||||
// Validate protocol
|
||||
if (requireProto && !proto) {
|
||||
throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
|
||||
}
|
||||
|
||||
// Validate and process name
|
||||
if (!name) {
|
||||
throw new Error('URI does not include name.');
|
||||
}
|
||||
|
||||
const isChannel = name[0] == '@';
|
||||
const properName = isChannel ? name.substr(1) : name;
|
||||
|
||||
if (isChannel) {
|
||||
if (!properName) {
|
||||
throw new Error('No channel name after @.');
|
||||
}
|
||||
|
||||
if (properName.length < CHANNEL_NAME_MIN_LEN) {
|
||||
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
|
||||
}
|
||||
}
|
||||
|
||||
const nameBadChars = properName.match(/[^A-Za-z0-9-]/g);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
|
||||
}
|
||||
|
||||
// Validate and process modifier (claim ID, bid position or claim sequence)
|
||||
let claimId, claimSequence, bidPosition;
|
||||
if (modSep) {
|
||||
if (!modVal) {
|
||||
throw new Error(`No modifier provided after separator ${modSep}.`);
|
||||
}
|
||||
|
||||
if (modSep == '#') {
|
||||
claimId = modVal;
|
||||
} else if (modSep == ':') {
|
||||
claimSequence = modVal;
|
||||
} else if (modSep == '$') {
|
||||
bidPosition = modVal;
|
||||
}
|
||||
}
|
||||
|
||||
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) {
|
||||
throw new Error(`Invalid claim ID ${claimId}.`);
|
||||
}
|
||||
|
||||
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]+$/)) {
|
||||
throw new Error('Bid position must be a number.');
|
||||
}
|
||||
|
||||
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]+$/)) {
|
||||
throw new Error('Claim sequence must be a number.');
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if (path) {
|
||||
if (!isChannel) {
|
||||
throw new Error('Only channel URIs may have a path.');
|
||||
}
|
||||
|
||||
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
|
||||
if (pathBadChars) {
|
||||
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
|
||||
}
|
||||
} else if (pathSep) {
|
||||
throw new Error('No path provided after /');
|
||||
}
|
||||
|
||||
return {
|
||||
name, properName, isChannel,
|
||||
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
|
||||
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
|
||||
... claimId ? {claimId} : {},
|
||||
... path ? {path} : {},
|
||||
};
|
||||
}
|
||||
|
||||
uri.buildLbryUri = function(uriObj, includeProto=true) {
|
||||
const {name, claimId, claimSequence, bidPosition, path} = uriObj;
|
||||
|
||||
return (includeProto ? 'lbry://' : '') + name +
|
||||
(claimId ? `#${claimId}` : '') +
|
||||
(claimSequence ? `:${claimSequence}` : '') +
|
||||
(bidPosition ? `\$${bidPosition}` : '') +
|
||||
(path ? `/${path}` : '');
|
||||
}
|
||||
|
||||
export default uri;
|
|
@ -2,9 +2,9 @@
|
|||
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
|
||||
* is not set yet.
|
||||
*/
|
||||
export function getLocal(key) {
|
||||
export function getLocal(key, fallback=undefined) {
|
||||
const itemRaw = localStorage.getItem(key);
|
||||
return itemRaw === null ? undefined : JSON.parse(itemRaw);
|
||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,3 +13,19 @@ export function getLocal(key) {
|
|||
export function setLocal(key, value) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
|
||||
* is not set yet.
|
||||
*/
|
||||
export function getSession(key, fallback=undefined) {
|
||||
const itemRaw = sessionStorage.getItem(key);
|
||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin wrapper around localStorage.setItem(). Converts value to JSON.
|
||||
*/
|
||||
export function setSession(key, value) {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "lbry-web-ui",
|
||||
"version": "0.9.2rc15",
|
||||
"version": "0.10.0",
|
||||
"description": "LBRY UI",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
@ -21,7 +21,6 @@
|
|||
"babel-cli": "^6.11.4",
|
||||
"babel-preset-es2015": "^6.13.2",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"clamp-js-main": "^0.11.1",
|
||||
"mediaelement": "^2.23.4",
|
||||
"node-sass": "^3.8.0",
|
||||
"rc-progress": "^2.0.6",
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
@import "global";
|
||||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
font-size: $font-size;
|
||||
}
|
||||
body
|
||||
{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: $font-line-height;
|
||||
}
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
#drawer
|
||||
{
|
||||
width: $drawer-width;
|
||||
position: fixed;
|
||||
min-height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: $color-bg;
|
||||
z-index: 3;
|
||||
.drawer-item
|
||||
{
|
||||
display: block;
|
||||
padding: $spacing-vertical / 2;
|
||||
font-size: 1.2em;
|
||||
height: $spacing-vertical * 1.5;
|
||||
.icon
|
||||
{
|
||||
margin-right: 6px;
|
||||
}
|
||||
.link-label
|
||||
{
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
}
|
||||
.badge
|
||||
{
|
||||
float: right;
|
||||
background: $color-money;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
margin-top: $spacing-vertical * 0.25 - 2;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.drawer-item-selected
|
||||
{
|
||||
background: $color-canvas;
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
#drawer-handle
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
max-height: $height-header - $spacing-vertical;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#window.drawer-closed
|
||||
{
|
||||
#drawer { display: none }
|
||||
}
|
||||
#window.drawer-open
|
||||
{
|
||||
#main-content { margin-left: $drawer-width; }
|
||||
.open-drawer-link { display: none }
|
||||
#header { padding-left: $drawer-width + $spacing-vertical / 2; }
|
||||
}
|
||||
|
||||
#header
|
||||
{
|
||||
background: $color-primary;
|
||||
color: white;
|
||||
&.header-no-subnav {
|
||||
height: $height-header;
|
||||
}
|
||||
&.header-with-subnav {
|
||||
height: $height-header * 2;
|
||||
}
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; }
|
||||
&.header-scrolled
|
||||
{
|
||||
box-shadow: $default-box-shadow;
|
||||
}
|
||||
}
|
||||
.header-top-bar
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
}
|
||||
.header-search
|
||||
{
|
||||
margin-left: 60px;
|
||||
text-align: center;
|
||||
input[type="search"] {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
width: 400px;
|
||||
@include placeholder-color(#e8e8e8);
|
||||
}
|
||||
}
|
||||
|
||||
nav.sub-header
|
||||
{
|
||||
background: $color-primary;
|
||||
text-transform: uppercase;
|
||||
padding: $spacing-vertical / 2;
|
||||
> a
|
||||
{
|
||||
$sub-header-selected-underline-height: 2px;
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
padding: 0 5px;
|
||||
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
|
||||
color: #e8e8e8;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child
|
||||
{
|
||||
margin-right: 0;
|
||||
}
|
||||
&.sub-header-selected
|
||||
{
|
||||
border-bottom: $sub-header-selected-underline-height solid #fff;
|
||||
color: #fff;
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#main-content
|
||||
{
|
||||
background: $color-canvas;
|
||||
&.no-sub-nav
|
||||
{
|
||||
min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $height-header; }
|
||||
}
|
||||
&.with-sub-nav
|
||||
{
|
||||
min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $height-header * 2; }
|
||||
}
|
||||
main
|
||||
{
|
||||
padding: $spacing-vertical;
|
||||
}
|
||||
h2
|
||||
{
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
h3, h4
|
||||
{
|
||||
margin-bottom: $spacing-vertical / 2;
|
||||
margin-top: $spacing-vertical;
|
||||
&:first-child
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.meta
|
||||
{
|
||||
+ h2, + h3, + h4
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$header-icon-size: 1.5em;
|
||||
|
||||
.open-drawer-link, .close-drawer-link
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: $header-icon-size;
|
||||
padding: 2px 6px 0 6px;
|
||||
float: left;
|
||||
}
|
||||
.close-lbry-link
|
||||
{
|
||||
font-size: $header-icon-size;
|
||||
float: right;
|
||||
padding: 0 6px 0 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
padding: $spacing-vertical;
|
||||
background: $color-bg;
|
||||
box-shadow: $default-box-shadow;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.card-obscured
|
||||
{
|
||||
position: relative;
|
||||
}
|
||||
.card-obscured .card-content {
|
||||
-webkit-filter: blur($blur-intensity);
|
||||
-moz-filter: blur($blur-intensity);
|
||||
-o-filter: blur($blur-intensity);
|
||||
-ms-filter: blur($blur-intensity);
|
||||
filter: blur($blur-intensity);
|
||||
}
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
padding: 20px;
|
||||
background-color: rgba(128, 128, 128, 0.8);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-series-submit
|
||||
{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 800px;
|
||||
padding: $spacing-vertical / 2;
|
||||
}
|
||||
|
||||
.full-screen
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -6,17 +6,20 @@ $padding-button: 12px;
|
|||
$padding-text-link: 4px;
|
||||
|
||||
$color-primary: #155B4A;
|
||||
$color-primary-light: saturate(lighten($color-primary, 50%), 20%);
|
||||
$color-light-alt: hsl(hue($color-primary), 15, 85);
|
||||
$color-text-dark: #000;
|
||||
$color-black-transparent: rgba(32,32,32,0.9);
|
||||
$color-help: rgba(0,0,0,.6);
|
||||
$color-notice: #921010;
|
||||
$color-warning: #ffffff;
|
||||
$color-notice: #8a6d3b;
|
||||
$color-error: #a94442;
|
||||
$color-load-screen-text: #c3c3c3;
|
||||
$color-canvas: #f5f5f5;
|
||||
$color-bg: #ffffff;
|
||||
$color-bg-alt: #D9D9D9;
|
||||
$color-money: #216C2A;
|
||||
$color-meta-light: #505050;
|
||||
$color-form-border: rgba(160,160,160,.5);
|
||||
|
||||
$font-size: 16px;
|
||||
$font-line-height: 1.3333;
|
||||
|
@ -25,10 +28,16 @@ $mobile-width-threshold: 801px;
|
|||
$max-content-width: 1000px;
|
||||
$max-text-width: 660px;
|
||||
|
||||
$width-page-constrained: 800px;
|
||||
|
||||
$height-header: $spacing-vertical * 2.5;
|
||||
$height-button: $spacing-vertical * 1.5;
|
||||
$height-video-embedded: $width-page-constrained * 9 / 16;
|
||||
|
||||
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
|
||||
$box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
|
||||
$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
|
||||
|
||||
$transition-standard: .225s ease;
|
||||
|
||||
$blur-intensity: 8px;
|
||||
|
||||
|
@ -151,4 +160,35 @@ $blur-intensity: 8px;
|
|||
width:1px;
|
||||
height:1px;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
|
||||
.icon
|
||||
{
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
&:last-child:not(:only-child) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.no-underline) {
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
opacity: $hover-opacity;
|
||||
transition: opacity $transition-standard;
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
color: $color;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
@import "global";
|
||||
|
||||
$gutter_fluid: 4;
|
||||
|
||||
[class*="span"] {
|
||||
min-height: 1px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.span12 { width: 100%; }
|
||||
.span11 { width: 91.666%; }
|
||||
.span10 { width: 83.333%; }
|
||||
.span9 { width: 75%; }
|
||||
.span8 { width: 66.666%; }
|
||||
.span7 { width: 58.333%; }
|
||||
.span6 { width: 50%; }
|
||||
.span5 { width: 41.666%; }
|
||||
.span4 { width: 33.333%; }
|
||||
.span3 { width: 25%; }
|
||||
.span2 { width: 16.666%; }
|
||||
.span1 { width: 8.333%; }
|
||||
|
||||
.row-fluid {
|
||||
width: 100%;
|
||||
> [class*="span"] {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-left: 1% * $gutter_fluid;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
$column_width: (100% - $gutter_fluid * 11) / 12;
|
||||
|
||||
> .span12 { width: $column_width * 12 + $gutter_fluid * 11; }
|
||||
> .span11 { width: $column_width * 11 + $gutter_fluid * 10; }
|
||||
> .span10 { width: $column_width * 10 + $gutter_fluid * 9; }
|
||||
> .span9 { width: $column_width * 9 + $gutter_fluid * 8; }
|
||||
> .span8 { width: $column_width * 8 + $gutter_fluid * 7; }
|
||||
> .span7 { width: $column_width * 7 + $gutter_fluid * 6; }
|
||||
> .span6 { width: $column_width * 6 + $gutter_fluid * 5; }
|
||||
> .span5 { width: $column_width * 5 + $gutter_fluid * 4; }
|
||||
> .span4 { width: $column_width * 4 + $gutter_fluid * 3; }
|
||||
> .span3 { width: $column_width * 3 + $gutter_fluid * 2; }
|
||||
> .span2 { width: $column_width * 2 + $gutter_fluid * 1; }
|
||||
> .span1 { width: $column_width; }
|
||||
}
|
||||
|
||||
.tile-fluid {
|
||||
width: 100%;
|
||||
> [class*="span"] {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.column-fluid {
|
||||
@include display-flex();
|
||||
flex-wrap: wrap;
|
||||
> [class*="span"] {
|
||||
@include display-flex();
|
||||
@include flex(1 0 auto);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.row-fluid, .tile-fluid {
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-width-threshold) {
|
||||
.row-fluid, .tile-fluid, .column-fluid {
|
||||
width: 100%;
|
||||
}
|
||||
.pull-left, .pull-right
|
||||
{
|
||||
float: none;
|
||||
}
|
||||
[class*="span"] {
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
|
@ -1,35 +1,51 @@
|
|||
@import "global";
|
||||
|
||||
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
font-size: $font-size;
|
||||
}
|
||||
body
|
||||
{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: $font-line-height;
|
||||
}
|
||||
|
||||
.icon
|
||||
#window
|
||||
{
|
||||
min-height: 100vh;
|
||||
background: $color-canvas;
|
||||
}
|
||||
|
||||
.badge
|
||||
{
|
||||
background: $color-money;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.credit-amount--indicator
|
||||
{
|
||||
font-weight: bold;
|
||||
color: $color-money;
|
||||
}
|
||||
|
||||
#main-content
|
||||
{
|
||||
padding: $spacing-vertical;
|
||||
margin-top: $height-header;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
main.main--single-column
|
||||
{
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
&:last-child:not(:only-child) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
width: $width-page-constrained;
|
||||
}
|
||||
|
||||
&:not(.no-underline) {
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
opacity: $hover-opacity;
|
||||
transition: opacity .225s ease;
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
color: $color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-fixed-width {
|
||||
|
@ -38,26 +54,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
section
|
||||
{
|
||||
margin-bottom: $spacing-vertical;
|
||||
&:last-child
|
||||
{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:only-child {
|
||||
/* If it's an only child, assume it's part of a React layout that will handle the last child condition on its own */
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
main h1 {
|
||||
font-size: 2.0em;
|
||||
margin-bottom: $spacing-vertical;
|
||||
margin-top: $spacing-vertical*2;
|
||||
font-family: 'Raleway', sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
@ -76,11 +72,6 @@ sup, sub {
|
|||
sup { top: -0.4em; }
|
||||
sub { top: 0.4em; }
|
||||
|
||||
label {
|
||||
cursor: default;
|
||||
display: block;
|
||||
}
|
||||
|
||||
code {
|
||||
font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace;
|
||||
background-color: #eee;
|
||||
|
@ -104,25 +95,11 @@ p
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="search"], textarea
|
||||
{
|
||||
@include placeholder {
|
||||
color: lighten($color-text-dark, 60%);
|
||||
}
|
||||
border: 2px solid rgba(160,160,160,.5);
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
input[type="text"], input[type="search"]
|
||||
{
|
||||
line-height: $spacing-vertical - 4;
|
||||
height: $spacing-vertical * 1.5;
|
||||
}
|
||||
|
||||
.truncated-text {
|
||||
display: inline-block;
|
||||
//display: inline-block;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.busy-indicator
|
||||
|
@ -144,75 +121,6 @@ input[type="text"], input[type="search"]
|
|||
}
|
||||
}
|
||||
|
||||
.button-set-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
+ .button-set-item
|
||||
{
|
||||
margin-left: $padding-button;
|
||||
}
|
||||
}
|
||||
|
||||
.button-block, .faux-button-block
|
||||
{
|
||||
display: inline-block;
|
||||
height: $height-button;
|
||||
line-height: $height-button;
|
||||
text-decoration: none;
|
||||
border: 0 none;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
.icon
|
||||
{
|
||||
top: 0em;
|
||||
}
|
||||
.icon:first-child
|
||||
{
|
||||
padding-right: 5px;
|
||||
}
|
||||
.icon:last-child
|
||||
{
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
.button-block
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-button;
|
||||
}
|
||||
|
||||
.button-primary
|
||||
{
|
||||
color: white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: $default-box-shadow;
|
||||
}
|
||||
.button-alt
|
||||
{
|
||||
background-color: $color-bg-alt;
|
||||
box-shadow: $default-box-shadow;
|
||||
}
|
||||
|
||||
.button-text
|
||||
{
|
||||
@include text-link();
|
||||
display: inline-block;
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-text-link;
|
||||
}
|
||||
}
|
||||
.button-text-help
|
||||
{
|
||||
@include text-link(#aaa);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.icon:only-child {
|
||||
position: relative;
|
||||
top: 0.16em;
|
||||
|
@ -235,169 +143,17 @@ input[type="text"], input[type="search"]
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-row
|
||||
{
|
||||
+ .form-row
|
||||
{
|
||||
margin-top: $spacing-vertical / 2;
|
||||
}
|
||||
.help
|
||||
{
|
||||
margin-top: $spacing-vertical / 2;
|
||||
}
|
||||
+ .form-row-submit
|
||||
{
|
||||
margin-top: $spacing-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-field--text {
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
.form-field--text-number {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.form-field-advice-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-field-advice {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
transition: opacity 400ms ease-in;
|
||||
}
|
||||
|
||||
.form-field-advice--fading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.form-field-advice__arrow {
|
||||
text-align: left;
|
||||
padding-left: 18px;
|
||||
|
||||
font-size: 22px;
|
||||
line-height: 0.3;
|
||||
color: darken($color-primary, 5%);
|
||||
}
|
||||
|
||||
|
||||
.form-field-advice__content-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-field-advice__content {
|
||||
display: inline-block;
|
||||
|
||||
padding: 5px;
|
||||
border-radius: 2px;
|
||||
|
||||
background-color: darken($color-primary, 5%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-field-label {
|
||||
width: 118px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.sort-section {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
|
||||
|
||||
text-align: right;
|
||||
line-height: 1;
|
||||
font-size: 0.85em;
|
||||
color: $color-help;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(255, 255, 255, 0.74902);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
background: rgb(255, 255, 255);
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
padding: 36px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.modal__button {
|
||||
margin: 0px 6px;
|
||||
}
|
||||
|
||||
.error-modal-overlay {
|
||||
background: rgba(#000, .88);
|
||||
}
|
||||
|
||||
.error-modal__content {
|
||||
display: flex;
|
||||
padding: 0px 8px 10px 10px;
|
||||
}
|
||||
|
||||
.error-modal__warning-symbol {
|
||||
margin-top: 6px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.download-started-modal__file-path {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error-modal {
|
||||
max-width: none;
|
||||
width: 400px;
|
||||
}
|
||||
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
|
||||
border: 1px solid #eee;
|
||||
padding: 8px;
|
||||
list-style: none;
|
||||
max-height: 400px;
|
||||
max-width: 400px;
|
||||
overflow-y: hidden;
|
||||
section.section-spaced {
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
|
|
|
@ -25,12 +25,6 @@
|
|||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.icon-mega
|
||||
{
|
||||
font-size: 200px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
|
||||
readers do not read off random characters that represent icons */
|
||||
.icon-glass:before {
|
||||
|
|
|
@ -3,20 +3,24 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel
|
|||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
input:focus, textarea:focus
|
||||
:focus
|
||||
{
|
||||
outline: 0;
|
||||
outline: 0;
|
||||
}
|
||||
table
|
||||
input::-webkit-search-cancel-button {
|
||||
/* Remove default */
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
table
|
||||
{
|
||||
border-collapse: collapse;
|
||||
border-spacing:0;
|
||||
}
|
||||
fieldset, img, iframe
|
||||
fieldset, img, iframe
|
||||
{
|
||||
border: 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
font-weight:normal;
|
||||
}
|
||||
|
@ -25,11 +29,12 @@ ol, ul
|
|||
list-style-position: inside;
|
||||
> li { list-style-position: inside; }
|
||||
}
|
||||
input, textarea, select
|
||||
input, textarea, select
|
||||
{
|
||||
font-family:inherit;
|
||||
font-size:inherit;
|
||||
font-weight:inherit;
|
||||
font-family:inherit;
|
||||
font-size:inherit;
|
||||
font-weight:inherit;
|
||||
border: 0 none;
|
||||
}
|
||||
img {
|
||||
width: auto\9;
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
@import "_reset";
|
||||
@import "_grid";
|
||||
@import "_icons";
|
||||
@import "_mediaelement";
|
||||
@import "_canvas";
|
||||
@import "_gui";
|
||||
@import "component/_table";
|
||||
@import "component/_button.scss";
|
||||
@import "component/_card.scss";
|
||||
@import "component/_file-actions.scss";
|
||||
@import "component/_file-tile.scss";
|
||||
@import "component/_form-field.scss";
|
||||
@import "component/_header.scss";
|
||||
@import "component/_menu.scss";
|
||||
@import "component/_tooltip.scss";
|
||||
@import "component/_load-screen.scss";
|
||||
@import "component/_channel-indicator.scss";
|
||||
@import "component/_notice.scss";
|
||||
@import "component/_modal.scss";
|
||||
@import "component/_modal-page.scss";
|
||||
@import "component/_snack-bar.scss";
|
||||
@import "component/_video.scss";
|
||||
@import "page/_developer.scss";
|
||||
@import "page/_watch.scss";
|
||||
@import "page/_watch.scss";
|
||||
@import "page/_reward.scss";
|
||||
@import "page/_show.scss";
|
||||
|
|
87
ui/scss/component/_button.scss
Normal file
87
ui/scss/component/_button.scss
Normal file
|
@ -0,0 +1,87 @@
|
|||
@import "../global";
|
||||
|
||||
$button-focus-shift: 12%;
|
||||
|
||||
.button-set-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
+ .button-set-item
|
||||
{
|
||||
margin-left: $padding-button;
|
||||
}
|
||||
}
|
||||
|
||||
.button-block, .faux-button-block
|
||||
{
|
||||
display: inline-block;
|
||||
height: $height-button;
|
||||
line-height: $height-button;
|
||||
text-decoration: none;
|
||||
border: 0 none;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
.icon
|
||||
{
|
||||
top: 0em;
|
||||
}
|
||||
.icon:first-child
|
||||
{
|
||||
padding-right: 5px;
|
||||
}
|
||||
.icon:last-child
|
||||
{
|
||||
padding-left: 5px;
|
||||
}
|
||||
.icon:only-child
|
||||
{
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.button-block
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-button;
|
||||
}
|
||||
|
||||
.button-primary
|
||||
{
|
||||
$color-button-text: white;
|
||||
color: darken($color-button-text, $button-focus-shift * 0.5);
|
||||
background-color: $color-primary;
|
||||
box-shadow: $box-shadow-layer;
|
||||
&:focus {
|
||||
color: $color-button-text;
|
||||
//box-shadow: $box-shadow-focus;
|
||||
background-color: mix(black, $color-primary, $button-focus-shift)
|
||||
}
|
||||
}
|
||||
.button-alt
|
||||
{
|
||||
background-color: $color-bg-alt;
|
||||
box-shadow: $box-shadow-layer;
|
||||
}
|
||||
|
||||
.button-text
|
||||
{
|
||||
@include text-link();
|
||||
display: inline-block;
|
||||
|
||||
.button__content {
|
||||
margin: 0 $padding-text-link;
|
||||
}
|
||||
}
|
||||
.button-text-help
|
||||
{
|
||||
@include text-link(#aaa);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.button--flat
|
||||
{
|
||||
box-shadow: none !important;
|
||||
}
|
151
ui/scss/component/_card.scss
Normal file
151
ui/scss/component/_card.scss
Normal file
|
@ -0,0 +1,151 @@
|
|||
@import "../global";
|
||||
|
||||
$padding-card-horizontal: $spacing-vertical * 2/3;
|
||||
|
||||
.card {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: $width-page-constrained;
|
||||
background: $color-bg;
|
||||
box-shadow: $box-shadow-layer;
|
||||
border-radius: 2px;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
overflow: auto;
|
||||
}
|
||||
.card--obscured
|
||||
{
|
||||
position: relative;
|
||||
}
|
||||
.card--obscured .card__inner {
|
||||
-webkit-filter: blur($blur-intensity);
|
||||
-moz-filter: blur($blur-intensity);
|
||||
-o-filter: blur($blur-intensity);
|
||||
-ms-filter: blur($blur-intensity);
|
||||
filter: blur($blur-intensity);
|
||||
}
|
||||
.card__title-primary {
|
||||
padding: 0 $padding-card-horizontal;
|
||||
margin-top: $spacing-vertical;
|
||||
}
|
||||
.card__title-identity {
|
||||
padding: 0 $padding-card-horizontal;
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
margin-bottom: $spacing-vertical * 1/3;
|
||||
}
|
||||
.card__actions {
|
||||
padding: 0 $padding-card-horizontal;
|
||||
}
|
||||
.card__actions {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
}
|
||||
.card__actions--bottom {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
margin-bottom: $spacing-vertical * 1/3;
|
||||
}
|
||||
.card__actions--form-submit {
|
||||
margin-top: $spacing-vertical;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
}
|
||||
.card__content {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
padding: 0 $padding-card-horizontal;
|
||||
}
|
||||
.card__subtext {
|
||||
color: #444;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9em;
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
padding: 0 $padding-card-horizontal;
|
||||
}
|
||||
.card__subtext--allow-newlines {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.card__subtext--two-lines {
|
||||
height: $font-size * 0.9 * $font-line-height * 2;
|
||||
}
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
padding: 20px;
|
||||
background-color: rgba(128, 128, 128, 0.8);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
$card-link-scaling: 1.1;
|
||||
.card__link {
|
||||
display: block;
|
||||
}
|
||||
.card--link:hover {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: $box-shadow-focus;
|
||||
transform: scale($card-link-scaling);
|
||||
transform-origin: 50% 50%;
|
||||
overflow-x: visible;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.card__media {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
$width-card-small: $spacing-vertical * 12;
|
||||
$height-card-small: $spacing-vertical * 15;
|
||||
|
||||
.card--small {
|
||||
width: $width-card-small;
|
||||
overflow-x: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
.card--small .card__media {
|
||||
height: $width-card-small * 9 / 16;
|
||||
}
|
||||
|
||||
.card__subtitle {
|
||||
color: $color-help;
|
||||
font-size: 0.85em;
|
||||
line-height: $font-line-height * 1 / 0.85;
|
||||
}
|
||||
|
||||
.card-series-submit
|
||||
{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: $width-page-constrained;
|
||||
padding: $spacing-vertical / 2;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
> .card {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
margin-right: $spacing-vertical / 3;
|
||||
}
|
||||
+ .card-row {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
}
|
||||
.card-row--small {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
/*hacky way to give space for hover */
|
||||
padding-left: 20px;
|
||||
margin-left: -20px;
|
||||
padding-right: 20px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
.card-row__header {
|
||||
margin-bottom: $spacing-vertical / 3;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
@import "../global";
|
||||
|
||||
.channel-indicator__icon--invalid {
|
||||
color: #b01c2e;
|
||||
color: $color-error;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
@import "../global";
|
||||
|
||||
$height-file-tile: $spacing-vertical * 6;
|
||||
.file-tile__row {
|
||||
height: $spacing-vertical * 7;
|
||||
}
|
||||
|
||||
.file-tile__row--unavailable {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.file-tile__thumbnail {
|
||||
max-width: 100%;
|
||||
max-height: $spacing-vertical * 7;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.file-tile__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-tile__cost {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.file-tile__description {
|
||||
color: #444;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9em;
|
||||
overflow: hidden;
|
||||
height: $height-file-tile;
|
||||
.credit-amount {
|
||||
float: right;
|
||||
}
|
||||
//also a hack
|
||||
.card__media {
|
||||
height: $height-file-tile;
|
||||
max-width: $height-file-tile;
|
||||
width: $height-file-tile;
|
||||
margin-right: $spacing-vertical / 2;
|
||||
float: left;
|
||||
}
|
||||
//basically everything here is a hack now
|
||||
.file-tile__content {
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
margin-left: $height-file-tile + $spacing-vertical / 2;
|
||||
}
|
||||
.card__title-primary {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
158
ui/scss/component/_form-field.scss
Normal file
158
ui/scss/component/_form-field.scss
Normal file
|
@ -0,0 +1,158 @@
|
|||
@import "../global";
|
||||
|
||||
$width-input-border: 2px;
|
||||
$width-input-text: 330px;
|
||||
|
||||
.form-row-submit
|
||||
{
|
||||
margin-top: $spacing-vertical;
|
||||
}
|
||||
.form-row-submit--with-footer
|
||||
{
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
|
||||
.form-row__label-row {
|
||||
margin-top: $spacing-vertical * 5/6;
|
||||
margin-bottom: $spacing-vertical * 1/6;
|
||||
line-height: 1;
|
||||
font-size: 0.9 * $font-size;
|
||||
}
|
||||
.form-row__label-row--prefix {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
input[type="text"].input-copyable {
|
||||
border: 1px solid $color-form-border;
|
||||
line-height: 1;
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
padding-bottom: $spacing-vertical * 1/3;
|
||||
width: $width-input-text;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
width: 100%;
|
||||
&:focus { border-color: black; }
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: inline-block;
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
transition: outline $transition-standard;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
height: $spacing-vertical;
|
||||
&:focus {
|
||||
outline: $width-input-border solid $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="date"] {
|
||||
@include placeholder {
|
||||
color: lighten($color-text-dark, 60%);
|
||||
}
|
||||
transition: all $transition-standard;
|
||||
cursor: pointer;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
&[readonly] {
|
||||
background-color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="date"] {
|
||||
border-bottom: $width-input-border solid $color-form-border;
|
||||
line-height: 1;
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
padding-bottom: $spacing-vertical * 1/3;
|
||||
&.form-field__input--error {
|
||||
border-color: $color-error;
|
||||
}
|
||||
&.form-field__input--inline {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom-width: 1px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="date"]:focus {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: $width-input-border solid $color-form-border;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
&[for] { cursor: pointer; }
|
||||
> input[type="checkbox"], input[type="radio"] {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__label--error {
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
.form-field__input-text {
|
||||
width: $width-input-text;
|
||||
}
|
||||
|
||||
.form-field__prefix {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.form-field__postfix {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.form-field__input-number {
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-field__input-textarea {
|
||||
width: $width-input-text;
|
||||
}
|
||||
|
||||
.form-field__error, .form-field__helper {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
font-size: 0.8em;
|
||||
transition: opacity $transition-standard;
|
||||
}
|
||||
|
||||
.form-field__error {
|
||||
color: $color-error;
|
||||
}
|
||||
.form-field__helper {
|
||||
color: $color-help;
|
||||
}
|
94
ui/scss/component/_header.scss
Normal file
94
ui/scss/component/_header.scss
Normal file
|
@ -0,0 +1,94 @@
|
|||
@import "../global";
|
||||
|
||||
$color-header: #666;
|
||||
$color-header-active: darken($color-header, 20%);
|
||||
|
||||
#header
|
||||
{
|
||||
color: $color-header;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
box-shadow: $box-shadow-layer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
padding: $spacing-vertical / 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header__item {
|
||||
flex: 0 0 content;
|
||||
padding-left: $spacing-vertical / 4;
|
||||
padding-right: $spacing-vertical / 4;
|
||||
}
|
||||
.header__item--wunderbar {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.wunderbar
|
||||
{
|
||||
position: relative;
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: $spacing-vertical / 2 - 4px; //hacked
|
||||
}
|
||||
}
|
||||
|
||||
.wunderbar--active .icon-search { color: $color-primary; }
|
||||
|
||||
.wunderbar__input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
width: 100%;
|
||||
color: $color-header;
|
||||
height: $spacing-vertical * 1.5;
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
padding-left: 38px;
|
||||
padding-right: 5px;
|
||||
border: 1px solid $color-text-dark;
|
||||
@include border-radius(2px);
|
||||
border: 1px solid #ccc;
|
||||
&:focus {
|
||||
color: $color-header-active;
|
||||
box-shadow: $box-shadow-focus;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
nav.sub-header
|
||||
{
|
||||
text-transform: uppercase;
|
||||
padding: 0 0 $spacing-vertical;
|
||||
&.sub-header--constrained {
|
||||
max-width: $width-page-constrained;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
> a
|
||||
{
|
||||
$sub-header-selected-underline-height: 2px;
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
padding: 0 5px;
|
||||
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
|
||||
color: $color-header;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child
|
||||
{
|
||||
margin-right: 0;
|
||||
}
|
||||
&.sub-header-selected
|
||||
{
|
||||
border-bottom: $sub-header-selected-underline-height solid $color-header-active;
|
||||
color: $color-header-active;
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
color: $color-header-active;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
|
||||
.load-screen__details--warning {
|
||||
color: $color-warning;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.load-screen__cancel-link {
|
||||
|
|
|
@ -10,7 +10,7 @@ $border-radius-menu: 2px;
|
|||
position: absolute;
|
||||
white-space: nowrap;
|
||||
background-color: white;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
border-radius: $border-radius-menu;
|
||||
padding-top: ($spacing-vertical / 5) 0px;
|
||||
z-index: 1;
|
||||
|
|
54
ui/scss/component/_modal-page.scss
Normal file
54
ui/scss/component/_modal-page.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
@import "../global";
|
||||
|
||||
.modal-page {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
background: rgb(255, 255, 255);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-page--full {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
.modal-page__content {
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.modal-page {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
background: rgb(255, 255, 255);
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
padding: 36px;
|
||||
|
||||
top: 25px;
|
||||
left: 25px;
|
||||
right: 25px;
|
||||
bottom: 25px;
|
||||
}
|
||||
*/
|
||||
|
||||
.modal-page__content {
|
||||
h1, h2 {
|
||||
margin-bottom: $spacing-vertical / 2;
|
||||
}
|
||||
h3, h4 {
|
||||
margin-bottom: $spacing-vertical / 4;
|
||||
}
|
||||
}
|
81
ui/scss/component/_modal.scss
Normal file
81
ui/scss/component/_modal.scss
Normal file
|
@ -0,0 +1,81 @@
|
|||
@import "../global";
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(255, 255, 255, 0.74902);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-overlay--clear {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
background: rgb(255, 255, 255);
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
padding: $spacing-vertical;
|
||||
box-shadow: $box-shadow-layer;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.modal__button {
|
||||
margin: 0px 6px;
|
||||
}
|
||||
|
||||
.error-modal-overlay {
|
||||
background: rgba(#000, .88);
|
||||
}
|
||||
|
||||
.error-modal__content {
|
||||
display: flex;
|
||||
padding: 0px 8px 10px 10px;
|
||||
}
|
||||
|
||||
.error-modal__warning-symbol {
|
||||
margin-top: 6px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.download-started-modal__file-path {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error-modal {
|
||||
max-width: none;
|
||||
width: 400px;
|
||||
}
|
||||
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
|
||||
border: 1px solid #eee;
|
||||
padding: 8px;
|
||||
list-style: none;
|
||||
max-height: 400px;
|
||||
max-width: 400px;
|
||||
overflow-y: hidden;
|
||||
}
|
18
ui/scss/component/_notice.scss
Normal file
18
ui/scss/component/_notice.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
@import "../global";
|
||||
|
||||
.notice {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #000;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
border-radius: 5px;
|
||||
|
||||
color: #468847;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
.notice--error {
|
||||
color: #b94a48;
|
||||
background-color: #f2dede;
|
||||
border-color: #eed3d7;
|
||||
}
|
42
ui/scss/component/_snack-bar.scss
Normal file
42
ui/scss/component/_snack-bar.scss
Normal file
|
@ -0,0 +1,42 @@
|
|||
@import "../global";
|
||||
|
||||
$padding-snack-horizontal: $spacing-vertical;
|
||||
|
||||
.snack-bar {
|
||||
$height-snack: $spacing-vertical * 2;
|
||||
$padding-snack-vertical: $spacing-vertical / 4;
|
||||
|
||||
line-height: $height-snack - $padding-snack-vertical * 2;
|
||||
padding: $padding-snack-vertical $padding-snack-horizontal;
|
||||
position: fixed;
|
||||
top: $spacing-vertical;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
background: $color-black-transparent;
|
||||
color: #f0f0f0;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 2px;
|
||||
|
||||
transition: all $transition-standard;
|
||||
|
||||
z-index: 10000; /*hack to get it over react modal */
|
||||
}
|
||||
|
||||
.snack-bar__action {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
color: $color-primary-light;
|
||||
margin: 0px 0px 0px $padding-snack-horizontal;
|
||||
min-width: min-content;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
z-index: 1;
|
||||
left: 50%;
|
||||
margin-left: $tooltip-body-width * -1 / 2;
|
||||
white-space: normal;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: $spacing-vertical / 2;
|
||||
|
@ -24,7 +25,7 @@
|
|||
background-color: $color-bg;
|
||||
font-size: $font-size * 7/8;
|
||||
line-height: $font-line-height;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
}
|
||||
|
||||
.tooltip--header .tooltip__link {
|
||||
|
|
56
ui/scss/component/_video.scss
Normal file
56
ui/scss/component/_video.scss
Normal file
|
@ -0,0 +1,56 @@
|
|||
video {
|
||||
object-fit: contain;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.video {
|
||||
background: #000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.video-embedded {
|
||||
max-width: $width-page-constrained;
|
||||
max-height: $height-video-embedded;
|
||||
height: $height-video-embedded;
|
||||
video {
|
||||
height: 100%;
|
||||
}
|
||||
&.video--hidden {
|
||||
height: $height-video-embedded;
|
||||
}
|
||||
&.video--active {
|
||||
/*background: none;*/
|
||||
}
|
||||
}
|
||||
|
||||
.video__cover {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: auto 100%;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
.video__play-button { @include absolute-center(); }
|
||||
}
|
||||
.video__play-button {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: $spacing-vertical * 3;
|
||||
color: white;
|
||||
z-index: 1;
|
||||
background: $color-black-transparent;
|
||||
opacity: 0.6;
|
||||
left: 0;
|
||||
top: 0;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transition: opacity $transition-standard;
|
||||
}
|
||||
}
|
5
ui/scss/page/_reward.scss
Normal file
5
ui/scss/page/_reward.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "../global";
|
||||
|
||||
.reward-page__details {
|
||||
background-color: lighten($color-canvas, 1.5%);
|
||||
}
|
9
ui/scss/page/_show.scss
Normal file
9
ui/scss/page/_show.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
@import "../global";
|
||||
|
||||
.show-page-media {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-vertical;
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
.video {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video__overlay {
|
||||
position: absolute;
|
||||
|
@ -23,7 +20,7 @@
|
|||
}
|
||||
|
||||
.video__back-label {
|
||||
opacity: 0;
|
||||
opacity: 0.5;
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,4 +35,4 @@ module.exports = {
|
|||
]
|
||||
},
|
||||
target: 'electron-main',
|
||||
};
|
||||
};
|
|
@ -38,4 +38,4 @@ module.exports = {
|
|||
]
|
||||
},
|
||||
target: 'electron-main',
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue