Merge branch 'master' into daemon19
This commit is contained in:
commit
cbe05c3bcc
46 changed files with 2661 additions and 1314 deletions
|
@ -4,7 +4,7 @@
|
|||
"airbnb",
|
||||
"plugin:import/electron",
|
||||
"plugin:flowtype/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
"prettier"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
[ignore]
|
||||
.*/node_modules/**
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
flow-typed
|
||||
./flow-typed
|
||||
|
||||
[lints]
|
||||
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
/node_modules
|
||||
/dist
|
||||
/build/daemon.ver
|
||||
/build/daemon.zip
|
||||
/build/venv
|
||||
*.pyc
|
||||
/static/daemon/lbrynet*
|
||||
|
|
76
.travis.yml
76
.travis.yml
|
@ -1,28 +1,50 @@
|
|||
env:
|
||||
global:
|
||||
- TRANSIFEX_USER: api
|
||||
- secure: u6gwnZlPGJLnvpoPPCpUZ+jBPajQuIW1+aq6UGW57z54AUjTAECxaYpqcGTGtDBjYark/yeiso887wP/EmJva7hMHeNMf6uLqwzP3YFsIv/Iv+9c1f4MXbJNgOrEKN834o/BdkD4ifi9CCiH9uPPVYGPx1bvfaxGpcHmQXdW4F4S0uj+jePB257mt+afGiNlz9wET6kWJKNNZf/4BNmefldVNq7h6oTSLsyO1TBhDcvSjatpKIwmXUNQfSUTFWvrtpWUB/m/IzitGuUtrt82vU2fPl7tuH6BHNrNp58MINjFzXXJLC+mMybb2UBDIAuc3+k7vj4J0U2rkcTloxDNCKNmYa1jBogOKBRgGp98Ct7E0V2vuLGAPniUbvBcCGK1wwed7uwDjsz3YNCGxUEcyyWc3OVDgN/up4+gXHxkh9FTpZy8Q3rSZx4Lwj700impBUQIVh/5p7Vgv+bSUdOeVRAMlcP9yT83jX50w9LkJMfICFPNv1tOZ3/SOnnB+JdW/ahpplFI4Z68/fBLttZTeaNcU4f28oJvPer8Wll+Elx5kxwLqLbwVUFNlxTxY1LYnPB7SPjGxrFNy3mVTRq5Pxp1hMiTZF4TlapkfHgR+gEzk2wpcJGmub70tW2baZaJF0jDBWIh7GXV+EGve53BKDhpX2Z6jTK0gkhrSUW1MT8=
|
||||
- secure: h1r9Qzv2xHRQl7nDHcscB4qDv7KlF/ncgHko1YuoY4oLZipBV8mzQXDmn3nlMKwaKOe1/Tty/bjoZexkWict4cwKBzU7/1HtJeMa6nxRICuS6DiVhLUNGZEddK6jQLxeEZFxkFPSCZyjybPWtasF8f8jd0lqqLIL4/FcIVV56aRCKAsUwCbedxi8Vnc19l74xjaQIK82xBFYOQPK078OBovk9DDOnicTjMulUo3/pKEZD1njSdcEhfSRv+MFE+31B/a6lpoLo7twPlyzLMfpo30NlEzIN0TeMAk44e4PV6DYg0wntC2GJ21p4BqMnDGocwnZwm7gpjflzUZdW6hF0esGLcqOdbyJLUb3rNX9AzQmn0p9KwDC3S80peZFxSiuLJGL8eivceVDUK/jwWinu3OHDJ/eO5iMDm9odm2ALemPtrDTSlRNT8HzNCY9PQTU9Dhdm4Q/dGDsRPWibFJSJ/qGKhVgadk2CUEAPua0hB1zZ556PkTGx4R1JDscgFDAkgemzgKl4Z/4qK3xGDoEtz1HmBlvQtn+B/PuhA2essADj0iTDiItxb7AYTA7EzsHEcRMmrbYarZ3Eh2onWy2GOpAGRN5Xl9cBIDbibcSC6BLI1m2PcLABpP7DhUX4bJbsVNSiGesHEU7o9Dgn8Ig09eHW/8F9i0VVoGUZXxKMJ8=
|
||||
|
||||
os: linux
|
||||
dist: xenial
|
||||
|
||||
branches:
|
||||
only:
|
||||
- "/^v\\d+\\.\\d+\\.\\d+$/"
|
||||
|
||||
install:
|
||||
- rvm install 2.3.1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode9.2
|
||||
language: node_js
|
||||
node_js: "9"
|
||||
env:
|
||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
- os: linux
|
||||
services: docker
|
||||
language: generic
|
||||
before_cache:
|
||||
- rm -rf $HOME/.cache/electron-builder/wine
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- $HOME/.cache/electron
|
||||
- $HOME/.cache/electron-builder
|
||||
before_install:
|
||||
- |
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
|
||||
mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.3.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.3.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1
|
||||
export PATH="/tmp/git-lfs:$PATH"
|
||||
fi
|
||||
before_script:
|
||||
- git lfs pull
|
||||
script:
|
||||
- rvm use 2.3.1 && gem install danger --version '~> 4.0' && danger
|
||||
- FULL_BUILD=true ./build.sh
|
||||
|
||||
sudo: required
|
||||
after_success:
|
||||
- pip install virtualenv
|
||||
- virtualenv ~/env
|
||||
- source ~/env/bin/activate
|
||||
- pip install transifex-client
|
||||
- sudo echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername= '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\ntoken = '"$TRANSIFEX_API_TOKEN"$'\n' > ~/.transifexrc
|
||||
- tx push -s
|
||||
- |
|
||||
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
|
||||
docker run --rm \
|
||||
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
|
||||
-v ${PWD}:/project \
|
||||
-v ~/.cache/electron:/root/.cache/electron \
|
||||
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
|
||||
electronuserland/builder:wine \
|
||||
/bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --linux --win"
|
||||
else
|
||||
yarn build
|
||||
fi
|
||||
branches:
|
||||
except:
|
||||
- "/^v\\d+\\.\\d+\\.\\d+$/"
|
||||
addons:
|
||||
artifacts:
|
||||
working_dir: dist
|
||||
paths:
|
||||
- $(git ls-files -o dist/{*.dmg,*.exe,*.AppImage} | tr "\n" ":")
|
||||
target_paths:
|
||||
- /commit-${TRAVIS_COMMIT:0:7}_build-${TRAVIS_BUILD_NUMBER}_tag-${TRAVIS_TAG}
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -8,7 +8,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
*
|
||||
* Save app state when closing to tray ([#968](https://github.com/lbryio/lbry-app/issues/968))
|
||||
* Added startup-troubleshooting FAQ URL to daemon error ([#1039](https://github.com/lbryio/lbry-app/pull/1039))
|
||||
* Added ability to export wallet transactions to JSON and CSV format ([#976](https://github.com/lbryio/lbry-app/pull/976))
|
||||
* Add Rewards FAQ to LBRY app ([#1041](https://github.com/lbryio/lbry-app/pull/1041))
|
||||
*
|
||||
|
||||
### Changed
|
||||
|
@ -16,8 +19,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
*
|
||||
|
||||
### Fixed
|
||||
*
|
||||
*
|
||||
* Fixed sort by date of published content ([#986](https://github.com/lbryio/lbry-app/issues/986))
|
||||
* Fix night mode start time, set to 9PM (#1050)
|
||||
* Fix night mode start time, set to 9PM ([#1050](https://github.com/lbryio/lbry-app/issues/1050))
|
||||
* Disable drag and drop of files into the app ([#1045](https://github.com/lbryio/lbry-app/pull/1045))
|
||||
|
||||
### Deprecated
|
||||
*
|
||||
|
|
|
@ -43,32 +43,6 @@ fi
|
|||
|
||||
yarn install
|
||||
|
||||
####################
|
||||
# daemon and cli #
|
||||
####################
|
||||
echo -e "\033[0;32mGrabbing Daemon and CLI\x1b[m"
|
||||
if $OSX; then
|
||||
OSNAME="macos"
|
||||
else
|
||||
OSNAME="linux"
|
||||
fi
|
||||
DAEMON_VER=$(node -e "console.log(require(\"$ROOT/package.json\").lbrySettings.lbrynetDaemonVersion)")
|
||||
DAEMON_URL_TEMPLATE=$(node -e "console.log(require(\"$ROOT/package.json\").lbrySettings.lbrynetDaemonUrlTemplate)")
|
||||
DAEMON_URL=$(echo ${DAEMON_URL_TEMPLATE//DAEMONVER/$DAEMON_VER} | sed "s/OSNAME/$OSNAME/g")
|
||||
DAEMON_VER_PATH="$BUILD_DIR/daemon.ver"
|
||||
echo "$DAEMON_VER_PATH"
|
||||
if [[ ! -f $DAEMON_VER_PATH || ! -f $ROOT/static/daemon/lbrynet-daemon || "$(< "$DAEMON_VER_PATH")" != "$DAEMON_VER" ]]; then
|
||||
curl -sL -o "$BUILD_DIR/daemon.zip" "$DAEMON_URL"
|
||||
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/static/daemon/"
|
||||
rm "$BUILD_DIR/daemon.zip"
|
||||
echo "$DAEMON_VER" > "$DAEMON_VER_PATH"
|
||||
else
|
||||
echo -e "\033[4;31mAlready have daemon version $DAEMON_VER, skipping download\x1b[m"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
###################
|
||||
# Build the app #
|
||||
###################
|
||||
|
@ -79,23 +53,6 @@ if [ "$FULL_BUILD" == "true" ]; then
|
|||
|
||||
yarn build
|
||||
|
||||
# Workaround: TeamCity expects the dmg to be in dist/mac, but in the new electron-builder
|
||||
# it's put directly in dist/ (the right way to solve this is to update the TeamCity config)
|
||||
if $OSX; then
|
||||
cp dist/*.dmg dist/mac
|
||||
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.
|
||||
VENV="$BUILD_DIR/venv"
|
||||
if [ -d "$VENV" ]; then
|
||||
rm -rf "$VENV"
|
||||
fi
|
||||
virtualenv "$VENV"
|
||||
"$VENV/bin/pip" install -r "$BUILD_DIR/requirements.txt"
|
||||
"$VENV/bin/python" "$BUILD_DIR/upload_assets.py"
|
||||
|
||||
echo -e '\033[0;32mBuild and packaging complete.\x1b[m'
|
||||
else
|
||||
echo -e 'Build complete. Run \033[1;31myarn dev\x1b[m to launch the app'
|
||||
|
|
47
build/downloadDaemon.js
Normal file
47
build/downloadDaemon.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable no-console,import/no-commonjs */
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const packageJSON = require('../package.json');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const axios = require('axios');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const decompress = require('decompress');
|
||||
const os = require('os');
|
||||
|
||||
const daemonURLTemplate = packageJSON.lbrySettings.lbrynetDaemonUrlTemplate;
|
||||
const daemonVersion = packageJSON.lbrySettings.lbrynetDaemonVersion;
|
||||
let currentPlatform = os.platform();
|
||||
if (currentPlatform === 'darwin') currentPlatform = 'macos';
|
||||
if (currentPlatform === 'win32') currentPlatform = 'windows';
|
||||
|
||||
const daemonURL = daemonURLTemplate
|
||||
.replace(/DAEMONVER/g, daemonVersion)
|
||||
.replace(/OSNAME/g, currentPlatform);
|
||||
const tmpZipPath = 'build/daemon.zip';
|
||||
|
||||
console.log('\x1b[34minfo\x1b[0m Downloading daemon...');
|
||||
axios
|
||||
.request({
|
||||
responseType: 'arraybuffer',
|
||||
url: daemonURL,
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
},
|
||||
})
|
||||
.then(result => {
|
||||
fs.writeFileSync(tmpZipPath, result.data);
|
||||
return true;
|
||||
})
|
||||
.then(() => {
|
||||
decompress(tmpZipPath, 'static/daemon', {
|
||||
filter: file =>
|
||||
path.basename(file.path).replace(path.extname(file.path), '') === 'lbrynet-daemon',
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
console.log('\x1b[32msuccess\x1b[0m Daemon downloaded!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m`);
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
{
|
||||
"appId": "io.lbry.LBRY",
|
||||
"publish": {
|
||||
"productName": "LBRY",
|
||||
"publish": [{
|
||||
"provider": "s3",
|
||||
"bucket": "releases.lbry.io",
|
||||
"path": "app/latest"
|
||||
},
|
||||
}, "github"],
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"category": "public.app-category.entertainment"
|
||||
},
|
||||
"dmg": {
|
||||
|
@ -28,36 +30,24 @@
|
|||
"width": 500,
|
||||
"height": 300
|
||||
},
|
||||
"background": "build/background.png"
|
||||
"background": "./build/background.png"
|
||||
},
|
||||
"protocols": [
|
||||
{
|
||||
"name": "lbry",
|
||||
"name": "LBRY URI",
|
||||
"schemes": ["lbry"],
|
||||
"role": "Viewer"
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"target": "deb",
|
||||
"category": "Video;AudioVideo;",
|
||||
"target": "AppImage",
|
||||
"category": "AudioVideo;Video",
|
||||
"desktop": {
|
||||
"MimeType": "x-scheme-handler/lbry",
|
||||
"Exec": "/opt/LBRY/lbry %U"
|
||||
"MimeType": "x-scheme-handler/lbry;"
|
||||
}
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"gconf2",
|
||||
"gconf-service",
|
||||
"libnotify4",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3",
|
||||
"libsecret-1-0"
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"perMachine": true
|
||||
},
|
||||
"artifactName": "${productName}_${version}_${arch}.${ext}"
|
||||
"artifactName": "${productName}_${version}.${ext}"
|
||||
}
|
||||
|
|
56
package.json
56
package.json
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"name": "lbry-app",
|
||||
"version": "0.20.0",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"homepage": "https://lbry.io/",
|
||||
|
@ -21,7 +21,8 @@
|
|||
"compile": "electron-webpack && yarn extract-langs",
|
||||
"build": "yarn compile && electron-builder build",
|
||||
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"release": "yarn compile && electron-builder build",
|
||||
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js",
|
||||
"lint": "eslint 'src/**/*.{js,jsx}' --fix",
|
||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write"
|
||||
},
|
||||
|
@ -33,16 +34,17 @@
|
|||
"classnames": "^2.2.5",
|
||||
"country-data": "^0.0.31",
|
||||
"electron-dl": "^1.6.0",
|
||||
"electron-is-dev": "^0.3.0",
|
||||
"electron-log": "^2.2.12",
|
||||
"electron-publisher-s3": "^19.47.0",
|
||||
"electron-updater": "^2.18.2",
|
||||
"electron-publisher-s3": "^20.2.0",
|
||||
"electron-updater": "^2.21.0",
|
||||
"find-process": "^1.1.0",
|
||||
"formik": "^0.10.4",
|
||||
"from2": "^2.3.0",
|
||||
"install": "^0.10.2",
|
||||
"jayson": "^2.0.2",
|
||||
"jshashes": "^1.0.7",
|
||||
"keytar-prebuild": "^4.0.4",
|
||||
"keytar-prebuild": "^4.1.1",
|
||||
"localforage": "^1.5.0",
|
||||
"mixpanel-browser": "^2.17.1",
|
||||
"moment": "^2.20.1",
|
||||
|
@ -72,43 +74,41 @@
|
|||
"y18n": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-plugin-module-resolver": "^3.0.0",
|
||||
"babel-plugin-react-require": "^3.0.0",
|
||||
"axios": "^0.18.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-plugin-module-resolver": "^3.1.0",
|
||||
"babel-polyfill": "^6.20.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"decompress": "^4.2.0",
|
||||
"devtron": "^1.4.0",
|
||||
"electron": "^1.7.11",
|
||||
"electron-builder": "^19.55.2",
|
||||
"electron-devtools-installer": "^2.2.1",
|
||||
"electron-webpack": "^1.11.0",
|
||||
"eslint": "^4.13.1",
|
||||
"electron": "^1.8.3",
|
||||
"electron-builder": "^20.3.1",
|
||||
"electron-devtools-installer": "^2.2.3",
|
||||
"electron-webpack": "^1.13.0",
|
||||
"eslint": "^4.18.2",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-import-resolver-webpack": "^0.8.3",
|
||||
"eslint-plugin-flowtype": "^2.40.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-import-resolver-webpack": "^0.8.4",
|
||||
"eslint-plugin-flowtype": "^2.46.1",
|
||||
"eslint-plugin-import": "^2.9.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-prettier": "^2.4.0",
|
||||
"eslint-plugin-react": "^7.5.1",
|
||||
"flow-babel-webpack-plugin": "^1.1.0",
|
||||
"flow-bin": "^0.61.0",
|
||||
"flow-typed": "^2.2.3",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"flow-babel-webpack-plugin": "^1.1.1",
|
||||
"flow-bin": "^0.66.0",
|
||||
"flow-typed": "^2.3.0",
|
||||
"husky": "^0.14.3",
|
||||
"i18n-extract": "^0.5.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"lint-staged": "^6.0.0",
|
||||
"lint-staged": "^7.0.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"prettier": "^1.4.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"prettier": "^1.11.1",
|
||||
"sass-loader": "^6.0.7",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-build-notifier": "^0.1.18"
|
||||
},
|
||||
"resolutions": {
|
||||
"webpack/webpack-sources": "1.0.1"
|
||||
"webpack-build-notifier": "^0.1.23"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6",
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { app, Menu, Tray as ElectronTray } from 'electron';
|
||||
import path from 'path';
|
||||
import createWindow from './createWindow';
|
||||
|
||||
export default class Tray {
|
||||
window;
|
||||
updateAttachedWindow;
|
||||
tray;
|
||||
|
||||
constructor(window, updateAttachedWindow) {
|
||||
this.window = window;
|
||||
this.updateAttachedWindow = updateAttachedWindow;
|
||||
}
|
||||
|
||||
create() {
|
||||
let iconPath;
|
||||
switch (process.platform) {
|
||||
case 'darwin': {
|
||||
iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png');
|
||||
break;
|
||||
}
|
||||
case 'win32': {
|
||||
iconPath = path.join(__static, '/img/tray/windows/tray.ico');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
iconPath = path.join(__static, '/img/tray/default/tray.png');
|
||||
}
|
||||
}
|
||||
|
||||
this.tray = new ElectronTray(iconPath);
|
||||
|
||||
this.tray.on('double-click', () => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
this.window = createWindow();
|
||||
this.updateAttachedWindow(this.window);
|
||||
} else {
|
||||
this.window.show();
|
||||
this.window.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.tray.setToolTip('LBRY App');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: `Open ${app.getName()}`,
|
||||
click: () => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
this.window = createWindow();
|
||||
this.updateAttachedWindow(this.window);
|
||||
} else {
|
||||
this.window.show();
|
||||
this.window.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
{ role: 'quit' },
|
||||
];
|
||||
const contextMenu = Menu.buildFromTemplate(template);
|
||||
this.tray.setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
41
src/main/createTray.js
Normal file
41
src/main/createTray.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { app, Menu, Tray } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
export default window => {
|
||||
let iconPath;
|
||||
switch (process.platform) {
|
||||
case 'darwin': {
|
||||
iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png');
|
||||
break;
|
||||
}
|
||||
case 'win32': {
|
||||
iconPath = path.join(__static, '/img/tray/windows/tray.ico');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
iconPath = path.join(__static, '/img/tray/default/tray.png');
|
||||
}
|
||||
}
|
||||
|
||||
const tray = new Tray(iconPath);
|
||||
|
||||
tray.on('double-click', () => {
|
||||
window.show();
|
||||
});
|
||||
|
||||
tray.setToolTip('LBRY App');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: `Open ${app.getName()}`,
|
||||
click: () => {
|
||||
window.show();
|
||||
},
|
||||
},
|
||||
{ role: 'quit' },
|
||||
];
|
||||
const contextMenu = Menu.buildFromTemplate(template);
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
return tray;
|
||||
};
|
|
@ -1,8 +1,9 @@
|
|||
import { app, BrowserWindow, dialog } from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
import setupBarMenu from './menu/setupBarMenu';
|
||||
import setupContextMenu from './menu/setupContextMenu';
|
||||
|
||||
export default deepLinkingURIArg => {
|
||||
export default appState => {
|
||||
let windowConfiguration = {
|
||||
backgroundColor: '#155B4A',
|
||||
minWidth: 800,
|
||||
|
@ -12,20 +13,18 @@ export default deepLinkingURIArg => {
|
|||
};
|
||||
|
||||
// Disable renderer process's webSecurity on development to enable CORS.
|
||||
windowConfiguration =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? {
|
||||
...windowConfiguration,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
},
|
||||
}
|
||||
: windowConfiguration;
|
||||
windowConfiguration = isDev
|
||||
? {
|
||||
...windowConfiguration,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
},
|
||||
}
|
||||
: windowConfiguration;
|
||||
|
||||
const rendererURL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
|
||||
: `file://${__dirname}/index.html`;
|
||||
const rendererURL = isDev
|
||||
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
|
||||
: `file://${__dirname}/index.html`;
|
||||
|
||||
let window = new BrowserWindow(windowConfiguration);
|
||||
|
||||
|
@ -35,11 +34,7 @@ export default deepLinkingURIArg => {
|
|||
|
||||
let deepLinkingURI;
|
||||
// Protocol handler for win32
|
||||
if (
|
||||
!deepLinkingURIArg &&
|
||||
process.platform === 'win32' &&
|
||||
String(process.argv[1]).startsWith('lbry')
|
||||
) {
|
||||
if (process.platform === 'win32' && String(process.argv[1]).startsWith('lbry')) {
|
||||
// Keep only command line / deep linked arguments
|
||||
// Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to
|
||||
// restore the original URI that was typed.
|
||||
|
@ -48,15 +43,16 @@ export default deepLinkingURIArg => {
|
|||
// - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as
|
||||
// an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well.
|
||||
deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#');
|
||||
} else {
|
||||
deepLinkingURI = deepLinkingURIArg;
|
||||
}
|
||||
|
||||
setupBarMenu();
|
||||
setupContextMenu(window);
|
||||
|
||||
window.on('closed', () => {
|
||||
window = null;
|
||||
window.on('close', event => {
|
||||
if (!appState.isQuitting) {
|
||||
event.preventDefault();
|
||||
window.hide();
|
||||
}
|
||||
});
|
||||
|
||||
window.on('focus', () => {
|
||||
|
@ -87,7 +83,7 @@ export default deepLinkingURIArg => {
|
|||
window.webContents.on('did-finish-load', () => {
|
||||
window.webContents.send('open-uri-requested', deepLinkingURI, true);
|
||||
window.webContents.session.setUserAgent(`LBRY/${app.getVersion()}`);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDev) {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,8 +7,9 @@ import url from 'url';
|
|||
import https from 'https';
|
||||
import { shell, app, ipcMain, dialog } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import isDev from 'electron-is-dev';
|
||||
import Daemon from './Daemon';
|
||||
import Tray from './Tray';
|
||||
import createTray from './createTray';
|
||||
import createWindow from './createWindow';
|
||||
|
||||
autoUpdater.autoDownload = true;
|
||||
|
@ -32,11 +33,7 @@ let rendererWindow;
|
|||
let tray;
|
||||
let daemon;
|
||||
|
||||
let isQuitting;
|
||||
|
||||
const updateRendererWindow = window => {
|
||||
rendererWindow = window;
|
||||
};
|
||||
const appState = {};
|
||||
|
||||
const installExtensions = async () => {
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies,global-require
|
||||
|
@ -64,28 +61,27 @@ app.on('ready', async () => {
|
|||
daemon = new Daemon();
|
||||
daemon.on('exit', () => {
|
||||
daemon = null;
|
||||
if (!isQuitting) {
|
||||
if (!appState.isQuitting) {
|
||||
dialog.showErrorBox(
|
||||
'Daemon has Exited',
|
||||
'The daemon may have encountered an unexpected error, or another daemon instance is already running.'
|
||||
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
|
||||
'For more information please visit: \n' +
|
||||
'https://lbry.io/faq/startup-troubleshooting'
|
||||
);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
daemon.launch();
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDev) {
|
||||
await installExtensions();
|
||||
}
|
||||
rendererWindow = createWindow();
|
||||
tray = new Tray(rendererWindow, updateRendererWindow);
|
||||
tray.create();
|
||||
rendererWindow = createWindow(appState);
|
||||
tray = createTray(rendererWindow);
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (!rendererWindow) rendererWindow = createWindow();
|
||||
rendererWindow.show();
|
||||
});
|
||||
|
||||
app.on('will-quit', event => {
|
||||
|
@ -117,7 +113,7 @@ app.on('will-quit', event => {
|
|||
return;
|
||||
}
|
||||
|
||||
isQuitting = true;
|
||||
appState.isQuitting = true;
|
||||
if (daemon) daemon.quit();
|
||||
});
|
||||
|
||||
|
@ -126,18 +122,13 @@ app.on('will-finish-launching', () => {
|
|||
// Protocol handler for macOS
|
||||
app.on('open-url', (event, URL) => {
|
||||
event.preventDefault();
|
||||
if (rendererWindow && !rendererWindow.isDestroyed()) {
|
||||
rendererWindow.webContents.send('open-uri-requested', URL);
|
||||
rendererWindow.show();
|
||||
rendererWindow.focus();
|
||||
} else {
|
||||
rendererWindow = createWindow(URL);
|
||||
}
|
||||
rendererWindow.webContents.send('open-uri-requested', URL);
|
||||
rendererWindow.show();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Subscribe to event so the app doesn't quit when closing the window.
|
||||
app.on('before-quit', () => {
|
||||
appState.isQuitting = true;
|
||||
});
|
||||
|
||||
ipcMain.on('upgrade', (event, installerPath) => {
|
||||
|
@ -224,7 +215,7 @@ ipcMain.on('set-auth-token', (event, token) => {
|
|||
|
||||
process.on('uncaughtException', error => {
|
||||
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
|
||||
isQuitting = true;
|
||||
appState.isQuitting = true;
|
||||
if (daemon) daemon.quit();
|
||||
app.exit(1);
|
||||
});
|
||||
|
@ -240,14 +231,8 @@ const isSecondInstance = app.makeSingleInstance(argv => {
|
|||
URI = argv[1].replace(/\/$/, '').replace('/#', '#');
|
||||
}
|
||||
|
||||
if (rendererWindow && !rendererWindow.isDestroyed()) {
|
||||
rendererWindow.webContents.send('open-uri-requested', URI);
|
||||
|
||||
rendererWindow.show();
|
||||
rendererWindow.focus();
|
||||
} else {
|
||||
rendererWindow = createWindow(URI);
|
||||
}
|
||||
rendererWindow.webContents.send('open-uri-requested', URI);
|
||||
rendererWindow.show();
|
||||
});
|
||||
|
||||
if (isSecondInstance) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import { Menu, BrowserWindow } from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
|
||||
export default (rendererWindow: BrowserWindow) => {
|
||||
rendererWindow.webContents.on('context-menu', (e, params) => {
|
||||
|
@ -17,7 +18,7 @@ export default (rendererWindow: BrowserWindow) => {
|
|||
},
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDev) {
|
||||
template.push(...developmentTemplateAddition);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
// @flow
|
||||
import mixpanel from 'mixpanel-browser';
|
||||
import Lbryio from 'lbryio';
|
||||
import isDev from 'electron-is-dev';
|
||||
|
||||
mixpanel.init('691723e855cabb9d27a7a79002216967');
|
||||
if (isDev) {
|
||||
mixpanel.init('691723e855cabb9d27a7a79002216967');
|
||||
} else {
|
||||
mixpanel.init('af5c6b8110068fa4f5c4600c81f05e60');
|
||||
}
|
||||
|
||||
type Analytics = {
|
||||
track: (string, ?Object) => void,
|
||||
setUser: (Object) => void,
|
||||
toggle: (boolean, ?boolean) => void
|
||||
}
|
||||
setUser: Object => void,
|
||||
toggle: (boolean, ?boolean) => void,
|
||||
apiLog: (string, string, string) => void,
|
||||
};
|
||||
|
||||
let analyticsEnabled: boolean = false;
|
||||
|
||||
const analytics: Analytics = {
|
||||
track: (name: string, payload: ?Object): void => {
|
||||
if(analyticsEnabled) {
|
||||
if(payload) {
|
||||
if (analyticsEnabled) {
|
||||
if (payload) {
|
||||
mixpanel.track(name, payload);
|
||||
} else {
|
||||
mixpanel.track(name);
|
||||
|
@ -22,21 +29,30 @@ const analytics: Analytics = {
|
|||
}
|
||||
},
|
||||
setUser: (user: Object): void => {
|
||||
if(user.id) {
|
||||
if (user.id) {
|
||||
mixpanel.identify(user.id);
|
||||
}
|
||||
if(user.primary_email) {
|
||||
if (user.primary_email) {
|
||||
mixpanel.people.set({
|
||||
"$email": user.primary_email
|
||||
$email: user.primary_email,
|
||||
});
|
||||
}
|
||||
},
|
||||
toggle: (enabled: boolean, logDisabled: ?boolean): void => {
|
||||
if(!enabled && logDisabled) {
|
||||
if (!enabled && logDisabled) {
|
||||
mixpanel.track('DISABLED');
|
||||
}
|
||||
analyticsEnabled = enabled;
|
||||
}
|
||||
}
|
||||
},
|
||||
apiLog: (uri: string, outpoint: string, claimId: string): void => {
|
||||
if (analyticsEnabled) {
|
||||
Lbryio.call('file', 'view', {
|
||||
uri,
|
||||
outpoint,
|
||||
claim_id: claimId,
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default analytics;
|
||||
|
|
|
@ -2,6 +2,7 @@ import store from 'store';
|
|||
import { remote } from 'electron';
|
||||
import Path from 'path';
|
||||
import y18n from 'y18n';
|
||||
import isDev from 'electron-is-dev';
|
||||
|
||||
const env = process.env.NODE_ENV || 'production';
|
||||
const i18n = y18n({
|
||||
|
@ -22,7 +23,7 @@ const app = {
|
|||
};
|
||||
|
||||
// Workaround for https://github.com/electron-userland/electron-webpack/issues/52
|
||||
if (env !== 'development') {
|
||||
if (!isDev) {
|
||||
window.staticResourcesPath = Path.join(remote.app.getAppPath(), '../static').replace(
|
||||
/\\/g,
|
||||
'\\\\'
|
||||
|
|
67
src/renderer/component/file-exporter.js
Normal file
67
src/renderer/component/file-exporter.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'component/link';
|
||||
import parseData from 'util/parseData';
|
||||
import * as icons from 'constants/icons';
|
||||
const { remote } = require('electron');
|
||||
|
||||
class FileExporter extends React.PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
defaultPath: PropTypes.string,
|
||||
onFileCreated: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleFileCreation(filename, data) {
|
||||
const { onFileCreated } = this.props;
|
||||
fs.writeFile(filename, data, err => {
|
||||
if (err) throw err;
|
||||
// Do something after creation
|
||||
onFileCreated && onFileCreated(filename);
|
||||
});
|
||||
}
|
||||
|
||||
handleButtonClick() {
|
||||
const { title, defaultPath, data } = this.props;
|
||||
|
||||
const options = {
|
||||
title,
|
||||
defaultPath,
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }, { name: 'CSV', extensions: ['csv'] }],
|
||||
};
|
||||
|
||||
remote.dialog.showSaveDialog(options, filename => {
|
||||
// User hit cancel so do nothing:
|
||||
if (!filename) return;
|
||||
// Get extension and remove initial dot
|
||||
const format = path.extname(filename).replace(/\./g, '');
|
||||
// Parse data to string with the chosen format
|
||||
const parsed = parseData(data, format);
|
||||
// Write file
|
||||
parsed && this.handleFileCreation(filename, parsed);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, label } = this.props;
|
||||
return (
|
||||
<Link
|
||||
button="primary"
|
||||
icon={icons.DOWNLOAD}
|
||||
title={title || __('Export')}
|
||||
label={label || __('Export')}
|
||||
onClick={() => this.handleButtonClick()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileExporter;
|
|
@ -9,12 +9,33 @@ class FileList extends React.PureComponent {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
sortBy: 'date',
|
||||
sortBy: 'dateNew',
|
||||
};
|
||||
|
||||
this._sortFunctions = {
|
||||
date(fileInfos) {
|
||||
return fileInfos.slice().reverse();
|
||||
dateNew(fileInfos) {
|
||||
return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const height1 = fileInfo1.height;
|
||||
const height2 = fileInfo2.height;
|
||||
if (height1 > height2) {
|
||||
return -1;
|
||||
} else if (height1 < height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
dateOld(fileInfos) {
|
||||
return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const height1 = fileInfo1.height;
|
||||
const height2 = fileInfo2.height;
|
||||
if (height1 < height2) {
|
||||
return -1;
|
||||
} else if (height1 > height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
title(fileInfos) {
|
||||
return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
|
@ -95,7 +116,8 @@ class FileList extends React.PureComponent {
|
|||
<span className="sort-section">
|
||||
{__('Sort by')}{' '}
|
||||
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||
<option value="date">{__('Date')}</option>
|
||||
<option value="dateNew">{__('Newest First')}</option>
|
||||
<option value="dateOld">{__('Oldest First')}</option>
|
||||
<option value="title">{__('Title')}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
|
|
|
@ -14,6 +14,10 @@ const RewardSummary = (props: Props) => {
|
|||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h3>{__('Rewards')}</h3>
|
||||
<p className="help">
|
||||
{__('Read our')} <Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '}
|
||||
{__('to learn more about LBRY Rewards')}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{unclaimedRewardAmount > 0 ? (
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import TransactionListItem from './internal/TransactionListItem';
|
||||
import FormField from 'component/formField';
|
||||
import Link from 'component/link';
|
||||
import FileExporter from 'component/file-exporter.js';
|
||||
import * as icons from 'constants/icons';
|
||||
import * as modals from 'constants/modal_types';
|
||||
|
||||
|
@ -43,6 +44,13 @@ class TransactionList extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{Boolean(transactionList.length) && (
|
||||
<FileExporter
|
||||
data={transactionList}
|
||||
title={__('Export Transactions')}
|
||||
label={__('Export')}
|
||||
/>
|
||||
)}
|
||||
{(transactionList.length || this.state.filter) && (
|
||||
<span className="sort-section">
|
||||
{__('Filter')}{' '}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const { remote } = require('electron');
|
||||
|
||||
import { remote } from 'electron';
|
||||
import React from 'react';
|
||||
import { Thumbnail } from 'component/common';
|
||||
import player from 'render-media';
|
||||
|
@ -21,33 +20,21 @@ class VideoPlayer extends React.PureComponent {
|
|||
this.togglePlayListener = this.togglePlay.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const el = this.refs.media.children[0];
|
||||
if (!this.props.paused && next.paused && !el.paused) el.pause();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const container = this.refs.media;
|
||||
const {
|
||||
contentType,
|
||||
downloadPath,
|
||||
mediaType,
|
||||
changeVolume,
|
||||
volume,
|
||||
position,
|
||||
claim,
|
||||
uri,
|
||||
} = this.props;
|
||||
const container = this.media;
|
||||
const { contentType, changeVolume, volume, position, claim } = this.props;
|
||||
|
||||
const loadedMetadata = e => {
|
||||
const loadedMetadata = () => {
|
||||
this.setState({ hasMetadata: true, startedPlaying: true });
|
||||
this.refs.media.children[0].play();
|
||||
this.media.children[0].play();
|
||||
};
|
||||
const renderMediaCallback = err => {
|
||||
if (err) this.setState({ unplayable: true });
|
||||
|
||||
const renderMediaCallback = error => {
|
||||
if (error) this.setState({ unplayable: true });
|
||||
};
|
||||
|
||||
// Handle fullscreen change for the Windows platform
|
||||
const win32FullScreenChange = e => {
|
||||
const win32FullScreenChange = () => {
|
||||
const win = remote.BrowserWindow.getFocusedWindow();
|
||||
if (process.platform === 'win32') {
|
||||
win.setMenu(document.webkitIsFullScreen ? null : remote.Menu.getApplicationMenu());
|
||||
|
@ -61,13 +48,13 @@ class VideoPlayer extends React.PureComponent {
|
|||
player.append(
|
||||
this.file(),
|
||||
container,
|
||||
{ autoplay: false, controls: true },
|
||||
{ autoplay: true, controls: true },
|
||||
renderMediaCallback.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.togglePlayListener);
|
||||
const mediaElement = this.refs.media.children[0];
|
||||
const mediaElement = this.media.children[0];
|
||||
if (mediaElement) {
|
||||
mediaElement.currentTime = position || 0;
|
||||
mediaElement.addEventListener('play', () => this.props.doPlay());
|
||||
|
@ -87,29 +74,38 @@ class VideoPlayer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const el = this.media.children[0];
|
||||
if (!this.props.paused && next.paused && !el.paused) el.pause();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { contentType, downloadCompleted } = this.props;
|
||||
const { startedPlaying } = this.state;
|
||||
|
||||
if (this.playableType() && !startedPlaying && downloadCompleted) {
|
||||
const container = this.media.children[0];
|
||||
|
||||
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
|
||||
this.renderAudio(this.media, true);
|
||||
} else {
|
||||
player.render(this.file(), container, {
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.togglePlayListener);
|
||||
const mediaElement = this.refs.media.children[0];
|
||||
const mediaElement = this.media.children[0];
|
||||
if (mediaElement) {
|
||||
mediaElement.removeEventListener('click', this.togglePlayListener);
|
||||
}
|
||||
this.props.doPause();
|
||||
}
|
||||
|
||||
renderAudio(container, autoplay) {
|
||||
if (container.firstChild) {
|
||||
container.firstChild.remove();
|
||||
}
|
||||
|
||||
// clear the container
|
||||
const { downloadPath } = this.props;
|
||||
const audio = document.createElement('audio');
|
||||
audio.autoplay = autoplay;
|
||||
audio.controls = true;
|
||||
audio.src = downloadPath;
|
||||
container.appendChild(audio);
|
||||
}
|
||||
|
||||
togglePlay(event) {
|
||||
// ignore all events except click and spacebar keydown, or input events in a form control
|
||||
if (
|
||||
|
@ -119,7 +115,7 @@ class VideoPlayer extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const mediaElement = this.refs.media.children[0];
|
||||
const mediaElement = this.media.children[0];
|
||||
if (mediaElement) {
|
||||
if (!mediaElement.paused) {
|
||||
mediaElement.pause();
|
||||
|
@ -129,24 +125,6 @@ class VideoPlayer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { contentType, downloadCompleted } = this.props;
|
||||
const { startedPlaying } = this.state;
|
||||
|
||||
if (this.playableType() && !startedPlaying && downloadCompleted) {
|
||||
const container = this.refs.media.children[0];
|
||||
|
||||
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
|
||||
this.renderAudio(this.refs.media, true);
|
||||
} else {
|
||||
player.render(this.file(), container, {
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file() {
|
||||
const { downloadPath, filename } = this.props;
|
||||
|
||||
|
@ -162,14 +140,26 @@ class VideoPlayer extends React.PureComponent {
|
|||
return ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||
}
|
||||
|
||||
renderAudio(container, autoplay) {
|
||||
if (container.firstChild) {
|
||||
container.firstChild.remove();
|
||||
}
|
||||
|
||||
// clear the container
|
||||
const { downloadPath } = this.props;
|
||||
const audio = document.createElement('audio');
|
||||
audio.autoplay = autoplay;
|
||||
audio.controls = true;
|
||||
audio.src = downloadPath;
|
||||
container.appendChild(audio);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mediaType, poster } = this.props;
|
||||
const { hasMetadata, unplayable } = this.state;
|
||||
const noMetadataMessage = 'Waiting for metadata.';
|
||||
const unplayableMessage = "Sorry, looks like we can't play this file.";
|
||||
|
||||
const needsMetadata = this.playableType();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{['audio', 'application'].indexOf(mediaType) !== -1 &&
|
||||
|
@ -179,7 +169,12 @@ class VideoPlayer extends React.PureComponent {
|
|||
!hasMetadata &&
|
||||
!unplayable && <LoadingScreen status={noMetadataMessage} />}
|
||||
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
|
||||
<div ref="media" className="media" />
|
||||
<div
|
||||
ref={container => {
|
||||
this.media = container;
|
||||
}}
|
||||
className="media"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -164,6 +164,10 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
|
|||
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
|
||||
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
|
||||
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
|
||||
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
|
||||
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
|
||||
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
|
||||
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
|
||||
|
||||
// Video controls
|
||||
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
|
||||
|
|
|
@ -3,3 +3,4 @@ export const LOCAL = 'folder';
|
|||
export const FILE = 'file';
|
||||
export const HISTORY = 'history';
|
||||
export const HELP_CIRCLE = 'question-circle';
|
||||
export const DOWNLOAD = 'download';
|
||||
|
|
|
@ -2,7 +2,7 @@ export const CONFIRM_FILE_REMOVE = 'confirmFileRemove';
|
|||
export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon';
|
||||
export const FILE_TIMEOUT = 'file_timeout';
|
||||
export const DOWNLOADING = 'downloading';
|
||||
export const AUTO_UPDATE_DOWNLOADED = "auto_update_downloaded";
|
||||
export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded';
|
||||
export const AUTO_UPDATE_CONFIRM = 'auto_update_confirm';
|
||||
export const ERROR = 'error';
|
||||
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
|
||||
|
|
|
@ -8,10 +8,14 @@ import lbry from 'lbry';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { doConditionalAuthNavigate, doDaemonReady, doShowSnackBar, doAutoUpdate } from 'redux/actions/app';
|
||||
import { doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||
import {
|
||||
doConditionalAuthNavigate,
|
||||
doDaemonReady,
|
||||
doShowSnackBar,
|
||||
doAutoUpdate,
|
||||
} from 'redux/actions/app';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import { doDownloadLanguages } from 'redux/actions/settings';
|
||||
import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||
import { doUserEmailVerify } from 'redux/actions/user';
|
||||
import 'scss/all.scss';
|
||||
import store from 'store';
|
||||
|
@ -20,12 +24,7 @@ import analytics from './analytics';
|
|||
|
||||
const { autoUpdater } = remote.require('electron-updater');
|
||||
|
||||
autoUpdater.logger = remote.require("electron-log");
|
||||
|
||||
window.addEventListener('contextmenu', event => {
|
||||
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
|
||||
event.preventDefault();
|
||||
});
|
||||
autoUpdater.logger = remote.require('electron-log');
|
||||
|
||||
ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
|
||||
if (uri && uri.startsWith('lbry://')) {
|
||||
|
@ -62,6 +61,12 @@ ipcRenderer.on('window-is-focused', () => {
|
|||
dock.setBadge('');
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
document.addEventListener('drop', event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
document.addEventListener('click', event => {
|
||||
let { target } = event;
|
||||
while (target && target !== document) {
|
||||
|
@ -91,22 +96,21 @@ document.addEventListener('click', event => {
|
|||
});
|
||||
|
||||
const init = () => {
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
app.store.dispatch(doAutoUpdate());
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
console.log('Update available');
|
||||
});
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.log('Update not available');
|
||||
});
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
console.log('Update downloaded');
|
||||
app.store.dispatch(doAutoUpdate());
|
||||
});
|
||||
|
||||
if (["win32", "darwin"].includes(process.platform)) {
|
||||
autoUpdater.on("update-available", () => {
|
||||
console.log("Update available");
|
||||
});
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("Update not available");
|
||||
});
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
console.log("Update downloaded");
|
||||
app.store.dispatch(doAutoUpdate());
|
||||
});
|
||||
}
|
||||
app.store.dispatch(doUpdateIsNightAsync());
|
||||
app.store.dispatch(doDownloadLanguages());
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ jsonrpc.call = (
|
|||
return response.json().then(json => {
|
||||
let error;
|
||||
if (json.error) {
|
||||
error = new Error(json.error);
|
||||
error = new Error(json.error.message);
|
||||
} else {
|
||||
error = new Error('Protocol error with unknown response signature');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
|
||||
import ModalAutoUpdateConfirm from "./view";
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
|
||||
import ModalAutoUpdateConfirm from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { Modal } from "modal/modal";
|
||||
import { Line } from "rc-progress";
|
||||
import Link from "component/link/index";
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import { Line } from 'rc-progress';
|
||||
import Link from 'component/link/index';
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
class ModalAutoUpdateConfirm extends React.PureComponent {
|
||||
render() {
|
||||
|
@ -13,11 +13,11 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
|
|||
<Modal
|
||||
isOpen={true}
|
||||
type="confirm"
|
||||
contentLabel={__("Update Downloaded")}
|
||||
confirmButtonLabel={__("Upgrade")}
|
||||
abortButtonLabel={__("Not now")}
|
||||
contentLabel={__('Update Downloaded')}
|
||||
confirmButtonLabel={__('Upgrade')}
|
||||
abortButtonLabel={__('Not now')}
|
||||
onConfirmed={() => {
|
||||
ipcRenderer.send("autoUpdateAccepted");
|
||||
ipcRenderer.send('autoUpdateAccepted');
|
||||
}}
|
||||
onAborted={() => {
|
||||
declineAutoUpdate();
|
||||
|
@ -25,12 +25,8 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
|
|||
}}
|
||||
>
|
||||
<section>
|
||||
<h3 className="text-center">{__("LBRY Update Ready")}</h3>
|
||||
<p>
|
||||
{__(
|
||||
'Your LBRY update is ready. Restart LBRY now to use it!'
|
||||
)}
|
||||
</p>
|
||||
<h3 className="text-center">{__('LBRY Update Ready')}</h3>
|
||||
<p>{__('Your LBRY update is ready. Restart LBRY now to use it!')}</p>
|
||||
<p className="meta text-center">
|
||||
{__('Want to know what has changed?')} See the{' '}
|
||||
<Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
|
||||
import ModalAutoUpdateDownloaded from "./view";
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
|
||||
import ModalAutoUpdateDownloaded from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { Modal } from "modal/modal";
|
||||
import { Line } from "rc-progress";
|
||||
import Link from "component/link/index";
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import { Line } from 'rc-progress';
|
||||
import Link from 'component/link/index';
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
class ModalAutoUpdateDownloaded extends React.PureComponent {
|
||||
render() {
|
||||
|
@ -13,20 +13,20 @@ class ModalAutoUpdateDownloaded extends React.PureComponent {
|
|||
<Modal
|
||||
isOpen={true}
|
||||
type="confirm"
|
||||
contentLabel={__("Update Downloaded")}
|
||||
confirmButtonLabel={__("Use it Now")}
|
||||
abortButtonLabel={__("Upgrade on Close")}
|
||||
contentLabel={__('Update Downloaded')}
|
||||
confirmButtonLabel={__('Use it Now')}
|
||||
abortButtonLabel={__('Upgrade on Close')}
|
||||
onConfirmed={() => {
|
||||
ipcRenderer.send("autoUpdateAccepted");
|
||||
ipcRenderer.send('autoUpdateAccepted');
|
||||
}}
|
||||
onAborted={() => {
|
||||
declineAutoUpdate();
|
||||
ipcRenderer.send("autoUpdateDeclined");
|
||||
ipcRenderer.send('autoUpdateDeclined');
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
<section>
|
||||
<h3 className="text-center">{__("LBRY Leveled Up")}</h3>
|
||||
<h3 className="text-center">{__('LBRY Leveled Up')}</h3>
|
||||
<p>
|
||||
{__(
|
||||
'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.'
|
||||
|
|
|
@ -30,7 +30,12 @@ class ModalEmailCollection extends React.PureComponent {
|
|||
<Modal type="custom" isOpen contentLabel="Email">
|
||||
<section>
|
||||
<h3 className="modal__header">Can We Stay In Touch?</h3>
|
||||
{this.renderInner()}
|
||||
<div className="card__content">{this.renderInner()}</div>
|
||||
<div className="card__content">
|
||||
<div className="help">
|
||||
{`${__('Your email may be used to sync usage data across devices.')} `}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -68,7 +68,7 @@ export class AuthPage extends React.PureComponent {
|
|||
<div className="card__content">
|
||||
<div className="help">
|
||||
{`${__(
|
||||
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards.'
|
||||
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.'
|
||||
)} `}
|
||||
<Link onClick={() => navigate('/discover')} label={__('Return home')} />.
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { doFetchFileInfo } from 'redux/actions/file_info';
|
|||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||
import { selectRewardContentClaimIds } from 'redux/selectors/content';
|
||||
import { doFetchCostInfoForUri } from 'redux/actions/cost_info';
|
||||
import { checkSubscriptionLatest } from 'redux/actions/subscriptions';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
|
@ -13,6 +14,7 @@ import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
|
|||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import FilePage from './view';
|
||||
import { makeSelectCurrentParam } from 'redux/selectors/navigation';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
|
@ -23,12 +25,15 @@ const select = (state, props) => ({
|
|||
tab: makeSelectCurrentParam('tab')(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
checkSubscriptionLatest: (subscription, uri) =>
|
||||
dispatch(checkSubscriptionLatest(subscription, uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
||||
|
|
|
@ -17,6 +17,7 @@ class FilePage extends React.PureComponent {
|
|||
componentDidMount() {
|
||||
this.fetchFileInfo(this.props);
|
||||
this.fetchCostInfo(this.props);
|
||||
this.checkSubscriptionLatest(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -35,6 +36,28 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
checkSubscriptionLatest(props) {
|
||||
if (
|
||||
props.subscriptions
|
||||
.map(subscription => subscription.channelName)
|
||||
.indexOf(props.claim.channel_name) !== -1
|
||||
) {
|
||||
props.checkSubscriptionLatest(
|
||||
{
|
||||
channelName: props.claim.channel_name,
|
||||
uri: buildURI(
|
||||
{
|
||||
contentName: props.claim.channel_name,
|
||||
claimId: props.claim.value.publisherSignature.certificateId,
|
||||
},
|
||||
false
|
||||
),
|
||||
},
|
||||
buildURI({ contentName: props.claim.name, claimId: props.claim.claim_id }, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
|
|
|
@ -14,6 +14,8 @@ class SettingsPage extends React.PureComponent {
|
|||
this.state = {
|
||||
clearingCache: false,
|
||||
};
|
||||
|
||||
this.onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
|
@ -62,11 +64,16 @@ class SettingsPage extends React.PureComponent {
|
|||
|
||||
onThemeChange(event) {
|
||||
const { value } = event.target;
|
||||
|
||||
if (value === 'dark') {
|
||||
this.onAutomaticDarkModeChange(false);
|
||||
}
|
||||
|
||||
this.props.setClientSetting(settings.THEME, value);
|
||||
}
|
||||
|
||||
onAutomaticDarkModeChange(event) {
|
||||
this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, event.target.checked);
|
||||
onAutomaticDarkModeChange(value) {
|
||||
this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value);
|
||||
}
|
||||
|
||||
onInstantPurchaseEnabledChange(enabled) {
|
||||
|
@ -143,6 +150,7 @@ class SettingsPage extends React.PureComponent {
|
|||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<SubHeader />
|
||||
|
@ -300,7 +308,9 @@ class SettingsPage extends React.PureComponent {
|
|||
type="checkbox"
|
||||
onChange={this.onShareDataChange.bind(this)}
|
||||
defaultChecked={daemonSettings.share_usage_data}
|
||||
label={__('Help make LBRY better by contributing diagnostic data about my usage')}
|
||||
label={__(
|
||||
'Help make LBRY better by contributing analytics and diagnostic data and about my usage'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -325,8 +335,9 @@ class SettingsPage extends React.PureComponent {
|
|||
|
||||
<FormRow
|
||||
type="checkbox"
|
||||
onChange={this.onAutomaticDarkModeChange.bind(this)}
|
||||
defaultChecked={automaticDarkModeEnabled}
|
||||
disabled={theme === 'dark'}
|
||||
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
|
||||
checked={automaticDarkModeEnabled}
|
||||
label={__('Automatic dark mode (9pm to 8am)')}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
/* eslint-disable import/no-commonjs */
|
||||
import isDev from 'electron-is-dev';
|
||||
import Lbry from 'lbry';
|
||||
import path from 'path';
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
import Lbry from 'lbry';
|
||||
import Path from 'path';
|
||||
import { doFetchRewardedContent } from 'redux/actions/content';
|
||||
import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info';
|
||||
import { doAuthNavigate } from 'redux/actions/navigation';
|
||||
import { doFetchDaemonSettings } from 'redux/actions/settings';
|
||||
import { doAuthenticate } from 'redux/actions/user';
|
||||
import { doBalanceSubscribe } from 'redux/actions/wallet';
|
||||
import { doPause } from "redux/actions/media";
|
||||
import { doPause } from 'redux/actions/media';
|
||||
import { doCheckSubscriptions } from 'redux/actions/subscriptions';
|
||||
|
||||
import {
|
||||
selectCurrentModal,
|
||||
selectIsUpgradeSkipped,
|
||||
selectRemoteVersion,
|
||||
selectUpdateUrl,
|
||||
selectUpgradeDownloadItem,
|
||||
selectUpgradeDownloadPath,
|
||||
selectUpgradeFilename,
|
||||
selectAutoUpdateDeclined,
|
||||
} from 'redux/selectors/app';
|
||||
import { lbrySettings as config } from 'package.json';
|
||||
|
||||
const { autoUpdater } = remote.require('electron-updater');
|
||||
const { download } = remote.require('electron-dl');
|
||||
const Fs = remote.require('fs');
|
||||
const { lbrySettings: config } = require('package.json');
|
||||
|
||||
const CHECK_UPGRADE_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
|
@ -84,23 +84,19 @@ export function doDownloadUpgradeRequested() {
|
|||
|
||||
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
||||
|
||||
if (['win32', 'darwin'].includes(process.platform)) { // electron-updater behavior
|
||||
if (autoUpdateDeclined) {
|
||||
// The user declined an update before, so show the "confirm" dialog
|
||||
dispatch({
|
||||
type: ACTIONS.OPEN_MODAL,
|
||||
data: { modal: MODALS.AUTO_UPDATE_CONFIRM },
|
||||
});
|
||||
} else {
|
||||
// The user was never shown the original update dialog (e.g. because they were
|
||||
// watching a video). So show the inital "update downloaded" dialog.
|
||||
dispatch({
|
||||
type: ACTIONS.OPEN_MODAL,
|
||||
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
|
||||
});
|
||||
}
|
||||
} else { // Old behavior for Linux
|
||||
dispatch(doDownloadUpgrade());
|
||||
if (autoUpdateDeclined) {
|
||||
// The user declined an update before, so show the "confirm" dialog
|
||||
dispatch({
|
||||
type: ACTIONS.OPEN_MODAL,
|
||||
data: { modal: MODALS.AUTO_UPDATE_CONFIRM },
|
||||
});
|
||||
} else {
|
||||
// The user was never shown the original update dialog (e.g. because they were
|
||||
// watching a video). So show the initial "update downloaded" dialog.
|
||||
dispatch({
|
||||
type: ACTIONS.OPEN_MODAL,
|
||||
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -109,7 +105,7 @@ export function doDownloadUpgrade() {
|
|||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
// Make a new directory within temp directory so the filename is guaranteed to be available
|
||||
const dir = Fs.mkdtempSync(remote.app.getPath('temp') + Path.sep);
|
||||
const dir = Fs.mkdtempSync(remote.app.getPath('temp') + path.sep);
|
||||
const upgradeFilename = selectUpgradeFilename(state);
|
||||
|
||||
const options = {
|
||||
|
@ -127,7 +123,7 @@ export function doDownloadUpgrade() {
|
|||
type: ACTIONS.UPGRADE_DOWNLOAD_COMPLETED,
|
||||
data: {
|
||||
downloadItem,
|
||||
path: Path.join(dir, upgradeFilename),
|
||||
path: path.join(dir, upgradeFilename),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -145,8 +141,7 @@ export function doDownloadUpgrade() {
|
|||
}
|
||||
|
||||
export function doAutoUpdate() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
|
||||
});
|
||||
|
@ -159,12 +154,11 @@ export function doAutoUpdate() {
|
|||
}
|
||||
|
||||
export function doAutoUpdateDeclined() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.AUTO_UPDATE_DECLINED,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function doCancelUpgrade() {
|
||||
|
@ -181,6 +175,7 @@ export function doCancelUpgrade() {
|
|||
try {
|
||||
upgradeDownloadItem.cancel();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
// Do nothing
|
||||
}
|
||||
|
@ -197,47 +192,11 @@ export function doCheckUpgradeAvailable() {
|
|||
type: ACTIONS.CHECK_UPGRADE_START,
|
||||
});
|
||||
|
||||
if (["win32", "darwin"].includes(process.platform)) {
|
||||
// On Windows and Mac, updates happen silently through
|
||||
// electron-updater.
|
||||
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
||||
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
||||
|
||||
if (!autoUpdateDeclined) {
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
return;
|
||||
if (!autoUpdateDeclined && !isDev) {
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
const success = ({ remoteVersion, upgradeAvailable }) => {
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_UPGRADE_SUCCESS,
|
||||
data: {
|
||||
upgradeAvailable,
|
||||
remoteVersion,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
upgradeAvailable &&
|
||||
!selectCurrentModal(state) &&
|
||||
(!selectIsUpgradeSkipped(state) || remoteVersion !== selectRemoteVersion(state))
|
||||
) {
|
||||
dispatch({
|
||||
type: ACTIONS.OPEN_MODAL,
|
||||
data: {
|
||||
modal: MODALS.UPGRADE,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fail = () => {
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_UPGRADE_FAIL,
|
||||
});
|
||||
};
|
||||
|
||||
Lbry.getAppVersionInfo().then(success, fail);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -296,6 +255,7 @@ export function doDaemonReady() {
|
|||
dispatch(doCheckUpgradeAvailable());
|
||||
}
|
||||
dispatch(doCheckUpgradeSubscribe());
|
||||
dispatch(doCheckSubscriptions());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import Lbryio from 'lbryio';
|
|||
import { normalizeURI, buildURI } from 'lbryURI';
|
||||
import { doAlertError, doOpenModal } from 'redux/actions/app';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { setSubscriptionLatest } from 'redux/actions/subscriptions';
|
||||
import { selectBadgeNumber } from 'redux/selectors/app';
|
||||
import { selectMyClaimsRaw } from 'redux/selectors/claims';
|
||||
import { selectResolvingUris } from 'redux/selectors/content';
|
||||
|
@ -21,6 +22,7 @@ import { selectBalance } from 'redux/selectors/wallet';
|
|||
import batchActions from 'util/batchActions';
|
||||
import setBadge from 'util/setBadge';
|
||||
import setProgressBar from 'util/setProgressBar';
|
||||
import analytics from 'analytics';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||
|
||||
|
@ -226,11 +228,7 @@ export function doDownloadFile(uri, streamInfo) {
|
|||
return dispatch => {
|
||||
dispatch(doStartDownload(uri, streamInfo.outpoint));
|
||||
|
||||
Lbryio.call('file', 'view', {
|
||||
uri,
|
||||
outpoint: streamInfo.outpoint,
|
||||
claim_id: streamInfo.claim_id,
|
||||
}).catch(() => {});
|
||||
analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id);
|
||||
|
||||
dispatch(doClaimEligiblePurchaseRewards());
|
||||
};
|
||||
|
@ -288,7 +286,7 @@ export function doLoadVideo(uri) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doPurchaseUri(uri) {
|
||||
export function doPurchaseUri(uri, specificCostInfo) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = selectBalance(state);
|
||||
|
@ -321,7 +319,7 @@ export function doPurchaseUri(uri) {
|
|||
return;
|
||||
}
|
||||
|
||||
const costInfo = makeSelectCostInfoForUri(uri)(state);
|
||||
const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
|
||||
const { cost } = costInfo;
|
||||
|
||||
if (cost > balance) {
|
||||
|
@ -358,6 +356,25 @@ export function doFetchClaimsByChannel(uri, page) {
|
|||
const claimResult = result[uri] || {};
|
||||
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;
|
||||
|
||||
if (claimsInChannel && claimsInChannel.length) {
|
||||
const latest = claimsInChannel[0];
|
||||
dispatch(
|
||||
setSubscriptionLatest(
|
||||
{
|
||||
channelName: latest.channel_name,
|
||||
uri: buildURI(
|
||||
{
|
||||
contentName: latest.channel_name,
|
||||
claimId: latest.value.publisherSignature.certificateId,
|
||||
},
|
||||
false
|
||||
),
|
||||
},
|
||||
buildURI({ contentName: latest.name, claimId: latest.claim_id }, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
||||
data: {
|
||||
|
|
|
@ -56,6 +56,20 @@ export function doGetThemes() {
|
|||
};
|
||||
}
|
||||
|
||||
export function doUpdateIsNight() {
|
||||
const momentNow = moment();
|
||||
return {
|
||||
type: ACTIONS.UPDATE_IS_NIGHT,
|
||||
data: {
|
||||
isNight: (() => {
|
||||
const startNightMoment = moment('21:00', 'HH:mm');
|
||||
const endNightMoment = moment('8:00', 'HH:mm');
|
||||
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
|
||||
})(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function doUpdateIsNightAsync() {
|
||||
return dispatch => {
|
||||
dispatch(doUpdateIsNight());
|
||||
|
@ -66,19 +80,6 @@ export function doUpdateIsNightAsync() {
|
|||
};
|
||||
}
|
||||
|
||||
export function doUpdateIsNight() {
|
||||
const momentNow = moment();
|
||||
return {
|
||||
type: ACTIONS.UPDATE_IS_NIGHT,
|
||||
data: { isNight: (() => {
|
||||
const startNightMoment = moment('19:00', 'HH:mm');
|
||||
const endNightMoment = moment('8:00', 'HH:mm');
|
||||
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
|
||||
})()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadLanguage(langFile) {
|
||||
return dispatch => {
|
||||
const destinationPath = `${app.i18n.directory}/${langFile}`;
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import type { Subscription, Dispatch } from 'redux/reducers/subscriptions';
|
||||
import type { Subscription, Dispatch, SubscriptionState } from 'redux/reducers/subscriptions';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import Lbry from 'lbry';
|
||||
import { doPurchaseUri } from 'redux/actions/content';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import { buildURI } from 'lbryURI';
|
||||
|
||||
const CHECK_SUBSCRIPTIONS_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) =>
|
||||
dispatch({
|
||||
|
@ -14,5 +21,113 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D
|
|||
data: subscription,
|
||||
});
|
||||
|
||||
export const doCheckSubscriptions = () => (
|
||||
dispatch: Dispatch,
|
||||
getState: () => SubscriptionState
|
||||
) => {
|
||||
const checkSubscriptionsTimer = setInterval(
|
||||
() =>
|
||||
selectSubscriptions(getState()).map((subscription: Subscription) =>
|
||||
dispatch(doCheckSubscription(subscription))
|
||||
),
|
||||
CHECK_SUBSCRIPTIONS_INTERVAL
|
||||
);
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
|
||||
data: { checkSubscriptionsTimer },
|
||||
});
|
||||
};
|
||||
|
||||
export const doCheckSubscription = (subscription: Subscription) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
|
||||
data: subscription,
|
||||
});
|
||||
|
||||
Lbry.claim_list_by_channel({ uri: subscription.uri, page: 1 }).then(result => {
|
||||
const claimResult = result[subscription.uri] || {};
|
||||
const { claims_in_channel: claimsInChannel } = claimResult;
|
||||
|
||||
const count = subscription.latest
|
||||
? claimsInChannel.reduce(
|
||||
(prev, cur, index) =>
|
||||
buildURI({ contentName: cur.name, claimId: cur.claim_id }, false) ===
|
||||
subscription.latest
|
||||
? index
|
||||
: prev,
|
||||
-1
|
||||
)
|
||||
: 1;
|
||||
|
||||
if (count !== 0) {
|
||||
if (!claimsInChannel[0].value.stream.metadata.fee) {
|
||||
dispatch(
|
||||
doPurchaseUri(
|
||||
buildURI(
|
||||
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
|
||||
false
|
||||
),
|
||||
{ cost: 0 }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const notif = new window.Notification(subscription.channelName, {
|
||||
body: `Posted ${claimsInChannel[0].value.stream.metadata.title}${
|
||||
count > 1 ? ` and ${count - 1} other new items` : ''
|
||||
}${count < 0 ? ' and 9+ other new items' : ''}`,
|
||||
silent: false,
|
||||
});
|
||||
notif.onclick = () => {
|
||||
dispatch(
|
||||
doNavigate('/show', {
|
||||
uri: buildURI(
|
||||
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
|
||||
true
|
||||
),
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
//$FlowIssue
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
|
||||
data: subscription,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const checkSubscriptionLatest = (channel: Subscription, uri: string) => (
|
||||
dispatch: Dispatch
|
||||
) => {
|
||||
Lbry.claim_list_by_channel({ uri: channel.uri, page: 1 }).then(result => {
|
||||
const claimResult = result[channel.uri] || {};
|
||||
const { claims_in_channel: claimsInChannel } = claimResult;
|
||||
|
||||
if (
|
||||
claimsInChannel &&
|
||||
claimsInChannel.length &&
|
||||
buildURI(
|
||||
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
|
||||
false
|
||||
) === uri
|
||||
) {
|
||||
dispatch(setSubscriptionLatest(channel, uri));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
|
||||
dispatch: Dispatch
|
||||
) =>
|
||||
dispatch({
|
||||
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
|
||||
data: {
|
||||
subscription,
|
||||
uri,
|
||||
},
|
||||
});
|
||||
|
||||
export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) =>
|
||||
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS });
|
||||
|
|
|
@ -93,7 +93,7 @@ reducers[ACTIONS.AUTO_UPDATE_DECLINED] = state => {
|
|||
return Object.assign({}, state, {
|
||||
autoUpdateDeclined: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils';
|
|||
export type Subscription = {
|
||||
channelName: string,
|
||||
uri: string,
|
||||
latest: ?string,
|
||||
};
|
||||
|
||||
// Subscription redux types
|
||||
|
@ -28,7 +29,30 @@ type HasFetchedSubscriptions = {
|
|||
type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS,
|
||||
};
|
||||
|
||||
export type Action = doChannelSubscribe | doChannelUnsubscribe | HasFetchedSubscriptions;
|
||||
type setSubscriptionLatest = {
|
||||
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
|
||||
data: {
|
||||
subscription: Subscription,
|
||||
uri: string,
|
||||
},
|
||||
};
|
||||
|
||||
type CheckSubscriptionStarted = {
|
||||
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
|
||||
};
|
||||
|
||||
type CheckSubscriptionCompleted = {
|
||||
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| doChannelSubscribe
|
||||
| doChannelUnsubscribe
|
||||
| HasFetchedSubscriptions
|
||||
| setSubscriptionLatest
|
||||
| CheckSubscriptionStarted
|
||||
| CheckSubscriptionCompleted
|
||||
| Function;
|
||||
export type Dispatch = (action: Action) => any;
|
||||
|
||||
const defaultState = {
|
||||
|
@ -70,6 +94,18 @@ export default handleActions(
|
|||
...state,
|
||||
hasFetchedSubscriptions: true,
|
||||
}),
|
||||
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
|
||||
state: SubscriptionState,
|
||||
action: setSubscriptionLatest
|
||||
): SubscriptionState => ({
|
||||
...state,
|
||||
subscriptions: state.subscriptions.map(
|
||||
subscription =>
|
||||
subscription.channelName === action.data.subscription.channelName
|
||||
? { ...subscription, latest: action.data.uri }
|
||||
: subscription
|
||||
),
|
||||
}),
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -56,9 +56,15 @@ export const selectUpgradeDownloadPath = createSelector(selectState, state => st
|
|||
|
||||
export const selectUpgradeDownloadItem = createSelector(selectState, state => state.downloadItem);
|
||||
|
||||
export const selectAutoUpdateDownloaded = createSelector(selectState, state => state.autoUpdateDownloaded);
|
||||
export const selectAutoUpdateDownloaded = createSelector(
|
||||
selectState,
|
||||
state => state.autoUpdateDownloaded
|
||||
);
|
||||
|
||||
export const selectAutoUpdateDeclined = createSelector(selectState, state => state.autoUpdateDeclined);
|
||||
export const selectAutoUpdateDeclined = createSelector(
|
||||
selectState,
|
||||
state => state.autoUpdateDeclined
|
||||
);
|
||||
|
||||
export const selectModalsAllowed = createSelector(selectState, state => state.modalsAllowed);
|
||||
|
||||
|
|
38
src/renderer/util/parseData.js
Normal file
38
src/renderer/util/parseData.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Beautify JSON
|
||||
const parseJson = data => JSON.stringify(data, null, '\t');
|
||||
|
||||
// No need for an external module:
|
||||
// https://gist.github.com/btzr-io/55c3450ea3d709fc57540e762899fb85
|
||||
const parseCsv = data => {
|
||||
// Get items for header
|
||||
const getHeaders = temp =>
|
||||
Object.entries(temp)
|
||||
.map(([key]) => key)
|
||||
.join(',');
|
||||
// Get rows content
|
||||
const getData = list =>
|
||||
list
|
||||
.map(item => {
|
||||
const row = Object.entries(item)
|
||||
.map(([key, value]) => value)
|
||||
.join(',');
|
||||
return row;
|
||||
})
|
||||
.join('\n');
|
||||
// Return CSV string
|
||||
return `${getHeaders(data[0])} \n ${getData(data)}`;
|
||||
};
|
||||
|
||||
const parseData = (data, format) => {
|
||||
// Check for validation
|
||||
const valid = data && data[0] && format;
|
||||
// Pick a format
|
||||
const formats = {
|
||||
csv: list => parseCsv(list),
|
||||
json: list => parseJson(list),
|
||||
};
|
||||
// Return parsed data: JSON || CSV
|
||||
return valid && formats[format] ? formats[format](data) : undefined;
|
||||
};
|
||||
|
||||
export default parseData;
|
|
@ -2,10 +2,10 @@ import { remote } from 'electron';
|
|||
|
||||
const application = remote.app;
|
||||
const { dock } = application;
|
||||
const win = remote.BrowserWindow.getFocusedWindow();
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
const setBadge = text => {
|
||||
if (!dock) return;
|
||||
if (win.isFocused()) return;
|
||||
if (browserWindow.isFocused()) return;
|
||||
|
||||
dock.setBadge(text);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const path = require('path');
|
||||
const FlowFlowPlugin = require('./flowtype-plugin');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const ELECTRON_RENDERER_PROCESS_ROOT = path.resolve(__dirname, 'src/renderer/');
|
||||
|
||||
|
@ -23,7 +24,7 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDev) {
|
||||
module.exports.plugins = [
|
||||
new FlowFlowPlugin({
|
||||
warn: true,
|
||||
|
|
Loading…
Reference in a new issue