Compare commits
No commits in common. "master" and "0.7.2" have entirely different histories.
2040 changed files with 213093 additions and 63024 deletions
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,32 +0,0 @@
|
||||||
## PR Checklist
|
|
||||||
|
|
||||||
<!-- For the checkbox formatting to work properly, make sure there are no spaces on either side of the "x" -->
|
|
||||||
|
|
||||||
Please check all that apply to this PR using "x":
|
|
||||||
|
|
||||||
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
|
|
||||||
- [ ] I have checked that this PR does not introduce a breaking change
|
|
||||||
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
|
|
||||||
|
|
||||||
## PR Type
|
|
||||||
|
|
||||||
What kind of change does this PR introduce?
|
|
||||||
|
|
||||||
- [ ] Bugfix
|
|
||||||
- [ ] Feature
|
|
||||||
- [ ] Code style update (formatting)
|
|
||||||
- [ ] Refactoring (no functional changes)
|
|
||||||
- [ ] Documentation changes
|
|
||||||
- [ ] Other - Please describe:
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
Issue Number:
|
|
||||||
|
|
||||||
## What is the current behavior?
|
|
||||||
|
|
||||||
## What is the new behavior?
|
|
||||||
|
|
||||||
## Other information
|
|
||||||
|
|
||||||
<!-- If this PR contains a breaking change, please describe the impact and solution strategy for existing applications below. -->
|
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
@ -1 +0,0 @@
|
||||||
|
|
80
.gitignore
vendored
80
.gitignore
vendored
|
@ -1,69 +1,15 @@
|
||||||
# OSX
|
.buildozer
|
||||||
#
|
app/node_modules/
|
||||||
.DS_Store
|
bin
|
||||||
|
buildozer.spec
|
||||||
# Xcode
|
build.log
|
||||||
#
|
recipes/**/*.pyc
|
||||||
build/
|
src/main/assets/index.android.bundle
|
||||||
*.pbxuser
|
src/main/assets/index.android.bundle.meta
|
||||||
!default.pbxuser
|
|
||||||
*.mode1v3
|
|
||||||
!default.mode1v3
|
|
||||||
*.mode2v3
|
|
||||||
!default.mode2v3
|
|
||||||
*.perspectivev3
|
|
||||||
!default.perspectivev3
|
|
||||||
xcuserdata
|
|
||||||
*.xccheckout
|
|
||||||
*.moved-aside
|
|
||||||
DerivedData
|
|
||||||
*.hmap
|
|
||||||
*.ipa
|
|
||||||
*.xcuserstate
|
|
||||||
|
|
||||||
# Android/IntelliJ
|
|
||||||
#
|
|
||||||
build/
|
|
||||||
.idea
|
|
||||||
.gradle
|
|
||||||
local.properties
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
# node.js
|
|
||||||
#
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# BUCK
|
|
||||||
buck-out/
|
|
||||||
\.buckd/
|
|
||||||
*.keystore
|
|
||||||
!debug.keystore
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
#
|
|
||||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
|
||||||
# screenshots whenever they are needed.
|
|
||||||
# For more information about the recommended setup visit:
|
|
||||||
# https://docs.fastlane.tools/best-practices/source-control/
|
|
||||||
|
|
||||||
*/fastlane/report.xml
|
|
||||||
*/fastlane/Preview.html
|
|
||||||
*/fastlane/screenshots
|
|
||||||
|
|
||||||
# Bundle artifact
|
|
||||||
*.jsbundle
|
|
||||||
|
|
||||||
# CocoaPods
|
|
||||||
/ios/Pods/
|
|
||||||
|
|
||||||
# Other Files
|
|
||||||
app/google-services.json
|
|
||||||
app/twitter.properties
|
|
||||||
*.log
|
*.log
|
||||||
.vagrant
|
.vagrant
|
||||||
*.hprof
|
|
||||||
app/build
|
lbry-android.keystore
|
||||||
bin
|
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
||||||
app/debuglib
|
.gitsecret/keys/random_seed
|
||||||
|
|
||||||
|
|
|
@ -2,74 +2,57 @@ stages:
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
- release
|
- release
|
||||||
|
|
||||||
|
|
||||||
build apk:
|
build apk:
|
||||||
stage: build
|
stage: build
|
||||||
image: lbry/android-base:platform-28
|
image: lbry/android-base:latest
|
||||||
before_script:
|
before_script:
|
||||||
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||||
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
|
|
||||||
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
|
|
||||||
- apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign git-secret
|
|
||||||
- git secret reveal
|
|
||||||
- chmod u+x $CI_PROJECT_DIR/gradlew
|
|
||||||
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- bin/browser-*-release__arm.apk
|
- bin/browser-*-release.apk
|
||||||
- bin/browser-*-release__arm64.apk
|
|
||||||
expire_in: 1 week
|
|
||||||
script:
|
script:
|
||||||
- export PATH=/usr/bin:$PATH
|
- export PATH=/usr/bin:$PATH
|
||||||
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
|
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
||||||
- chmod u+x ./release.sh
|
- cd app
|
||||||
- ./release.sh
|
- npm install
|
||||||
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
|
- cd ..
|
||||||
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
|
- wget -q 'https://eu.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
|
||||||
|
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
|
||||||
|
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
- cp -f $CI_PROJECT_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||||
|
- cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
|
||||||
|
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
|
||||||
|
- git secret reveal
|
||||||
|
- mv buildozer.spec.travis buildozer.spec
|
||||||
|
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
|
||||||
|
- cp $CI_PROJECT_DIR/bin/browser-$BUILD_VERSION-release.apk /dev/null
|
||||||
|
|
||||||
deploy build.lbry.io:
|
deploy build.lbry.io:
|
||||||
image: python:stretch
|
image: python:latest
|
||||||
stage: deploy
|
stage: deploy
|
||||||
dependencies:
|
dependencies:
|
||||||
- build apk
|
- build apk
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get -y update && apt-get -y install apt-transport-https
|
|
||||||
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
|
||||||
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
|
|
||||||
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
|
|
||||||
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
|
|
||||||
- pip install awscli
|
- pip install awscli
|
||||||
- chmod u+x $CI_PROJECT_DIR/gradlew
|
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||||
- git secret reveal
|
- export BUILD_APK_FILENAME=browser-$BUILD_VERSION-release.apk
|
||||||
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
|
|
||||||
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
|
|
||||||
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
|
||||||
script:
|
script:
|
||||||
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/build-${CI_PIPELINE_IID}_commit-${CI_COMMIT_SHA:0:7}/$BUILD_APK_FILENAME__64
|
- aws s3 cp bin/$BUILD_APK_FILENAME s3://build.lbry.io/android/build-${CI_PIPELINE_IID}_commit-${CI_COMMIT_SHA:0:7}/$BUILD_APK_FILENAME
|
||||||
- aws s3 cp bin/$BUILD_APK_FILENAME__32 s3://build.lbry.io/android/build-${CI_PIPELINE_IID}_commit-${CI_COMMIT_SHA:0:7}/$BUILD_APK_FILENAME__32
|
- aws s3 cp bin/$BUILD_APK_FILENAME s3://build.lbry.io/android/push.apk
|
||||||
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/push.apk
|
|
||||||
|
|
||||||
release apk:
|
release apk:
|
||||||
image: python:stretch
|
image: python:latest
|
||||||
stage: release
|
stage: release
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
dependencies:
|
dependencies:
|
||||||
- build apk
|
- build apk
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get -y update && apt-get -y install apt-transport-https
|
|
||||||
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
|
||||||
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
|
|
||||||
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
|
|
||||||
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
|
|
||||||
- pip install awscli githubrelease
|
- pip install awscli githubrelease
|
||||||
- git secret reveal
|
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||||
- chmod u+x $CI_PROJECT_DIR/gradlew
|
- export BUILD_APK_FILENAME=browser-$BUILD_VERSION-release.apk
|
||||||
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
|
|
||||||
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
|
|
||||||
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
|
||||||
script:
|
script:
|
||||||
- githubrelease release lbryio/lbry-android create $CI_COMMIT_TAG --publish bin/$BUILD_APK_FILENAME__64 bin/$BUILD_APK_FILENAME__32
|
- githubrelease release lbryio/lbry-android create $CI_COMMIT_TAG --publish bin/$BUILD_APK_FILENAME
|
||||||
- githubrelease release lbryio/lbry-android edit $CI_COMMIT_TAG --draft
|
- aws s3 cp bin/$BUILD_APK_FILENAME s3://build.lbry.io/android/latest.apk
|
||||||
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/latest.apk
|
|
||||||
|
|
1
.gitmodules
vendored
1
.gitmodules
vendored
|
@ -1 +0,0 @@
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,3 +1,2 @@
|
||||||
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
|
lbry-android.keystore:
|
||||||
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
|
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
||||||
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033
|
|
||||||
|
|
95
.travis.yml
Normal file
95
.travis.yml
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
sudo: required
|
||||||
|
dist: xenial
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- '3.7'
|
||||||
|
install:
|
||||||
|
- deactivate
|
||||||
|
- export PATH=/usr/bin:$PATH
|
||||||
|
- export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
|
||||||
|
- sudo dpkg --add-architecture i386
|
||||||
|
- sudo add-apt-repository ppa:deadsnakes/ppa -y
|
||||||
|
- sudo apt-get -qq update
|
||||||
|
- sudo apt-get -qq install build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
|
||||||
|
- sudo pip install --upgrade cython==0.28.1 pip setuptools
|
||||||
|
- wget -q https://nodejs.org/dist/v8.11.1/node-v8.11.1-linux-x64.tar.xz
|
||||||
|
- tar -xf node-v8.11.1-linux-x64.tar.xz
|
||||||
|
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/node /usr/bin/node
|
||||||
|
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/npm /usr/bin/npm
|
||||||
|
- git clone https://github.com/lbryio/buildozer.git
|
||||||
|
- cd app
|
||||||
|
- npm config set registry="http://registry.npmjs.org/"
|
||||||
|
- npm install
|
||||||
|
- sudo npm install -g react-native-cli
|
||||||
|
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/react-native /usr/bin/react-native
|
||||||
|
- cd ..
|
||||||
|
- cd buildozer
|
||||||
|
- sudo python setup.py install
|
||||||
|
- cd ..
|
||||||
|
- mv buildozer.spec.travis buildozer.spec
|
||||||
|
- mkdir -p cd ~/.buildozer/android/platform/
|
||||||
|
- wget -q 'https://dist.testnet.lbry.tech/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
|
||||||
|
- wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/
|
||||||
|
- wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P ~/.buildozer/android/platform/
|
||||||
|
- wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip' -P ~/.buildozer/android/platform/
|
||||||
|
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
|
||||||
|
- cp -f $TRAVIS_BUILD_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||||
|
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
- tar -xf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/
|
||||||
|
- mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
|
||||||
|
- unzip -qq ~/.buildozer/android/platform/platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
|
||||||
|
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
|
||||||
|
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
- unzip -qq ~/.buildozer/android/platform/build-tools_r26.0.2-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
- mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.2
|
||||||
|
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
|
||||||
|
- echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||||
|
- echo $'\n84831b9409646a918e30573bab4c9c91346d8abd' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-preview-license
|
||||||
|
script:
|
||||||
|
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
|
||||||
|
- cp $TRAVIS_BUILD_DIR/bin/*.apk /dev/null
|
||||||
|
before_deploy:
|
||||||
|
- cd $TRAVIS_BUILD_DIR/bin
|
||||||
|
- export BUILD_VERSION=$(cat ../src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||||
|
- cp browser-$BUILD_VERSION-release.apk latest.apk
|
||||||
|
deploy:
|
||||||
|
- provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: m+FYX7vHZoiLSHHiJ2d3y8Fm4qSRoIVjEei+5BV17awiow/U8UKvy/5J1n8qfBdq+dpst5z58pTHCKWPbJz84C3z/posJ5mwEcOAaD/kxSAMHbtlaPW90pRWHUu3aW86UM/ggqtljE9Qz8KS/9a0xNUDfcXLkLgxuxgwodMcacEulAAc9TIOCUeR3IFI+KN0ptTCVahCu2JN8DCHKomaR+yKZHdo/9v9XCAcvmImSDu9nUDLH3+A7xQeRpPJqSspk1dadgdXP76kU8t3OKsYuM7DS5AoKvMIc9lZot4UYYKAx7/zavbzeEmqnyskULgsmV8/UDI1AV9U7uFBdrR6dSjISA1k6EHnCgqzasF+lp0hz5iE/0yPxlE9Z1kLW9gZgxSJtjr6Kv2uqAjHYYmpkjtTwHPwBugRM7PWMTxHNcPwkIHpBSRkXjpyDjkWd/LY4X866Y1g2BdIhbGshjy/9Fb2vnYxNZW6drLHn+wWeHJ41Vfgtg1cn01yZGJqgIkcTkhzNL6Bi++y8EBJXDr4L870s336SpbqRuIrO/C16ZFB+XnOg4Ty50Fk5zkbySMHII58iWqSyDYWNvhqo9zU9jn1XQQeok12129Y4t9TUOcJRbxhQ+511lCmVcFIkWHsXDK2QSZ7TeMK5GQUA8OvcNe+WLCJaQ/YD7OZvwlPTvc=
|
||||||
|
file_glob: true
|
||||||
|
file: browser-*-release.apk
|
||||||
|
overwrite: true
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
- provider: s3
|
||||||
|
access_key_id:
|
||||||
|
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
|
||||||
|
secret_access_key:
|
||||||
|
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
|
||||||
|
bucket: "build.lbry.io"
|
||||||
|
upload-dir: android
|
||||||
|
region: us-east-1
|
||||||
|
overwrite: true
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
- provider: s3
|
||||||
|
access_key_id:
|
||||||
|
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
|
||||||
|
secret_access_key:
|
||||||
|
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
|
||||||
|
bucket: "build.lbry.io"
|
||||||
|
upload-dir: "android/build-${TRAVIS_BUILD_NUMBER}_commit-${TRAVIS_COMMIT:0:7}"
|
||||||
|
region: us-east-1
|
||||||
|
overwrite: true
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
all_branches: true
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- secure: GS3Cp1QXiX8UPye3kdk2A2f3iFRr02sHKpY+RE+Zvx3Q7GDmhDuepHKzx6Hq5Os5fZN9Y/Bdds+XH+vLIRtT6XsWR7AONPhSifVY3XB5/2F+lDcZ538W8P8GZvXejpY4VecMUWHoWbuyt0s3PpaGXZJcHp8ir+CUJ0NUmU3I9w449pqj9/de2LHtG3qKH1lG0Xz58iOC0mmEeH451cQv3dDw851ihA4ak9vCTV1KKuMJUcv+2u6PxXGVX0mrJLEssjL6ze6G5iZUB4PM1vUpe3HqcVw8CSOa8O79BQxoB00qyA3WD+LpZDPpI0wh6gmBsR/2nCFyMJndJr3CjyB6lHdK7PgBoK0CJjszKawiZqg74O9DOjzTJTO2v9bnkfPrNxu4/3D/tbDg+whY8k5oV1sgDue9KAo/2aEEO0LGlKP4W3Qqt/lzRKsfpMVrMTdCNKJ8rG/wUFWw8ehOCmAsJaQ1saDOZDMNPLLuYpxFgmXFqWV5ThbUHgEJVj+G7qt6CMEussKvuZJoJZx24Pdk5Prr7ENzTyPmE5gk4b8WNfVNleOEC09xu5tFk2yOdzF1dawKsa1Mog6gImirTQ/INC/3BANdKoG9/cLJEIt9boJaFDXE1dpqoLVzoez9znHKOGSAU/1PaH3thjVnbUyO5z24PpPZ12zM3+3P8DbI454=
|
||||||
|
before_install:
|
||||||
|
- openssl aes-256-cbc -K $encrypted_b4c9b905b12e_key -iv $encrypted_b4c9b905b12e_iv
|
||||||
|
-in lbry-android.keystore.enc -out lbry-android.keystore -d
|
117
BUILD.md
Normal file
117
BUILD.md
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
## Linux Build Instructions
|
||||||
|
|
||||||
|
This app has currently only been built on Ubuntu 14.04, 16.04, 17.10, and 18.04, but these instructions, or an analog of them, should work on most Linux or macOS environments.
|
||||||
|
|
||||||
|
### Install Prerequisites
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
* JDK 1.8
|
||||||
|
* Android SDK
|
||||||
|
* Crystax Android NDK
|
||||||
|
* Buildozer
|
||||||
|
* Node.js
|
||||||
|
* npm
|
||||||
|
* yarn
|
||||||
|
|
||||||
|
#### apt Packages
|
||||||
|
Based on the quick-start instructions at http://buildozer.readthedocs.io/en/latest/installation.html
|
||||||
|
```
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install autoconf autogen build-essential curl libtool libffi-dev python python-pip python-openssl python3.7 python3.7-dev python3-pip ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
|
||||||
|
```
|
||||||
|
Alternatively, the JDK available from http://www.oracle.com/technetwork/java/javase/downloads/index.html can be installed instead of the `openjdk-8-jdk` package.
|
||||||
|
|
||||||
|
#### Install Cython and Setuptools
|
||||||
|
```
|
||||||
|
sudo pip install --upgrade cython==0.28.1 setuptools
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install buildozer
|
||||||
|
A forked version of `buildozer` needs to be installed in order to copy the React Native UI source files into the corresponding directories.
|
||||||
|
```
|
||||||
|
git clone https://github.com/lbryio/buildozer.git
|
||||||
|
cd buildozer
|
||||||
|
sudo python2.7 setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create buildozer.spec
|
||||||
|
Assuming `lbry-android` as the current working folder:
|
||||||
|
* Copy `buildozer.spec.sample` to `buildozer.spec` in the `lbry-android` folder. Running `buildozer init` instead will create a new `buildozer.spec` file.
|
||||||
|
* Update `buildozer.spec` settings to match your environment. The basic recommended settings are outlined below.
|
||||||
|
|
||||||
|
|
||||||
|
| Setting | Description |
|
||||||
|
|:------------------- |:-----------------------------|
|
||||||
|
| title | application title |
|
||||||
|
| package.name | package name (e.g. browser) |
|
||||||
|
| package.domain | package domain (e.g. io.lbry) |
|
||||||
|
| source.dir | the location of the application main.py |
|
||||||
|
| version | application version |
|
||||||
|
| requirements | the Python module requirements for building the application |
|
||||||
|
| services | list of Android background services and their corresponding Python entry points |
|
||||||
|
| android.permissions | Android manifest permissions required by the application. This should be set to `INTERNET` at the very least to enable internet connectivity |
|
||||||
|
| android.api | Android API version (Should be at least 23 for Gradle build support) |
|
||||||
|
| android.sdk | Android SDK version (Should be at least 23 for Gradle build support) |
|
||||||
|
| android.ndk | Android NDK version (not required when using crystax Android NDK) |
|
||||||
|
| android.ndk_path | Android NDK path. This should be set to the crystax Android NDK path) |
|
||||||
|
| android.sdk_path | Android SDK path. This should be set to the path where the Android SDK is manually set up (if not set up in the `.buildozer` path). |
|
||||||
|
| p4a.source_dir | Path to the python-for-android repository folder. Currently set to the included `p4a` folder |
|
||||||
|
| p4a.local_recipes | Path to a folder containing python_for_android recipes to be used in the build. The included `recipes` folder includes recipes for a successful build |
|
||||||
|
|
||||||
|
#### Setup Android SDK for buildozer
|
||||||
|
Download the Android SDK, platform and build tools archives.
|
||||||
|
* Android API 23 SDK - https://dl.google.com/android/android-sdk_r23-linux.tgz
|
||||||
|
* Android API 27 platform - https://dl.google.com/android/repository/platform-27_r01.zip
|
||||||
|
* Android build tools 26.0.1 - https://dl.google.com/android/repository/build-tools_r26.0.1-linux.
|
||||||
|
|
||||||
|
Create the `.buildozer` path (and the `android` sub-path) in your home folder if it doesn't already exist.
|
||||||
|
`mkdir ~/.buildozer`
|
||||||
|
`mkdir ~/.buildozer/android`
|
||||||
|
|
||||||
|
Extract the API 23 SDK to the `~/.buildozer/android` path and rename the extracted folder.
|
||||||
|
```
|
||||||
|
tar -xf android-sdk_r23-linux.tgz ~/.buildozer/android/platform/
|
||||||
|
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the API 27 platform archive into the `android-sdk-23` folder and rename the extracted folder.
|
||||||
|
```
|
||||||
|
unzip platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
|
||||||
|
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the build tools 26.0.1 build tools into the `android-sdk-23` folder and rename the extracted folder.
|
||||||
|
```
|
||||||
|
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
unzip ~/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, create the Android SDK license file. This prevents being prompted to accept the SDK license during the build process.
|
||||||
|
```
|
||||||
|
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
|
||||||
|
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Setup Crystax Android NDK for buildozer
|
||||||
|
* Download the Crystax Android NDK from https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz and extract. Remember to update `android.ndk_path` in your `buildozer.spec` to the path of the extracted Crystax NDK archive.
|
||||||
|
* Copy `build-target-python.sh` from the `scripts` folder in the cloned `lbry-android` repository to the `crystax-ndk-10.3.2/build/tools/` folder.
|
||||||
|
* Delete the `android-9` folder in `crystax-ndk-10.3.2/platforms`, and create a symbolic link named `android-9` to the `android-21` folder.
|
||||||
|
|
||||||
|
#### Build and Deploy
|
||||||
|
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface.
|
||||||
|
|
||||||
|
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
|
||||||
|
|
||||||
|
To build and deploy, you can run `./deploy.sh`. This requires a connected device or running Android emulator.
|
||||||
|
|
||||||
|
#### Development
|
||||||
|
If you already installed `Android SDK` and `adb`
|
||||||
|
|
||||||
|
* Run `adb reverse tcp:8081 tcp:8081`
|
||||||
|
* Then go to the `lbry-android/app` folder and run `npm start`
|
||||||
|
|
||||||
|
Note: You need to have your device connected with USB debugging.
|
||||||
|
|
||||||
|
Once the bundler is ready, run the LBRY Browser app on your device and then shake the device violently until you see the React Native dev menu. You can enable "Live Reloading" and "Hot Reloading" from this menu, so any changes you make to the React Native code will be visible as you save. This will only reload React Native Javascript files. Native Java code needs to be redeployed by running the command `./deploy.sh`
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017-2020 LBRY Inc
|
Copyright (c) 2017-2018 LBRY Inc
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|
110
QUICKSTART.md
Normal file
110
QUICKSTART.md
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
### Introduction
|
||||||
|
If you would like to contribute to the Android app, but find the build documentation a little daunting, this guide lets you copy-paste your way to a successful APK build.
|
||||||
|
|
||||||
|
#### Estimated build time
|
||||||
|
25 - 40 minutes (depending on Internet connection speeds)
|
||||||
|
|
||||||
|
#### What do you need?
|
||||||
|
* A computer running Ubuntu 18.04
|
||||||
|
* Internet access to download modules and packages.
|
||||||
|
* At least 15GB of free disk space.
|
||||||
|
* Alternatively, Docker. You can skip steps 1 through 5 if you make use of the `lbry/android-base` Docker base image. Scroll down to Fast track if you would prefer to use Docker.
|
||||||
|
|
||||||
|
### Step 1 of 10
|
||||||
|
Install all the apt packages required by running the following commands. You can copy-paste directly to your terminal.
|
||||||
|
```
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
sudo apt-get -y update
|
||||||
|
sudo apt-get install -y curl ca-certificates software-properties-common gpg-agent wget
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
|
||||||
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||||
|
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||||
|
sudo apt-get -y update && apt-get -y install autoconf autogen automake libtool libffi-dev \
|
||||||
|
build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 \
|
||||||
|
libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev \
|
||||||
|
python-pip openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 yarn gawk nodejs npm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 of 10
|
||||||
|
Install a couple of packages using the Python package installer
|
||||||
|
```
|
||||||
|
sudo -H pip install --upgrade cython==0.28.1 setuptools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3 of 10
|
||||||
|
Install buildozer, a tool for creating the apk package using the python for android toolcahin.
|
||||||
|
```
|
||||||
|
git clone https://github.com/lbryio/buildozer.git
|
||||||
|
cd buildozer && python2.7 setup.py install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4 of 10
|
||||||
|
The Android SDK needs to be setup for buildozer. This requires creating a few directories and downloading a number of files. Run the following commands to create the buildozer directory, download the required archives and extract them into their proper destination folders.
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p ~/.buildozer/android/platform
|
||||||
|
wget 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/ && \
|
||||||
|
wget 'https://dl.google.com/android/repository/platform-28_r06.zip' -P ~/.buildozer/android/platform/ && \
|
||||||
|
wget 'https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip' -P ~/.buildozer/android/platform/
|
||||||
|
tar -xvf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/ && \
|
||||||
|
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23 && \
|
||||||
|
unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms && \
|
||||||
|
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-9 ~/.buildozer/android/platform/android-sdk-23/platforms/android-28 && \
|
||||||
|
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools && \
|
||||||
|
unzip ~/.buildozer/android/platform/build-tools_r26.0.2-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools && \
|
||||||
|
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses && \
|
||||||
|
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5 of 10
|
||||||
|
Install the react-native-cli npm package.
|
||||||
|
```
|
||||||
|
sudo npm install -g react-native-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6 of 10
|
||||||
|
Install the Crystax NDK which is required for building Python 3.7 for the mobile app, and a number of native C / C++ modules and packages used by the app. Run the following commands to download and extract the NDK.
|
||||||
|
```
|
||||||
|
wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/ && \
|
||||||
|
tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ && \
|
||||||
|
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 && \
|
||||||
|
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7 of 10
|
||||||
|
Clone the lbryio/lbry-android git repository and create your buildozer.spec file. The provide buildozer.spec.sample contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to.
|
||||||
|
```
|
||||||
|
git clone https://github.com/lbryio/lbry-android
|
||||||
|
cd lbry-android
|
||||||
|
cp buildozer.spec.sample buildozer.spec
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8 of 10
|
||||||
|
Install the npm packages required for the app's React Native code.
|
||||||
|
```
|
||||||
|
cd app
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 9 of 10
|
||||||
|
Copy a couple of required files from the repository for the build to be successful.
|
||||||
|
```
|
||||||
|
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||||
|
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 10 of 10
|
||||||
|
If you made it this far, you're finally ready to build the package! You just have to run a single command to generate the APK.
|
||||||
|
```
|
||||||
|
buildozer android debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fast Track
|
||||||
|
Install Docker and start a container using the `lbry/android-base` image, which is about 1.72GB in size. Run the following commands for Ubuntu and then follow steps 6 through 10 in the container's bash prompt.
|
||||||
|
```
|
||||||
|
sudo apt-get install docker-ce
|
||||||
|
docker run -it lbry/android-base:latest /bin/bash
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protip:** You can also make use of Docker to run your builds on macOS or Windows.
|
25
README.md
25
README.md
|
@ -1,12 +1,9 @@
|
||||||
# LBRY Android
|
# LBRY Android
|
||||||
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
|
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
|
||||||
[![GitHub license](https://img.shields.io/github/license/lbryio/lbry-android)](https://github.com/lbryio/lbry-android/blob/master/LICENSE)
|
|
||||||
|
|
||||||
An Android browser and wallet for the [LBRY](https://lbry.com) network.
|
An Android browser and wallet for the [LBRY](https://lbry.com) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.
|
||||||
|
|
||||||
|
|
||||||
<img src="https://spee.ch/@lbry:3f/android-08-homepage.gif" alt="LBRY Android GIF" width="384px" />
|
|
||||||
|
|
||||||
|
<img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" />
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
||||||
|
@ -15,22 +12,10 @@ The minimum supported Android version is 5.0 Lollipop. There are two ways to ins
|
||||||
1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source.
|
1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
The app can be launched by opening **LBRY** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
|
The app can be launched by opening **LBRY Browser** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
|
||||||
|
|
||||||
## Running from Source
|
## Running from Source
|
||||||
Clone the repository and open the project in Android Studio. Android Studio will automatically run the initial build process.
|
The app is built from source via [Buildozer](https://github.com/kivy/buildozer). After cloning the repository, copy `buildozer.spec.sample` to `buildozer.spec` and modify this file as necessary for your environment. Please see [BUILD.md](BUILD.md) for detailed build instructions.
|
||||||
|
|
||||||
Create file 'twitter.properties' in app/ folder with the following content:
|
|
||||||
|
|
||||||
```
|
|
||||||
twitterConsumerKey=XXXXXX
|
|
||||||
|
|
||||||
twitterConsumerSecret=XXXXXX
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the file 'google-services.sample.json' to 'google-services.json' in the app/ folder.
|
|
||||||
|
|
||||||
Click the Sync button and when process finishes, the Run button to launch the app on your simulator or connected debugging device after the build process is complete.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
||||||
|
@ -39,7 +24,7 @@ Contributions to this project are welcome, encouraged, and compensated. For more
|
||||||
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://lbry.com/faq/pgp-key) if you need it.
|
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
||||||
|
|
100
Vagrantfile
vendored
Normal file
100
Vagrantfile
vendored
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
echoed=false
|
||||||
|
|
||||||
|
Vagrant.configure("2") do |config|
|
||||||
|
config.vm.box = "ubuntu/bionic64"
|
||||||
|
#config.disksize.size = "20GB"
|
||||||
|
|
||||||
|
config.vm.provider "virtualbox" do |v|
|
||||||
|
host = RbConfig::CONFIG['host_os']
|
||||||
|
|
||||||
|
# Give VM 1/4 system memory & access to all cpu cores on the host
|
||||||
|
if host =~ /darwin/
|
||||||
|
cpus = `sysctl -n hw.ncpu`.to_i
|
||||||
|
# sysctl returns Bytes and we need to convert to MB
|
||||||
|
mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / 4
|
||||||
|
elsif host =~ /linux/
|
||||||
|
cpus = `nproc`.to_i
|
||||||
|
# meminfo shows KB and we need to convert to MB
|
||||||
|
mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 / 4
|
||||||
|
else
|
||||||
|
cpus = `wmic cpu get NumberOfCores`.split("\n")[2].to_i
|
||||||
|
mem = `wmic OS get TotalVisibleMemorySize`.split("\n")[2].to_i / 1024 /4
|
||||||
|
end
|
||||||
|
|
||||||
|
mem = mem / 1024 / 4
|
||||||
|
mem = [mem, 2048].max # Minimum 2048
|
||||||
|
|
||||||
|
if echoed === false
|
||||||
|
echoed=true
|
||||||
|
puts("Memory", mem)
|
||||||
|
puts("CPUs", cpus)
|
||||||
|
end
|
||||||
|
|
||||||
|
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/home_vagrant_lbry-android", "1"]
|
||||||
|
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
|
||||||
|
v.customize ["modifyvm", :id, "--memory", mem]
|
||||||
|
v.customize ["modifyvm", :id, "--cpus", cpus]
|
||||||
|
end
|
||||||
|
|
||||||
|
config.vm.synced_folder "./", "/home/vagrant/lbry-android"
|
||||||
|
|
||||||
|
|
||||||
|
config.vm.provision "shell", inline: <<-SHELL
|
||||||
|
dpkg --add-architecture i386
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libssl-dev
|
||||||
|
apt-get install -y python3.6 python3.6-dev python3-pip autoconf libffi-dev pkg-config libtool build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 python-pip
|
||||||
|
pip install -f --upgrade setuptools pyopenssl
|
||||||
|
git clone https://github.com/lbryio/buildozer.git
|
||||||
|
cd buildozer
|
||||||
|
python2.7 setup.py install
|
||||||
|
cd ../
|
||||||
|
rm -rf ./buildozer
|
||||||
|
|
||||||
|
# Install additonal buildozer dependencies
|
||||||
|
sudo apt-get install cython
|
||||||
|
|
||||||
|
# Install node
|
||||||
|
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
export HOME=/home/vagrant
|
||||||
|
|
||||||
|
cp $HOME/lbry-android/buildozer.spec.vagrant $HOME/lbry-android/buildozer.spec
|
||||||
|
|
||||||
|
mkdir -p cd $HOME/.buildozer/android/platform/
|
||||||
|
wget -q 'https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P $HOME/.buildozer/android/
|
||||||
|
wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P $HOME/.buildozer/android/platform/
|
||||||
|
wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P $HOME/.buildozer/android/platform/
|
||||||
|
wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P $HOME/.buildozer/android/platform/
|
||||||
|
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C $HOME/.buildozer/android/
|
||||||
|
rm $HOME/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
|
||||||
|
ln -s $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
cp -f $HOME/lbry-android/scripts/build-target-python.sh $HOME/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||||
|
rm -rf $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||||
|
tar -xf $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz -C $HOME/.buildozer/android/platform/
|
||||||
|
rm $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz
|
||||||
|
mv $HOME/.buildozer/android/platform/android-sdk-linux $HOME/.buildozer/android/platform/android-sdk-23
|
||||||
|
unzip -qq $HOME/.buildozer/android/platform/android-23_r02.zip -d $HOME/.buildozer/android/platform/android-sdk-23/platforms
|
||||||
|
rm $HOME/.buildozer/android/platform/platform-27_r01.zip
|
||||||
|
mv $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-27
|
||||||
|
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
unzip -qq $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/build-tools
|
||||||
|
rm $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip
|
||||||
|
mv $HOME/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 $HOME/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
|
||||||
|
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/licenses
|
||||||
|
|
||||||
|
rm -rf $HOME/.buildozer/android/platform/android-sdk-23/tools
|
||||||
|
# https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
||||||
|
wget -q https://dl.google.com/android/repository/tools_r25.2.5-linux.zip
|
||||||
|
unzip -o -q ./tools_r25.2.5-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/
|
||||||
|
rm sdk-tools-linux-3859397.zip
|
||||||
|
|
||||||
|
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > $HOME/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||||
|
|
||||||
|
sudo chown -r vagrant $HOME
|
||||||
|
|
||||||
|
echo "Installing React Native via NPM..."
|
||||||
|
sudo npm install -g react-native-cli
|
||||||
|
SHELL
|
||||||
|
end
|
9
app/.babelrc
Normal file
9
app/.babelrc
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"presets": ["module:metro-react-native-babel-preset"],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
|
["module-resolver", {
|
||||||
|
root: ["./src"],
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
}
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/build
|
|
144
app/build.gradle
144
app/build.gradle
|
@ -1,144 +0,0 @@
|
||||||
import com.google.gms.googleservices.GoogleServicesPlugin
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 29
|
|
||||||
buildToolsVersion "29.0.2"
|
|
||||||
flavorDimensions "default"
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "io.lbry.browser"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 29
|
|
||||||
versionCode 1701
|
|
||||||
versionName "0.17.1"
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/DEPENDENCIES'
|
|
||||||
exclude 'lib/x86_64/darwin/libscrypt.dylib'
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
__32bit {
|
|
||||||
versionCode android.defaultConfig.versionCode * 10 + 1
|
|
||||||
ndk {
|
|
||||||
abiFilter "armeabi-v7a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
__64bit {
|
|
||||||
versionCode android.defaultConfig.versionCode * 10 + 2
|
|
||||||
ndk {
|
|
||||||
abiFilter "arm64-v8a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
Properties twitterProps = new Properties()
|
|
||||||
twitterProps.load(project.file('twitter.properties').newDataInputStream())
|
|
||||||
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
|
|
||||||
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
Properties twitterProps = new Properties()
|
|
||||||
twitterProps.load(project.file('twitter.properties').newDataInputStream())
|
|
||||||
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
|
|
||||||
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task printVersionName {
|
|
||||||
doLast {
|
|
||||||
println android.defaultConfig.versionName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
all {
|
|
||||||
exclude module: 'httpclient'
|
|
||||||
exclude module: 'commons-logging'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha01'
|
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
|
||||||
implementation 'androidx.navigation:navigation-fragment:2.3.1'
|
|
||||||
implementation 'androidx.navigation:navigation-ui:2.3.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
|
||||||
implementation 'androidx.webkit:webkit:1.4.0-rc01'
|
|
||||||
implementation 'androidx.camera:camera-core:1.0.0-beta03'
|
|
||||||
implementation 'androidx.camera:camera-camera2:1.0.0-beta03'
|
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.0.0-beta03'
|
|
||||||
implementation 'androidx.camera:camera-view:1.0.0-alpha10'
|
|
||||||
implementation 'androidx.browser:browser:1.2.0'
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.4.1'
|
|
||||||
implementation 'com.google.firebase:firebase-analytics:18.0.0'
|
|
||||||
implementation 'com.google.android.gms:play-services-base:17.5.0'
|
|
||||||
implementation 'com.google.firebase:firebase-messaging:21.0.0'
|
|
||||||
implementation 'com.google.oauth-client:google-oauth-client:1.30.4'
|
|
||||||
|
|
||||||
implementation 'com.android.billingclient:billing:3.0.2'
|
|
||||||
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.2'
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.2'
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.2'
|
|
||||||
implementation 'com.google.android.exoplayer:extension-cast:2.12.2'
|
|
||||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.12.2'
|
|
||||||
|
|
||||||
implementation 'com.google.android:flexbox:2.0.1'
|
|
||||||
|
|
||||||
implementation 'com.hbb20:ccp:2.3.8'
|
|
||||||
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
|
||||||
implementation 'com.atlassian.commonmark:commonmark:0.14.0'
|
|
||||||
|
|
||||||
implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.3.1.LTS'
|
|
||||||
|
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
|
||||||
implementation 'org.bitcoinj:bitcoinj-tools:0.14.7'
|
|
||||||
implementation 'org.java-websocket:Java-WebSocket:1.5.1'
|
|
||||||
|
|
||||||
implementation ('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
|
|
||||||
implementation 'com.google.zxing:core:3.3.0'
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.10'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.10'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
|
||||||
|
|
||||||
__32bitImplementation 'io.lbry:lbrysdk32:0.102.0'
|
|
||||||
__64bitImplementation 'io.lbry:lbrysdk64:0.102.0'
|
|
||||||
//__64bitImplementation(name: 'lbrysdk', ext: 'aar')
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
GoogleServicesPlugin.config.disableVersionCheck = true
|
|
2
app/bundle.sh
Executable file
2
app/bundle.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
react-native bundle --platform android --dev false --entry-file src/index.js --bundle-output ../src/main/assets/index.android.bundle --assets-dest ../src/main/res/
|
Binary file not shown.
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "861521963586",
|
|
||||||
"firebase_url": "https://lbry-mobile-builds-debug.firebaseio.com",
|
|
||||||
"project_id": "lbry-mobile-builds-debug",
|
|
||||||
"storage_bucket": "lbry-mobile-builds-debug.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:861521963586:android:592958d248940ab2",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.lbry.browser"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "861521963586-60cmvg5nmnrqkrc11a7bpmpv5ra2d50q.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyC7A3BYcIdZP9-Q-VNHoexYJWgZA7WzsPI"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "861521963586-60cmvg5nmnrqkrc11a7bpmpv5ra2d50q.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
3
app/index.js
Normal file
3
app/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import LBRYApp from './src/index';
|
||||||
|
|
||||||
|
export default LBRYApp;
|
8189
app/package-lock.json
generated
Normal file
8189
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
47
app/package.json
Normal file
47
app/package.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "LBRYApp",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": "true",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node node_modules/react-native/local-cli/cli.js start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"base-64": "^0.1.0",
|
||||||
|
"@expo/vector-icons": "^8.1.0",
|
||||||
|
"lbry-redux": "lbryio/lbry-redux",
|
||||||
|
"lbryinc": "lbryio/lbryinc",
|
||||||
|
"lodash": ">=4.17.11",
|
||||||
|
"merge": ">=1.2.1",
|
||||||
|
"moment": "^2.22.1",
|
||||||
|
"react": "16.8.6",
|
||||||
|
"react-native": "0.59.3",
|
||||||
|
"@react-native-community/async-storage": "^1.2.2",
|
||||||
|
"react-native-country-picker-modal": "^0.6.2",
|
||||||
|
"react-native-exception-handler": "2.9.0",
|
||||||
|
"react-native-fast-image": "^5.0.3",
|
||||||
|
"react-native-gesture-handler": "^1.1.0",
|
||||||
|
"react-native-image-zoom-viewer": "^2.2.5",
|
||||||
|
"react-native-password-strength-meter": "^0.0.2",
|
||||||
|
"react-native-phone-input": "lbryio/react-native-phone-input",
|
||||||
|
"react-native-vector-icons": "^6.4.2",
|
||||||
|
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||||
|
"react-navigation": "^3.11.0",
|
||||||
|
"react-navigation-redux-helpers": "^3.0.2",
|
||||||
|
"react-redux": "^5.0.3",
|
||||||
|
"redux": "^3.6.0",
|
||||||
|
"redux-persist": "^4.10.2",
|
||||||
|
"redux-persist-filesystem-storage": "^1.3.2",
|
||||||
|
"redux-persist-transform-compress": "^4.2.0",
|
||||||
|
"redux-persist-transform-filter": "0.0.18",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
|
"rn-fetch-blob": "^0.10.15"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.4.3",
|
||||||
|
"babel-preset-env": "^1.6.1",
|
||||||
|
"babel-preset-react-native": "5.0.2",
|
||||||
|
"babel-preset-stage-2": "^6.18.0",
|
||||||
|
"babel-plugin-module-resolver": "^3.1.1",
|
||||||
|
"flow-babel-webpack-plugin": "^1.1.1"
|
||||||
|
}
|
||||||
|
}
|
21
app/proguard-rules.pro
vendored
21
app/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
|
@ -1,27 +0,0 @@
|
||||||
package io.lbry.browser;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
public void useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
||||||
|
|
||||||
assertEquals("io.lbry.browser", appContext.getPackageName());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package io.lbry.browser.utils;
|
|
||||||
|
|
||||||
import androidx.test.filters.SmallTest;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
@SmallTest
|
|
||||||
public class HelperTest {
|
|
||||||
|
|
||||||
}
|
|
BIN
app/src/assets/default_avatar.jpg
Normal file
BIN
app/src/assets/default_avatar.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
392
app/src/component/AppNavigator.js
Normal file
392
app/src/component/AppNavigator.js
Normal file
|
@ -0,0 +1,392 @@
|
||||||
|
import React from 'react';
|
||||||
|
import AboutPage from 'page/about';
|
||||||
|
import DiscoverPage from 'page/discover';
|
||||||
|
import DownloadsPage from 'page/downloads';
|
||||||
|
import DrawerContent from 'component/drawerContent';
|
||||||
|
import FilePage from 'page/file';
|
||||||
|
import FirstRunScreen from 'page/firstRun';
|
||||||
|
import RewardsPage from 'page/rewards';
|
||||||
|
import TrendingPage from 'page/trending';
|
||||||
|
import SearchPage from 'page/search';
|
||||||
|
import SettingsPage from 'page/settings';
|
||||||
|
import SplashScreen from 'page/splash';
|
||||||
|
import SubscriptionsPage from 'page/subscriptions';
|
||||||
|
import TransactionHistoryPage from 'page/transactionHistory';
|
||||||
|
import VerificationScreen from 'page/verification';
|
||||||
|
import WalletPage from 'page/wallet';
|
||||||
|
import {
|
||||||
|
createDrawerNavigator,
|
||||||
|
createStackNavigator,
|
||||||
|
NavigationActions
|
||||||
|
} from 'react-navigation';
|
||||||
|
import {
|
||||||
|
createReduxContainer,
|
||||||
|
createReactNavigationReduxMiddleware,
|
||||||
|
createNavigationReducer
|
||||||
|
} from 'react-navigation-redux-helpers';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
AppState,
|
||||||
|
BackHandler,
|
||||||
|
Linking,
|
||||||
|
NativeModules,
|
||||||
|
TextInput,
|
||||||
|
ToastAndroid
|
||||||
|
} from 'react-native';
|
||||||
|
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||||
|
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
doGetSync,
|
||||||
|
doUserCheckEmailVerified,
|
||||||
|
doUserEmailVerify,
|
||||||
|
doUserEmailVerifyFailure,
|
||||||
|
selectEmailToVerify,
|
||||||
|
selectEmailVerifyIsPending,
|
||||||
|
selectEmailVerifyErrorMessage,
|
||||||
|
selectUser
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { decode as atob } from 'base-64';
|
||||||
|
import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import Constants from 'constants';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import NavigationButton from 'component/navigationButton';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
import searchStyle from 'styles/search';
|
||||||
|
import SearchRightHeaderIcon from 'component/searchRightHeaderIcon';
|
||||||
|
|
||||||
|
const menuNavigationButton = (navigation) => <NavigationButton
|
||||||
|
name="bars"
|
||||||
|
size={24}
|
||||||
|
style={discoverStyle.drawerMenuButton}
|
||||||
|
iconStyle={discoverStyle.drawerHamburger}
|
||||||
|
onPress={() => navigation.openDrawer() } />
|
||||||
|
|
||||||
|
const discoverStack = createStackNavigator({
|
||||||
|
Discover: {
|
||||||
|
screen: DiscoverPage,
|
||||||
|
navigationOptions: ({ navigation }) => ({
|
||||||
|
title: 'Explore',
|
||||||
|
header: null
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
File: {
|
||||||
|
screen: FilePage,
|
||||||
|
navigationOptions: ({ navigation }) => ({
|
||||||
|
header: null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Search: {
|
||||||
|
screen: SearchPage,
|
||||||
|
navigationOptions: ({ navigation }) => ({
|
||||||
|
header: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
headerMode: 'screen',
|
||||||
|
transitionConfig: () => ({ screenInterpolator: () => null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
discoverStack.navigationOptions = ({ navigation }) => {
|
||||||
|
let drawerLockMode = 'unlocked';
|
||||||
|
/*if (navigation.state.index > 0) {
|
||||||
|
drawerLockMode = 'locked-closed';
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawerLockMode
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const walletStack = createStackNavigator({
|
||||||
|
Wallet: {
|
||||||
|
screen: WalletPage,
|
||||||
|
navigationOptions: ({ navigation }) => ({
|
||||||
|
title: 'Wallet',
|
||||||
|
header: null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
TransactionHistory: {
|
||||||
|
screen: TransactionHistoryPage,
|
||||||
|
navigationOptions: {
|
||||||
|
title: 'Transaction History',
|
||||||
|
header: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
headerMode: 'screen',
|
||||||
|
transitionConfig: () => ({ screenInterpolator: () => null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawer = createDrawerNavigator({
|
||||||
|
DiscoverStack: { screen: discoverStack, navigationOptions: {
|
||||||
|
title: 'Explore', drawerIcon: ({ tintColor }) => <Icon name="home" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
TrendingStack: { screen: TrendingPage, navigationOptions: {
|
||||||
|
title: 'Trending', drawerIcon: ({ tintColor }) => <Icon name="fire" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
MySubscriptionsStack: { screen: SubscriptionsPage, navigationOptions: {
|
||||||
|
title: 'Subscriptions', drawerIcon: ({ tintColor }) => <Icon name="heart" solid={true} size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
WalletStack: { screen: walletStack, navigationOptions: {
|
||||||
|
title: 'Wallet', drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
Rewards: { screen: RewardsPage, navigationOptions: {
|
||||||
|
drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
MyLBRYStack: { screen: DownloadsPage, navigationOptions: {
|
||||||
|
title: 'Library', drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
Settings: { screen: SettingsPage, navigationOptions: {
|
||||||
|
drawerLockMode: 'locked-closed',
|
||||||
|
drawerIcon: ({ tintColor }) => <Icon name="cog" size={20} style={{ color: tintColor }} />
|
||||||
|
}},
|
||||||
|
About: { screen: AboutPage, navigationOptions: {
|
||||||
|
drawerLockMode: 'locked-closed',
|
||||||
|
drawerIcon: ({ tintColor }) => <Icon name="info" size={20} style={{ color: tintColor }} />
|
||||||
|
}}
|
||||||
|
}, {
|
||||||
|
drawerWidth: 300,
|
||||||
|
headerMode: 'none',
|
||||||
|
contentComponent: DrawerContent,
|
||||||
|
contentOptions: {
|
||||||
|
activeTintColor: Colors.LbryGreen,
|
||||||
|
labelStyle: discoverStyle.menuText
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainStackNavigator = new createStackNavigator({
|
||||||
|
FirstRun: {
|
||||||
|
screen: FirstRunScreen,
|
||||||
|
navigationOptions: {
|
||||||
|
drawerLockMode: 'locked-closed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Splash: {
|
||||||
|
screen: SplashScreen,
|
||||||
|
navigationOptions: {
|
||||||
|
drawerLockMode: 'locked-closed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Main: {
|
||||||
|
screen: drawer
|
||||||
|
},
|
||||||
|
Verification: {
|
||||||
|
screen: VerificationScreen,
|
||||||
|
navigationOptions: {
|
||||||
|
drawerLockMode: 'locked-closed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
headerMode: 'none'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const AppNavigator = mainStackNavigator;
|
||||||
|
export const navigatorReducer = createNavigationReducer(AppNavigator);
|
||||||
|
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(
|
||||||
|
state => state.nav,
|
||||||
|
);
|
||||||
|
|
||||||
|
const App = createReduxContainer(mainStackNavigator);
|
||||||
|
const appMapStateToProps = (state) => ({
|
||||||
|
state: state.nav,
|
||||||
|
});
|
||||||
|
const ReduxAppNavigator = connect(appMapStateToProps)(App);
|
||||||
|
|
||||||
|
class AppWithNavigationState extends React.Component {
|
||||||
|
static supportedDisplayTypes = ['toast'];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.emailVerifyCheckInterval = null;
|
||||||
|
this.state = {
|
||||||
|
emailVerifyDone: false,
|
||||||
|
verifyPending: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
AppState.addEventListener('change', this._handleAppStateChange);
|
||||||
|
BackHandler.addEventListener('hardwareBackPress', function() {
|
||||||
|
const { dispatch, nav, drawerStack } = this.props;
|
||||||
|
// There should be a better way to check this
|
||||||
|
if (nav.routes.length > 0) {
|
||||||
|
if (nav.routes[0].routeName === 'Main') {
|
||||||
|
const mainRoute = nav.routes[0];
|
||||||
|
if (mainRoute.index > 0 ||
|
||||||
|
mainRoute.routes[0].index > 0 /* Discover stack index */ ||
|
||||||
|
mainRoute.routes[4].index > 0 /* Wallet stack index */ ||
|
||||||
|
mainRoute.index >= 5 /* Settings and About screens */) {
|
||||||
|
dispatchNavigateBack(dispatch, nav, drawerStack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.emailVerifyCheckInterval = setInterval(() => this.checkEmailVerification(), 5000);
|
||||||
|
Linking.addEventListener('url', this._handleUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmailVerification = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
AsyncStorage.getItem(Constants.KEY_EMAIL_VERIFY_PENDING).then(pending => {
|
||||||
|
this.setState({ verifyPending: ('true' === pending) });
|
||||||
|
if ('true' === pending) {
|
||||||
|
dispatch(doUserCheckEmailVerified());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||||
|
BackHandler.removeEventListener('hardwareBackPress');
|
||||||
|
Linking.removeEventListener('url', this._handleUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { dispatch, user } = this.props;
|
||||||
|
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
|
||||||
|
clearInterval(this.emailVerifyCheckInterval);
|
||||||
|
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
|
||||||
|
this.setState({ verifyPending: false });
|
||||||
|
|
||||||
|
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
|
||||||
|
|
||||||
|
// upon successful email verification, do wallet sync (if password has been set)
|
||||||
|
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||||
|
if (walletPassword && walletPassword.trim().length > 0) {
|
||||||
|
dispatch(doGetSync(walletPassword));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate(nextProps) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const {
|
||||||
|
toast,
|
||||||
|
emailToVerify,
|
||||||
|
emailVerifyPending,
|
||||||
|
emailVerifyErrorMessage,
|
||||||
|
user
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
|
if (toast) {
|
||||||
|
const { message } = toast;
|
||||||
|
let currentDisplayType;
|
||||||
|
if (!currentDisplayType && message) {
|
||||||
|
// default to toast if no display type set and there is a message specified
|
||||||
|
currentDisplayType = 'toast';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('toast' === currentDisplayType) {
|
||||||
|
ToastAndroid.show(message, ToastAndroid.LONG);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(doDismissToast());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user &&
|
||||||
|
!emailVerifyPending &&
|
||||||
|
!this.state.emailVerifyDone &&
|
||||||
|
(emailToVerify || emailVerifyErrorMessage)) {
|
||||||
|
AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => {
|
||||||
|
if ('true' === shouldVerify) {
|
||||||
|
this.setState({ emailVerifyDone: true });
|
||||||
|
const message = emailVerifyErrorMessage ?
|
||||||
|
String(emailVerifyErrorMessage) : 'Your email address was successfully verified.';
|
||||||
|
if (!emailVerifyErrorMessage) {
|
||||||
|
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL);
|
||||||
|
dispatch(doToast({ message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleAppStateChange = (nextAppState) => {
|
||||||
|
const { backgroundPlayEnabled, dispatch } = this.props;
|
||||||
|
// Check if the app was suspended
|
||||||
|
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||||
|
AsyncStorage.getItem('firstLaunchTime').then(start => {
|
||||||
|
if (start !== null && !isNaN(parseInt(start, 10))) {
|
||||||
|
// App suspended during first launch?
|
||||||
|
// If so, this needs to be included as a property when tracking
|
||||||
|
AsyncStorage.setItem('firstLaunchSuspended', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background media
|
||||||
|
if (backgroundPlayEnabled && NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||||
|
const { title, channel, uri } = window.currentMediaInfo;
|
||||||
|
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||||
|
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
|
||||||
|
NativeModules.BackgroundMedia.hidePlaybackNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleUrl = (evt) => {
|
||||||
|
const { dispatch, nav } = this.props;
|
||||||
|
if (evt.url) {
|
||||||
|
if (evt.url.startsWith('lbry://?verify=')) {
|
||||||
|
this.setState({ emailVerifyDone: false });
|
||||||
|
let verification = {};
|
||||||
|
try {
|
||||||
|
verification = JSON.parse(atob(evt.url.substring(15)));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verification.token && verification.recaptcha) {
|
||||||
|
AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true');
|
||||||
|
try {
|
||||||
|
dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
|
||||||
|
} catch (error) {
|
||||||
|
const message = 'Invalid Verification Token';
|
||||||
|
dispatch(doUserEmailVerifyFailure(message));
|
||||||
|
dispatch(doToast({ message }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(doToast({
|
||||||
|
message: 'Invalid Verification URI',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatchNavigateToUri(dispatch, nav, evt.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <ReduxAppNavigator />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||||
|
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||||
|
nav: state.nav,
|
||||||
|
toast: selectToast(state),
|
||||||
|
drawerStack: selectDrawerStack(state),
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
|
emailVerifyPending: selectEmailVerifyIsPending(state),
|
||||||
|
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
|
||||||
|
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
|
||||||
|
user: selectUser(state)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(AppWithNavigationState);
|
7
app/src/component/address/index.js
Normal file
7
app/src/component/address/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import Address from './view';
|
||||||
|
|
||||||
|
export default connect(null, {
|
||||||
|
doToast,
|
||||||
|
})(Address);
|
28
app/src/component/address/view.js
Normal file
28
app/src/component/address/view.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Clipboard, Text, View } from 'react-native';
|
||||||
|
import Button from '../button';
|
||||||
|
import walletStyle from '../../styles/wallet';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
address: string,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Address extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { address, doToast, style } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[walletStyle.row, style]}>
|
||||||
|
<Text selectable={true} numberOfLines={1} style={walletStyle.address}>{address || ''}</Text>
|
||||||
|
<Button icon={'clipboard'} style={walletStyle.button} onPress={() => {
|
||||||
|
Clipboard.setString(address);
|
||||||
|
doToast({
|
||||||
|
message: 'Address copied',
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
4
app/src/component/button/index.js
Normal file
4
app/src/component/button/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Button from './view';
|
||||||
|
|
||||||
|
export default connect(null, null)(Button);
|
58
app/src/component/button/view.js
Normal file
58
app/src/component/button/view.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
|
import buttonStyle from '../../styles/button';
|
||||||
|
import Colors from '../../styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
|
||||||
|
export default class Button extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
disabled,
|
||||||
|
style,
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
iconColor,
|
||||||
|
solid,
|
||||||
|
theme,
|
||||||
|
onPress,
|
||||||
|
onLayout
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let styles = [buttonStyle.button, buttonStyle.row];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
styles.push(buttonStyle.disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textStyles = [buttonStyle.text];
|
||||||
|
if (icon && icon.trim().length > 0) {
|
||||||
|
textStyles.push(buttonStyle.textWithIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme === 'light') {
|
||||||
|
textStyles.push(buttonStyle.textDark);
|
||||||
|
} else {
|
||||||
|
// Dark background, default
|
||||||
|
textStyles.push(buttonStyle.textLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderIcon = (<Icon name={icon} size={18} color={iconColor ? iconColor : ('light' === theme ? Colors.DarkGrey : Colors.White)} />);
|
||||||
|
if (solid) {
|
||||||
|
renderIcon = (<Icon name={icon} size={18} color={iconColor ? iconColor : ('light' === theme ? Colors.DarkGrey : Colors.White)} solid />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress} onLayout={onLayout}>
|
||||||
|
{icon && renderIcon}
|
||||||
|
{text && (text.trim().length > 0) && <Text style={textStyles}>{text}</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
4
app/src/component/categoryList/index.js
Normal file
4
app/src/component/categoryList/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import CategoryList from './view';
|
||||||
|
|
||||||
|
export default connect(null, null)(CategoryList);
|
39
app/src/component/categoryList/view.js
Normal file
39
app/src/component/categoryList/view.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import NavigationActions from 'react-navigation';
|
||||||
|
import { FlatList, Text, View } from 'react-native';
|
||||||
|
import { normalizeURI } from 'lbry-redux';
|
||||||
|
import FileItem from '/component/fileItem';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
|
||||||
|
class CategoryList extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const { category, categoryMap, navigation } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
style={discoverStyle.horizontalScrollContainer}
|
||||||
|
contentContainerStyle={discoverStyle.horizontalScrollPadding}
|
||||||
|
initialNumToRender={3}
|
||||||
|
maxToRenderPerBatch={3}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
renderItem={ ({item}) => (
|
||||||
|
<FileItem
|
||||||
|
style={discoverStyle.fileItem}
|
||||||
|
mediaStyle={discoverStyle.fileItemMedia}
|
||||||
|
key={item}
|
||||||
|
uri={normalizeURI(item)}
|
||||||
|
navigation={navigation}
|
||||||
|
showDetails={true}
|
||||||
|
compactView={false} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
horizontal={true}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
data={categoryMap[category]}
|
||||||
|
keyExtractor={(item, index) => item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryList;
|
26
app/src/component/customRewardCard/index.js
Normal file
26
app/src/component/customRewardCard/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
doClaimRewardType,
|
||||||
|
doClaimRewardClearError,
|
||||||
|
makeSelectClaimRewardError,
|
||||||
|
makeSelectIsRewardClaimPending,
|
||||||
|
rewards as REWARD_TYPES
|
||||||
|
} from 'lbryinc';
|
||||||
|
import CustomRewardCard from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
rewardIsPending: makeSelectIsRewardClaimPending()(state, {
|
||||||
|
reward_type: REWARD_TYPES.TYPE_REWARD_CODE,
|
||||||
|
}),
|
||||||
|
error: makeSelectClaimRewardError()(state, { reward_type: REWARD_TYPES.TYPE_REWARD_CODE }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
|
||||||
|
clearError: reward => dispatch(doClaimRewardClearError(reward)),
|
||||||
|
notify: data => dispatch(doToast(data)),
|
||||||
|
submitRewardCode: code => dispatch(doClaimRewardType(REWARD_TYPES.TYPE_REWARD_CODE, { params: { code } }))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(CustomRewardCard);
|
88
app/src/component/customRewardCard/view.js
Normal file
88
app/src/component/customRewardCard/view.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
import Colors from '../../styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import Button from '../button';
|
||||||
|
import Link from '../link';
|
||||||
|
import rewardStyle from '../../styles/reward';
|
||||||
|
|
||||||
|
class CustomRewardCard extends React.PureComponent<Props> {
|
||||||
|
state = {
|
||||||
|
claimStarted: false,
|
||||||
|
rewardCode: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const { error, rewardIsPending } = nextProps;
|
||||||
|
const { clearError, notify } = this.props;
|
||||||
|
if (this.state.claimStarted && !rewardIsPending) {
|
||||||
|
if (error && error.trim().length > 0) {
|
||||||
|
notify({ message: error });
|
||||||
|
} else {
|
||||||
|
notify({ message: 'Reward successfully claimed!' });
|
||||||
|
this.setState({ rewardCode: '' });
|
||||||
|
}
|
||||||
|
this.setState({ claimStarted: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClaimPress = () => {
|
||||||
|
const { canClaim, notify, showVerification, submitRewardCode } = this.props;
|
||||||
|
const { rewardCode } = this.state;
|
||||||
|
|
||||||
|
Keyboard.dismiss();
|
||||||
|
|
||||||
|
if (!canClaim) {
|
||||||
|
if (showVerification) {
|
||||||
|
showVerification();
|
||||||
|
}
|
||||||
|
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rewardCode || rewardCode.trim().length === 0) {
|
||||||
|
notify({ message: 'Please enter a reward code to claim.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ claimStarted: true }, () => {
|
||||||
|
submitRewardCode(rewardCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { canClaim, rewardIsPending } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[rewardStyle.rewardCard, rewardStyle.row]} >
|
||||||
|
<View style={rewardStyle.leftCol}>
|
||||||
|
{rewardIsPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
|
||||||
|
</View>
|
||||||
|
<View style={rewardStyle.midCol}>
|
||||||
|
<Text style={rewardStyle.rewardTitle}>Custom Code</Text>
|
||||||
|
<Text style={rewardStyle.rewardDescription}>Are you a supermodel or rockstar that received a custom reward code? Claim it here.</Text>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<TextInput style={rewardStyle.customCodeInput}
|
||||||
|
placeholder={"0123abc"}
|
||||||
|
onChangeText={text => this.setState({ rewardCode: text })}
|
||||||
|
value={this.state.rewardCode} />
|
||||||
|
<Button style={rewardStyle.redeemButton}
|
||||||
|
text={"Redeem"}
|
||||||
|
disabled={(!this.state.rewardCode || this.state.rewardCode.trim().length === 0 || rewardIsPending)}
|
||||||
|
onPress={() => {
|
||||||
|
if (!rewardIsPending) { this.onClaimPress(); }
|
||||||
|
}} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={rewardStyle.rightCol}>
|
||||||
|
<Text style={rewardStyle.rewardAmount}>?</Text>
|
||||||
|
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomRewardCard;
|
16
app/src/component/dateTime/index.js
Normal file
16
app/src/component/dateTime/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectDateForUri } from 'lbry-redux';
|
||||||
|
import DateTime from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
date: props.date || makeSelectDateForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchBlock: height => dispatch(doFetchBlock(height)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(DateTime);
|
55
app/src/component/dateTime/view.js
Normal file
55
app/src/component/dateTime/view.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
date?: number,
|
||||||
|
timeAgo?: boolean,
|
||||||
|
formatOptions: {},
|
||||||
|
show?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class DateTime extends React.PureComponent<Props> {
|
||||||
|
static SHOW_DATE = 'date';
|
||||||
|
static SHOW_TIME = 'time';
|
||||||
|
static SHOW_BOTH = 'both';
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
formatOptions: {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { date, formatOptions, timeAgo, style, textStyle } = this.props;
|
||||||
|
const show = this.props.show || DateTime.SHOW_BOTH;
|
||||||
|
const locale = 'en-US'; // default to en-US until we get a working i18n module for RN
|
||||||
|
|
||||||
|
if (timeAgo) {
|
||||||
|
return date ? <View style={style}><Text style={textStyle}>{moment(date).from(moment())}</Text></View> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: formatOptions not working as expected in RN
|
||||||
|
// date.toLocaleDateString([locale, 'en-US'], formatOptions)}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<Text style={textStyle}>
|
||||||
|
{date &&
|
||||||
|
(show === DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) &&
|
||||||
|
moment(date).format('MMMM D, YYYY')}
|
||||||
|
{show === DateTime.SHOW_BOTH && ' '}
|
||||||
|
{date &&
|
||||||
|
(show === DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) &&
|
||||||
|
date.toLocaleTimeString()}
|
||||||
|
{!date && '...'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateTime;
|
4
app/src/component/drawerContent/index.js
Normal file
4
app/src/component/drawerContent/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import DrawerContent from './view';
|
||||||
|
|
||||||
|
export default connect()(DrawerContent);
|
38
app/src/component/drawerContent/view.js
Normal file
38
app/src/component/drawerContent/view.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { DrawerItems, SafeAreaView } from 'react-navigation';
|
||||||
|
import { ScrollView } from 'react-native';
|
||||||
|
import Constants from 'constants';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
|
||||||
|
class DrawerContent extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const { navigation, onItemPress } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<SafeAreaView style={discoverStyle.drawerContentContainer} forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||||
|
<DrawerItems
|
||||||
|
{...props}
|
||||||
|
onItemPress={(route) => {
|
||||||
|
const { routeName } = route.route;
|
||||||
|
if (Constants.FULL_ROUTE_NAME_DISCOVER === routeName) {
|
||||||
|
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_DISCOVER });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Constants.FULL_ROUTE_NAME_WALLET === routeName) {
|
||||||
|
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_WALLET });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemPress(route);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DrawerContent;
|
25
app/src/component/fileDownloadButton/index.js
Normal file
25
app/src/component/fileDownloadButton/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doPurchaseUri,
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectDownloadingForUri,
|
||||||
|
makeSelectLoadingForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
|
import { doStartDownload } from 'redux/actions/file';
|
||||||
|
import FileDownloadButton from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
downloading: makeSelectDownloadingForUri(props.uri)(state),
|
||||||
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
loading: makeSelectLoadingForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
|
||||||
|
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||||
|
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileDownloadButton);
|
103
app/src/component/fileDownloadButton/view.js
Normal file
103
app/src/component/fileDownloadButton/view.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||||
|
import Button from '../button';
|
||||||
|
import fileDownloadButtonStyle from 'styles/fileDownloadButton';
|
||||||
|
|
||||||
|
class FileDownloadButton extends React.PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
const { costInfo, fetchCostInfo, uri } = this.props;
|
||||||
|
if (costInfo === undefined) {
|
||||||
|
fetchCostInfo(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
//this.checkAvailability(nextProps.uri);
|
||||||
|
//this.restartDownload(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
restartDownload(props) {
|
||||||
|
const { downloading, fileInfo, uri, restartDownload } = props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!downloading &&
|
||||||
|
fileInfo &&
|
||||||
|
!fileInfo.completed &&
|
||||||
|
fileInfo.written_bytes !== false &&
|
||||||
|
fileInfo.written_bytes < fileInfo.total_bytes
|
||||||
|
) {
|
||||||
|
restartDownload(uri, fileInfo.outpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fileInfo,
|
||||||
|
downloading,
|
||||||
|
uri,
|
||||||
|
purchaseUri,
|
||||||
|
costInfo,
|
||||||
|
isPlayable,
|
||||||
|
isViewable,
|
||||||
|
onPlay,
|
||||||
|
onView,
|
||||||
|
loading,
|
||||||
|
doPause,
|
||||||
|
style,
|
||||||
|
openFile,
|
||||||
|
onButtonLayout,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
|
||||||
|
const progress =
|
||||||
|
fileInfo && fileInfo.written_bytes ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
|
||||||
|
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||||
|
<View style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}></View>
|
||||||
|
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (!fileInfo && !downloading) {
|
||||||
|
if (!costInfo) {
|
||||||
|
return (
|
||||||
|
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||||
|
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button icon={isPlayable ? 'play' : null}
|
||||||
|
text={(isPlayable ? 'Play' : (isViewable ? 'View' : 'Download'))}
|
||||||
|
onLayout={onButtonLayout}
|
||||||
|
style={[style, fileDownloadButtonStyle.container]} onPress={() => {
|
||||||
|
if (NativeModules.Firebase) {
|
||||||
|
NativeModules.Firebase.track('purchase_uri', { uri: uri });
|
||||||
|
}
|
||||||
|
purchaseUri(uri, costInfo, !isPlayable);
|
||||||
|
if (NativeModules.UtilityModule) {
|
||||||
|
NativeModules.UtilityModule.checkDownloads();
|
||||||
|
}
|
||||||
|
if (isPlayable && onPlay) {
|
||||||
|
this.props.onPlay();
|
||||||
|
}
|
||||||
|
if (isViewable && onView) {
|
||||||
|
this.props.onView();
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
} else if (fileInfo && fileInfo.download_path) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onLayout={onButtonLayout}
|
||||||
|
style={[style, fileDownloadButtonStyle.container]} onPress={openFile}>
|
||||||
|
<Text style={fileDownloadButtonStyle.text}>{isViewable ? 'View' : 'Open'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileDownloadButton;
|
32
app/src/component/fileItem/index.js
Normal file
32
app/src/component/fileItem/index.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectClaimIsNsfw
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { selectRewardContentClaimIds } from 'lbryinc';
|
||||||
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
|
import FileItem from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
|
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileItem);
|
104
app/src/component/fileItem/view.js
Normal file
104
app/src/component/fileItem/view.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { normalizeURI } from 'lbry-redux';
|
||||||
|
import { NavigationActions } from 'react-navigation';
|
||||||
|
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||||
|
import { navigateToUri } from 'utils/helper';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import DateTime from 'component/dateTime';
|
||||||
|
import FileItemMedia from 'component/fileItemMedia';
|
||||||
|
import FilePrice from 'component/filePrice';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import NsfwOverlay from 'component/nsfwOverlay';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
|
||||||
|
class FileItem extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.resolve(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.resolve(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(props) {
|
||||||
|
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||||
|
|
||||||
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
|
resolveUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToFileUri = () => {
|
||||||
|
const { navigation, uri } = this.props;
|
||||||
|
const normalizedUri = normalizeURI(uri);
|
||||||
|
if (NativeModules.Firebase) {
|
||||||
|
NativeModules.Firebase.track('explore_click', { uri: normalizedUri });
|
||||||
|
}
|
||||||
|
navigateToUri(navigation, normalizedUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
title,
|
||||||
|
thumbnail,
|
||||||
|
fileInfo,
|
||||||
|
metadata,
|
||||||
|
isResolvingUri,
|
||||||
|
rewardedContentClaimIds,
|
||||||
|
style,
|
||||||
|
mediaStyle,
|
||||||
|
navigation,
|
||||||
|
showDetails,
|
||||||
|
compactView,
|
||||||
|
titleBeforeThumbnail
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const uri = normalizeURI(this.props.uri);
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||||
|
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||||
|
const channelName = claim ? claim.channel_name : null;
|
||||||
|
const channelClaimId = claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
|
||||||
|
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
|
||||||
|
const height = claim ? claim.height : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<TouchableOpacity style={discoverStyle.container} onPress={this.navigateToFileUri}>
|
||||||
|
{!compactView && titleBeforeThumbnail && <Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>{title}</Text>}
|
||||||
|
<FileItemMedia title={title}
|
||||||
|
thumbnail={thumbnail}
|
||||||
|
blurRadius={obscureNsfw ? 15 : 0}
|
||||||
|
resizeMode="cover"
|
||||||
|
isResolvingUri={isResolvingUri}
|
||||||
|
style={mediaStyle} />
|
||||||
|
|
||||||
|
{(!compactView && fileInfo && fileInfo.completed && fileInfo.download_path) &&
|
||||||
|
<Icon style={discoverStyle.downloadedIcon} solid={true} color={Colors.NextLbryGreen} name={"folder"} size={16} />}
|
||||||
|
{(!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path)) &&
|
||||||
|
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />}
|
||||||
|
{!compactView && <View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
|
||||||
|
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>{title}</Text>
|
||||||
|
{isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />}
|
||||||
|
</View>}
|
||||||
|
{(!compactView && showDetails) &&
|
||||||
|
<View style={discoverStyle.detailsRow}>
|
||||||
|
{channelName &&
|
||||||
|
<Link style={discoverStyle.channelName} text={channelName} onPress={() => {
|
||||||
|
navigateToUri(navigation, normalizeURI(fullChannelUri));
|
||||||
|
}} />}
|
||||||
|
<DateTime style={discoverStyle.dateTime} textStyle={discoverStyle.dateTimeText} timeAgo uri={uri} />
|
||||||
|
</View>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileItem;
|
7
app/src/component/fileItemMedia/index.js
Normal file
7
app/src/component/fileItemMedia/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FileItemMedia from './view';
|
||||||
|
|
||||||
|
const select = state => ({});
|
||||||
|
const perform = dispatch => ({});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileItemMedia);
|
108
app/src/component/fileItemMedia/view.js
Normal file
108
app/src/component/fileItemMedia/view.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, Image, Text, View } from 'react-native';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
|
import fileItemMediaStyle from 'styles/fileItemMedia';
|
||||||
|
|
||||||
|
class FileItemMedia extends React.PureComponent {
|
||||||
|
static AUTO_THUMB_STYLES = [
|
||||||
|
fileItemMediaStyle.autothumbPurple,
|
||||||
|
fileItemMediaStyle.autothumbRed,
|
||||||
|
fileItemMediaStyle.autothumbPink,
|
||||||
|
fileItemMediaStyle.autothumbIndigo,
|
||||||
|
fileItemMediaStyle.autothumbBlue,
|
||||||
|
fileItemMediaStyle.autothumbLightBlue,
|
||||||
|
fileItemMediaStyle.autothumbCyan,
|
||||||
|
fileItemMediaStyle.autothumbTeal,
|
||||||
|
fileItemMediaStyle.autothumbGreen,
|
||||||
|
fileItemMediaStyle.autothumbYellow,
|
||||||
|
fileItemMediaStyle.autothumbOrange,
|
||||||
|
];
|
||||||
|
|
||||||
|
state: {
|
||||||
|
imageLoadFailed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({
|
||||||
|
autoThumbStyle:
|
||||||
|
FileItemMedia.AUTO_THUMB_STYLES[
|
||||||
|
Math.floor(Math.random() * FileItemMedia.AUTO_THUMB_STYLES.length)
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFastImageResizeMode(resizeMode) {
|
||||||
|
switch (resizeMode) {
|
||||||
|
case "contain":
|
||||||
|
return FastImage.resizeMode.contain;
|
||||||
|
case "stretch":
|
||||||
|
return FastImage.resizeMode.stretch;
|
||||||
|
case "center":
|
||||||
|
return FastImage.resizeMode.center;
|
||||||
|
default:
|
||||||
|
return FastImage.resizeMode.cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isThumbnailValid = (thumbnail) => {
|
||||||
|
if (!thumbnail || ((typeof thumbnail) !== 'string')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail.substring(0, 7) != 'http://' && thumbnail.substring(0, 8) != 'https://') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let style = this.props.style;
|
||||||
|
const { blurRadius, isResolvingUri, thumbnail, title, resizeMode } = this.props;
|
||||||
|
const atStyle = this.state.autoThumbStyle;
|
||||||
|
if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) {
|
||||||
|
if (style == null) {
|
||||||
|
style = fileItemMediaStyle.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blurRadius > 0) {
|
||||||
|
// No blur radius support in FastImage yet
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={{uri: thumbnail}}
|
||||||
|
blurRadius={blurRadius}
|
||||||
|
resizeMode={resizeMode ? resizeMode : "cover"}
|
||||||
|
style={style}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FastImage
|
||||||
|
source={{uri: thumbnail}}
|
||||||
|
onError={() => this.setState({ imageLoadFailed: true })}
|
||||||
|
resizeMode={this.getFastImageResizeMode(resizeMode)}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
|
||||||
|
{isResolvingUri && (
|
||||||
|
<View style={fileItemMediaStyle.resolving}>
|
||||||
|
<ActivityIndicator color={Colors.White} size={"large"} />
|
||||||
|
<Text style={fileItemMediaStyle.text}>Resolving...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!isResolvingUri && <Text style={fileItemMediaStyle.autothumbText}>{title &&
|
||||||
|
title
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.substring(0, Math.min(title.replace(' ', '').length, 5))
|
||||||
|
.toUpperCase()}</Text>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileItemMedia;
|
11
app/src/component/fileList/index.js
Normal file
11
app/src/component/fileList/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FileList from './view';
|
||||||
|
import { selectClaimsById } from 'lbry-redux';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
claimsById: selectClaimsById(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileList);
|
196
app/src/component/fileList/view.js
Normal file
196
app/src/component/fileList/view.js
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { buildURI } from 'lbry-redux';
|
||||||
|
import { FlatList } from 'react-native';
|
||||||
|
import FileItem from 'component/fileItem';
|
||||||
|
import fileListStyle from 'styles/fileList';
|
||||||
|
|
||||||
|
// In the future, all Flow types need to be specified in a common source (lbry-redux, perhaps?)
|
||||||
|
type FileInfo = {
|
||||||
|
name: string,
|
||||||
|
channelName: ?string,
|
||||||
|
pending?: boolean,
|
||||||
|
channel_claim_id: string,
|
||||||
|
value?: {
|
||||||
|
publisherSignature: {
|
||||||
|
certificateId: string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
publisherSignature: {
|
||||||
|
certificateId: string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hideFilter: boolean,
|
||||||
|
sortByHeight?: boolean,
|
||||||
|
claimsById: Array<{}>,
|
||||||
|
fileInfos: Array<FileInfo>,
|
||||||
|
checkPending?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
sortBy: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileList extends React.PureComponent<Props, State> {
|
||||||
|
static defaultProps = {
|
||||||
|
hideFilter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
sortBy: 'dateNew',
|
||||||
|
};
|
||||||
|
|
||||||
|
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
|
||||||
|
|
||||||
|
this.sortFunctions = {
|
||||||
|
dateNew: fileInfos =>
|
||||||
|
this.props.sortByHeight
|
||||||
|
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||||
|
if (fileInfo1.pending) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||||
|
? this.props.claimsById[fileInfo1.claim_id].height
|
||||||
|
: 0;
|
||||||
|
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||||
|
? this.props.claimsById[fileInfo2.claim_id].height
|
||||||
|
: 0;
|
||||||
|
if (height1 > height2) {
|
||||||
|
return -1;
|
||||||
|
} else if (height1 < height2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
: [...fileInfos].reverse(),
|
||||||
|
dateOld: fileInfos =>
|
||||||
|
this.props.sortByHeight
|
||||||
|
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||||
|
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||||
|
? this.props.claimsById[fileInfo1.claim_id].height
|
||||||
|
: 999999;
|
||||||
|
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||||
|
? this.props.claimsById[fileInfo2.claim_id].height
|
||||||
|
: 999999;
|
||||||
|
if (height1 < height2) {
|
||||||
|
return -1;
|
||||||
|
} else if (height1 > height2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
: fileInfos,
|
||||||
|
title: fileInfos =>
|
||||||
|
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||||
|
const getFileTitle = fileInfo => {
|
||||||
|
const { value, metadata, name, claim_name: claimName } = fileInfo;
|
||||||
|
if (metadata) {
|
||||||
|
// downloaded claim
|
||||||
|
return metadata.title || claimName;
|
||||||
|
} else if (value) {
|
||||||
|
// published claim
|
||||||
|
const { title } = value.stream.metadata;
|
||||||
|
return title || name;
|
||||||
|
}
|
||||||
|
// Invalid claim
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const title1 = getFileTitle(fileInfo1).toLowerCase();
|
||||||
|
const title2 = getFileTitle(fileInfo2).toLowerCase();
|
||||||
|
if (title1 < title2) {
|
||||||
|
return -1;
|
||||||
|
} else if (title1 > title2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}),
|
||||||
|
filename: fileInfos =>
|
||||||
|
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
|
||||||
|
const fileName1Lower = fileName1.toLowerCase();
|
||||||
|
const fileName2Lower = fileName2.toLowerCase();
|
||||||
|
if (fileName1Lower < fileName2Lower) {
|
||||||
|
return -1;
|
||||||
|
} else if (fileName2Lower > fileName1Lower) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannelSignature = (fileInfo: FileInfo) => {
|
||||||
|
if (fileInfo.pending) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.value) {
|
||||||
|
return fileInfo.value.publisherSignature.certificateId;
|
||||||
|
}
|
||||||
|
return fileInfo.channel_claim_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSortChanged(event: SyntheticInputEvent<*>) {
|
||||||
|
this.setState({
|
||||||
|
sortBy: event.target.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sortFunctions: {};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
contentContainerStyle,
|
||||||
|
fileInfos,
|
||||||
|
hideFilter,
|
||||||
|
checkPending,
|
||||||
|
navigation,
|
||||||
|
onEndReached,
|
||||||
|
style
|
||||||
|
} = this.props;
|
||||||
|
const { sortBy } = this.state;
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
if (!fileInfos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||||
|
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
|
||||||
|
const uriParams = {};
|
||||||
|
|
||||||
|
// This is unfortunate
|
||||||
|
// https://github.com/lbryio/lbry/issues/1159
|
||||||
|
const name = claimName || claimNameDownloaded;
|
||||||
|
uriParams.contentName = name;
|
||||||
|
uriParams.claimId = claimId;
|
||||||
|
const uri = buildURI(uriParams);
|
||||||
|
|
||||||
|
items.push(uri);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
style={style}
|
||||||
|
contentContainerStyle={contentContainerStyle}
|
||||||
|
data={items}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
keyExtractor={(item, index) => item}
|
||||||
|
renderItem={({item}) => (
|
||||||
|
<FileItem style={fileListStyle.fileItem}
|
||||||
|
uri={item}
|
||||||
|
navigation={navigation}
|
||||||
|
showDetails={true}
|
||||||
|
compactView={false} />
|
||||||
|
)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileList;
|
29
app/src/component/fileListItem/index.js
Normal file
29
app/src/component/fileListItem/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
|
import FileListItem from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileListItem);
|
134
app/src/component/fileListItem/view.js
Normal file
134
app/src/component/fileListItem/view.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
import { navigateToUri, formatBytes } from 'utils/helper';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import DateTime from 'component/dateTime';
|
||||||
|
import FileItemMedia from 'component/fileItemMedia';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import NsfwOverlay from 'component/nsfwOverlay';
|
||||||
|
import ProgressBar from 'component/progressBar';
|
||||||
|
import fileListStyle from 'styles/fileList';
|
||||||
|
|
||||||
|
class FileListItem extends React.PureComponent {
|
||||||
|
getStorageForFileInfo = (fileInfo) => {
|
||||||
|
if (!fileInfo.completed) {
|
||||||
|
const written = formatBytes(fileInfo.written_bytes);
|
||||||
|
const total = formatBytes(fileInfo.total_bytes);
|
||||||
|
return `(${written} / ${total})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatBytes(fileInfo.written_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTitle = (title) => {
|
||||||
|
if (!title) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (title.length > 80) ? title.substring(0, 77).trim() + '...' : title;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDownloadProgress = (fileInfo) => {
|
||||||
|
return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { claim, resolveUri, uri } = this.props;
|
||||||
|
if (!claim) {
|
||||||
|
resolveUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
fileInfo,
|
||||||
|
metadata,
|
||||||
|
featuredResult,
|
||||||
|
isResolvingUri,
|
||||||
|
isDownloaded,
|
||||||
|
style,
|
||||||
|
onPress,
|
||||||
|
navigation,
|
||||||
|
thumbnail,
|
||||||
|
title
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const uri = normalizeURI(this.props.uri);
|
||||||
|
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||||
|
const isResolving = !fileInfo && isResolvingUri;
|
||||||
|
|
||||||
|
let name, channel, height, channelClaimId, fullChannelUri;
|
||||||
|
if (claim) {
|
||||||
|
name = claim.name;
|
||||||
|
channel = claim.channel_name;
|
||||||
|
height = claim.height;
|
||||||
|
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
|
||||||
|
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featuredResult && !isResolvingUri && !claim && !title && !name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<TouchableOpacity style={style} onPress={onPress}>
|
||||||
|
<FileItemMedia style={fileListStyle.thumbnail}
|
||||||
|
blurRadius={obscureNsfw ? 15 : 0}
|
||||||
|
resizeMode="cover"
|
||||||
|
title={(title || name)}
|
||||||
|
thumbnail={thumbnail} />
|
||||||
|
{(fileInfo && fileInfo.completed && fileInfo.download_path) &&
|
||||||
|
<Icon style={fileListStyle.downloadedIcon} solid={true} color={Colors.NextLbryGreen} name={"folder"} size={16} />}
|
||||||
|
<View style={fileListStyle.detailsContainer}>
|
||||||
|
{featuredResult && <Text style={fileListStyle.featuredUri} numberOfLines={1}>{uri}</Text>}
|
||||||
|
|
||||||
|
{!title && !name && !channel && isResolving && (
|
||||||
|
<View>
|
||||||
|
{(!title && !name) && <Text style={fileListStyle.uri}>{uri}</Text>}
|
||||||
|
{(!title && !name) && <View style={fileListStyle.row}>
|
||||||
|
<ActivityIndicator size={"small"} color={featuredResult ? Colors.White : Colors.LbryGreen} />
|
||||||
|
</View>}
|
||||||
|
</View>)}
|
||||||
|
|
||||||
|
{(title || name) && <Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}>{this.formatTitle(title) || this.formatTitle(name)}</Text>}
|
||||||
|
{channel &&
|
||||||
|
<Link style={fileListStyle.publisher} text={channel} onPress={() => {
|
||||||
|
navigateToUri(navigation, normalizeURI(fullChannelUri));
|
||||||
|
}} />}
|
||||||
|
|
||||||
|
<View style={fileListStyle.info}>
|
||||||
|
{(fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0) &&
|
||||||
|
<Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>}
|
||||||
|
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{(fileInfo && fileInfo.download_path) &&
|
||||||
|
<View style={fileListStyle.downloadInfo}>
|
||||||
|
{!fileInfo.completed &&
|
||||||
|
<ProgressBar
|
||||||
|
borderRadius={3}
|
||||||
|
color={Colors.NextLbryGreen}
|
||||||
|
height={3}
|
||||||
|
style={fileListStyle.progress}
|
||||||
|
progress={this.getDownloadProgress(fileInfo)} />}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileListItem;
|
16
app/src/component/filePrice/index.js
Normal file
16
app/src/component/filePrice/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import { doFetchCostInfoForUri, makeSelectCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
|
||||||
|
import FilePrice from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FilePrice);
|
120
app/src/component/filePrice/view.js
Normal file
120
app/src/component/filePrice/view.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import { formatCredits, formatFullPrice } from 'lbry-redux';
|
||||||
|
|
||||||
|
class CreditAmount extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
amount: PropTypes.number.isRequired,
|
||||||
|
precision: PropTypes.number,
|
||||||
|
isEstimate: PropTypes.bool,
|
||||||
|
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||||
|
showFree: PropTypes.bool,
|
||||||
|
showFullPrice: PropTypes.bool,
|
||||||
|
showPlus: PropTypes.bool,
|
||||||
|
look: PropTypes.oneOf(['indicator', 'plain', 'fee']),
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
precision: 2,
|
||||||
|
label: true,
|
||||||
|
showFree: false,
|
||||||
|
look: 'indicator',
|
||||||
|
showFullPrice: false,
|
||||||
|
showPlus: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const minimumRenderableAmount = Math.pow(10, -1 * this.props.precision);
|
||||||
|
const { amount, precision, showFullPrice, style } = this.props;
|
||||||
|
|
||||||
|
let formattedAmount;
|
||||||
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
|
|
||||||
|
if (showFullPrice) {
|
||||||
|
formattedAmount = fullPrice;
|
||||||
|
} else {
|
||||||
|
formattedAmount =
|
||||||
|
amount > 0 && amount < minimumRenderableAmount
|
||||||
|
? `<${minimumRenderableAmount}`
|
||||||
|
: formatCredits(amount, precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
let amountText;
|
||||||
|
if (this.props.showFree && parseFloat(this.props.amount) === 0) {
|
||||||
|
amountText = 'FREE';
|
||||||
|
} else {
|
||||||
|
if (this.props.label) {
|
||||||
|
const label =
|
||||||
|
typeof this.props.label === 'string'
|
||||||
|
? this.props.label
|
||||||
|
: parseFloat(amount) == 1 ? 'credit' : 'credits';
|
||||||
|
|
||||||
|
amountText = `${formattedAmount} ${label}`;
|
||||||
|
} else {
|
||||||
|
amountText = formattedAmount;
|
||||||
|
}
|
||||||
|
if (this.props.showPlus && amount > 0) {
|
||||||
|
amountText = `+${amountText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*{this.props.isEstimate ? (
|
||||||
|
<span
|
||||||
|
className="credit-amount__estimate"
|
||||||
|
title={__('This is an estimate and does not include data fees')}
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
) : null}*/
|
||||||
|
return (
|
||||||
|
<Text style={style}>{amountText}</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilePrice extends React.PureComponent {
|
||||||
|
componentWillMount() {
|
||||||
|
this.fetchCost(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.fetchCost(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCost(props) {
|
||||||
|
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
||||||
|
|
||||||
|
if (costInfo === undefined && !fetching && claim) {
|
||||||
|
fetchCostInfo(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { costInfo, look = 'indicator', showFullPrice = false, style, textStyle } = this.props;
|
||||||
|
|
||||||
|
const isEstimate = costInfo ? !costInfo.includesData : null;
|
||||||
|
|
||||||
|
if (!costInfo) {
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<Text style={textStyle}>???</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style}>
|
||||||
|
<CreditAmount
|
||||||
|
style={textStyle}
|
||||||
|
label={false}
|
||||||
|
amount={parseFloat(costInfo.cost)}
|
||||||
|
isEstimate={isEstimate}
|
||||||
|
showFree
|
||||||
|
showFullPrice={showFullPrice}>???</CreditAmount>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilePrice;
|
4
app/src/component/fileRewardsDriver/index.js
Normal file
4
app/src/component/fileRewardsDriver/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FileRewardsDriver from './view';
|
||||||
|
|
||||||
|
export default connect()(FileRewardsDriver);
|
20
app/src/component/fileRewardsDriver/view.js
Normal file
20
app/src/component/fileRewardsDriver/view.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import filePageStyle from 'styles/filePage';
|
||||||
|
|
||||||
|
class FileRewardsDriver extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
|
||||||
|
<Icon name="award" size={16} style={filePageStyle.rewardIcon} />
|
||||||
|
<Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileRewardsDriver;
|
14
app/src/component/floatingWalletBalance/index.js
Normal file
14
app/src/component/floatingWalletBalance/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { selectBalance } from 'lbry-redux';
|
||||||
|
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||||
|
import Constants from 'constants';
|
||||||
|
import FloatingWalletBalance from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
balance: selectBalance(state),
|
||||||
|
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||||
|
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, null)(FloatingWalletBalance);
|
38
app/src/component/floatingWalletBalance/view.js
Normal file
38
app/src/component/floatingWalletBalance/view.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { formatCredits } from 'lbry-redux'
|
||||||
|
import Address from 'component/address';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import floatingButtonStyle from 'styles/floatingButton';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
balance: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
class FloatingWalletBalance extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { balance, navigation, rewardsNotInterested, unclaimedRewardAmount } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[floatingButtonStyle.view, floatingButtonStyle.bottomRight]}>
|
||||||
|
{(!rewardsNotInterested && unclaimedRewardAmount > 0) &&
|
||||||
|
<TouchableOpacity style={floatingButtonStyle.pendingContainer}
|
||||||
|
onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })} >
|
||||||
|
<Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} />
|
||||||
|
<Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text>
|
||||||
|
</TouchableOpacity>}
|
||||||
|
<TouchableOpacity style={floatingButtonStyle.container}
|
||||||
|
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}>
|
||||||
|
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
|
||||||
|
{(!isNaN(balance) || balance === 0) && (
|
||||||
|
<Text style={floatingButtonStyle.text}>{(formatCredits(parseFloat(balance), 2) + ' LBC')}</Text>)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FloatingWalletBalance;
|
9
app/src/component/link/index.js
Normal file
9
app/src/component/link/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import Link from './view';
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
notify: (data) => dispatch(doToast(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(Link);
|
68
app/src/component/link/view.js
Normal file
68
app/src/component/link/view.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Linking, Text, TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
export default class Link extends React.PureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
tappedStyle: false,
|
||||||
|
}
|
||||||
|
this.addTappedStyle = this.addTappedStyle.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePress = () => {
|
||||||
|
const { error, href, navigation, notify } = this.props;
|
||||||
|
|
||||||
|
if (navigation && href.startsWith('#')) {
|
||||||
|
navigation.navigate(href.substring(1));
|
||||||
|
} else {
|
||||||
|
if (this.props.effectOnTap) this.addTappedStyle();
|
||||||
|
Linking.openURL(href)
|
||||||
|
.then(() => setTimeout(() => { this.setState({ tappedStyle: false }); }, 2000))
|
||||||
|
.catch(err => {
|
||||||
|
notify({ message: error, isError: true })
|
||||||
|
this.setState({tappedStyle: false})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTappedStyle() {
|
||||||
|
this.setState({ tappedStyle: true });
|
||||||
|
setTimeout(() => { this.setState({ tappedStyle: false }); }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
ellipsizeMode,
|
||||||
|
numberOfLines,
|
||||||
|
onPress,
|
||||||
|
style,
|
||||||
|
text
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let styles = [];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.effectOnTap && this.state.tappedStyle) {
|
||||||
|
styles.push(this.props.effectOnTap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={styles}
|
||||||
|
numberOfLines={numberOfLines}
|
||||||
|
ellipsizeMode={ellipsizeMode}
|
||||||
|
onPress={onPress ? onPress : this.handlePress}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
18
app/src/component/mediaPlayer/index.js
Normal file
18
app/src/component/mediaPlayer/index.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { SETTINGS, savePosition } from 'lbry-redux';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { doSetPlayerVisible } from 'redux/actions/drawer';
|
||||||
|
import { selectIsPlayerVisible } from 'redux/selectors/drawer';
|
||||||
|
import MediaPlayer from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||||
|
isPlayerVisible: selectIsPlayerVisible(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
|
||||||
|
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(MediaPlayer);
|
472
app/src/component/mediaPlayer/view.js
Normal file
472
app/src/component/mediaPlayer/view.js
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Lbry } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
AppState,
|
||||||
|
ActivityIndicator,
|
||||||
|
DeviceEventEmitter,
|
||||||
|
NativeModules,
|
||||||
|
PanResponder,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity
|
||||||
|
} from 'react-native';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
|
import Video from 'react-native-video';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import FileItemMedia from 'component/fileItemMedia';
|
||||||
|
import mediaPlayerStyle from 'styles/mediaPlayer';
|
||||||
|
|
||||||
|
const positionSaveInterval = 10
|
||||||
|
|
||||||
|
class MediaPlayer extends React.PureComponent {
|
||||||
|
static ControlsTimeout = 3000;
|
||||||
|
|
||||||
|
seekResponder = null;
|
||||||
|
|
||||||
|
seekerWidth = 0;
|
||||||
|
|
||||||
|
trackingOffset = 0;
|
||||||
|
|
||||||
|
tracking = null;
|
||||||
|
|
||||||
|
video = null;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
buffering: false,
|
||||||
|
backgroundPlayEnabled: false,
|
||||||
|
autoPaused: false,
|
||||||
|
rate: 1,
|
||||||
|
volume: 1,
|
||||||
|
muted: false,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
duration: 0.0,
|
||||||
|
currentTime: 0.0,
|
||||||
|
paused: !props.autoPlay,
|
||||||
|
fullscreenMode: false,
|
||||||
|
areControlsVisible: true,
|
||||||
|
controlsTimeout: -1,
|
||||||
|
seekerOffset: 0,
|
||||||
|
seekerPosition: 0,
|
||||||
|
firstPlay: true,
|
||||||
|
seekTimeout: -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTime(time) {
|
||||||
|
let str = '';
|
||||||
|
let minutes = 0, hours = 0, seconds = parseInt(time, 10);
|
||||||
|
if (seconds > 60) {
|
||||||
|
minutes = parseInt(seconds / 60, 10);
|
||||||
|
seconds = seconds % 60;
|
||||||
|
|
||||||
|
if (minutes > 60) {
|
||||||
|
hours = parseInt(minutes / 60, 10);
|
||||||
|
minutes = minutes % 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
|
||||||
|
} else {
|
||||||
|
str = '00:' + this.pad(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pad(value) {
|
||||||
|
if (value < 10) {
|
||||||
|
return '0' + String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad = (data) => {
|
||||||
|
this.setState({
|
||||||
|
duration: data.duration
|
||||||
|
});
|
||||||
|
|
||||||
|
const { position } = this.props;
|
||||||
|
if (!isNaN(parseFloat(position)) && position > 0) {
|
||||||
|
this.video.seek(position);
|
||||||
|
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onMediaLoaded) {
|
||||||
|
this.props.onMediaLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress = (data) => {
|
||||||
|
const { savePosition, claim } = this.props;
|
||||||
|
|
||||||
|
this.setState({ buffering: false, currentTime: data.currentTime });
|
||||||
|
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
|
||||||
|
const { claim_id: claimId, txid, nout } = claim;
|
||||||
|
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.seeking) {
|
||||||
|
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.firstPlay) {
|
||||||
|
if (this.props.onPlaybackStarted) {
|
||||||
|
this.props.onPlaybackStarted();
|
||||||
|
}
|
||||||
|
this.setState({ firstPlay: false });
|
||||||
|
|
||||||
|
this.hidePlayerControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearControlsTimeout = () => {
|
||||||
|
if (this.state.controlsTimeout > -1) {
|
||||||
|
clearTimeout(this.state.controlsTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPlayerControls = () => {
|
||||||
|
this.clearControlsTimeout();
|
||||||
|
if (!this.state.areControlsVisible) {
|
||||||
|
this.setState({ areControlsVisible: true });
|
||||||
|
}
|
||||||
|
this.hidePlayerControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
manualHidePlayerControls = () => {
|
||||||
|
this.clearControlsTimeout();
|
||||||
|
this.setState({ areControlsVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePlayerControls() {
|
||||||
|
const player = this;
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
player.setState({ areControlsVisible: false });
|
||||||
|
}, MediaPlayer.ControlsTimeout);
|
||||||
|
player.setState({ controlsTimeout: timeout });
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlayerControls = () => {
|
||||||
|
const { setPlayerVisible, isPlayerVisible } = this.props;
|
||||||
|
if (!isPlayerVisible) {
|
||||||
|
setPlayerVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.areControlsVisible) {
|
||||||
|
this.manualHidePlayerControls();
|
||||||
|
} else {
|
||||||
|
this.showPlayerControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlay = () => {
|
||||||
|
this.showPlayerControls();
|
||||||
|
this.setState({ paused: !this.state.paused }, this.handlePausedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePausedState = () => {
|
||||||
|
if (!this.state.paused) {
|
||||||
|
// onProgress will automatically clear this, so it's fine
|
||||||
|
this.setState({ buffering: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreenMode = () => {
|
||||||
|
this.showPlayerControls();
|
||||||
|
const { onFullscreenToggled } = this.props;
|
||||||
|
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
|
||||||
|
if (onFullscreenToggled) {
|
||||||
|
onFullscreenToggled(this.state.fullscreenMode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd = () => {
|
||||||
|
this.setState({ paused: true });
|
||||||
|
if (this.props.onPlaybackFinished) {
|
||||||
|
this.props.onPlaybackFinished();
|
||||||
|
}
|
||||||
|
this.video.seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSeekerPosition(position = 0) {
|
||||||
|
position = this.checkSeekerPosition(position);
|
||||||
|
this.setState({ seekerPosition: position });
|
||||||
|
if (!this.state.seeking) {
|
||||||
|
this.setState({ seekerOffset: position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSeekerPosition(val = 0) {
|
||||||
|
if (val < 0) {
|
||||||
|
val = 0;
|
||||||
|
} else if (val >= this.seekerWidth) {
|
||||||
|
return this.seekerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
seekTo(time = 0) {
|
||||||
|
if (time > this.state.duration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.video.seek(time);
|
||||||
|
this.setState({ currentTime: time });
|
||||||
|
}
|
||||||
|
|
||||||
|
initSeeker() {
|
||||||
|
this.seekResponder = PanResponder.create({
|
||||||
|
onStartShouldSetPanResponder: (evt, gestureState) => true,
|
||||||
|
onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
||||||
|
|
||||||
|
onPanResponderGrant: (evt, gestureState) => {
|
||||||
|
this.clearControlsTimeout();
|
||||||
|
if (this.state.seekTimeout > 0) {
|
||||||
|
clearTimeout(this.state.seekTimeout);
|
||||||
|
}
|
||||||
|
this.setState({ seeking: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
onPanResponderMove: (evt, gestureState) => {
|
||||||
|
const position = this.state.seekerOffset + gestureState.dx;
|
||||||
|
this.setSeekerPosition(position);
|
||||||
|
},
|
||||||
|
|
||||||
|
onPanResponderRelease: (evt, gestureState) => {
|
||||||
|
const time = this.getCurrentTimeForSeekerPosition();
|
||||||
|
if (time >= this.state.duration) {
|
||||||
|
this.setState({ paused: true }, this.handlePausedState);
|
||||||
|
this.onEnd();
|
||||||
|
} else {
|
||||||
|
this.seekTo(time);
|
||||||
|
this.setState({ seekTimeout: setTimeout(() => { this.setState({ seeking: false }); }, 100) });
|
||||||
|
}
|
||||||
|
this.hidePlayerControls();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackingOffset() {
|
||||||
|
return this.state.fullscreenMode ? this.trackingOffset : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTimeForSeekerPosition() {
|
||||||
|
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSeekerPosition() {
|
||||||
|
return this.seekerWidth * this.getCurrentTimePercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTimePercentage() {
|
||||||
|
if (this.state.currentTime > 0) {
|
||||||
|
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.initSeeker();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const { isPlayerVisible } = nextProps;
|
||||||
|
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
|
||||||
|
// force pause if the player is not visible and background play is not enabled
|
||||||
|
this.setState({ paused: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { assignPlayer, backgroundPlayEnabled } = this.props;
|
||||||
|
if (assignPlayer) {
|
||||||
|
assignPlayer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
|
||||||
|
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||||
|
AppState.addEventListener('change', this.handleAppStateChange);
|
||||||
|
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
|
||||||
|
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||||
|
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
|
||||||
|
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
|
||||||
|
this.clearControlsTimeout();
|
||||||
|
this.setState({ paused: true, fullscreenMode: false });
|
||||||
|
const { onFullscreenToggled } = this.props;
|
||||||
|
if (onFullscreenToggled) {
|
||||||
|
onFullscreenToggled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAppStateChange = () => {
|
||||||
|
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||||
|
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
|
||||||
|
this.setState({ paused: true, autoPaused: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||||
|
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
|
||||||
|
this.setState({ paused: false, autoPaused: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuffer = () => {
|
||||||
|
if (!this.state.paused) {
|
||||||
|
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
play = () => {
|
||||||
|
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
pause = () => {
|
||||||
|
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBackgroundMediaNotification = () => {
|
||||||
|
this.handlePausedState();
|
||||||
|
const { backgroundPlayEnabled } = this.props;
|
||||||
|
if (backgroundPlayEnabled) {
|
||||||
|
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||||
|
const { title, channel, uri } = window.currentMediaInfo;
|
||||||
|
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, this.state.paused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPlayerControls() {
|
||||||
|
const { onBackButtonPressed } = this.props;
|
||||||
|
|
||||||
|
if (this.state.areControlsVisible) {
|
||||||
|
return (
|
||||||
|
<View style={mediaPlayerStyle.playerControlsContainer}>
|
||||||
|
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
|
||||||
|
<Icon name={"arrow-left"} size={18} style={mediaPlayerStyle.backButtonIcon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
|
||||||
|
onPress={this.togglePlay}>
|
||||||
|
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
|
||||||
|
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
|
||||||
|
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
|
||||||
|
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
|
||||||
|
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSeekerTouchAreaPressed = (evt) => {
|
||||||
|
if (evt && evt.nativeEvent) {
|
||||||
|
const newSeekerPosition = evt.nativeEvent.locationX;
|
||||||
|
if (!isNaN(newSeekerPosition)) {
|
||||||
|
const time = this.state.duration * (newSeekerPosition / this.seekerWidth);
|
||||||
|
this.setSeekerPosition(newSeekerPosition);
|
||||||
|
this.seekTo(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrackingLayout = (evt) => {
|
||||||
|
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||||
|
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||||
|
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onLayout, source, style, thumbnail } = this.props;
|
||||||
|
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
|
||||||
|
const remainingWidth = this.seekerWidth - completedWidth;
|
||||||
|
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackingStyle = [mediaPlayerStyle.trackingControls, this.state.fullscreenMode ?
|
||||||
|
mediaPlayerStyle.fullscreenTrackingControls : mediaPlayerStyle.containedTrackingControls];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles} onLayout={onLayout}>
|
||||||
|
<Video source={{ uri: source }}
|
||||||
|
bufferConfig={{ minBufferMs: 15000, maxBufferMs: 60000, bufferForPlaybackMs: 5000, bufferForPlaybackAfterRebufferMs: 5000 }}
|
||||||
|
ref={(ref: Video) => { this.video = ref; }}
|
||||||
|
resizeMode={this.state.resizeMode}
|
||||||
|
playInBackground={this.state.backgroundPlayEnabled}
|
||||||
|
style={mediaPlayerStyle.player}
|
||||||
|
rate={this.state.rate}
|
||||||
|
volume={this.state.volume}
|
||||||
|
paused={this.state.paused}
|
||||||
|
onLoad={this.onLoad}
|
||||||
|
onBuffer={this.onBuffer}
|
||||||
|
onProgress={this.onProgress}
|
||||||
|
onEnd={this.onEnd}
|
||||||
|
onError={this.onError}
|
||||||
|
minLoadRetryCount={999}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 &&
|
||||||
|
<FastImage
|
||||||
|
source={{uri: thumbnail}}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
style={mediaPlayerStyle.playerThumbnail}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.togglePlayerControls}>
|
||||||
|
{this.renderPlayerControls()}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
|
||||||
|
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
|
||||||
|
<View style={mediaPlayerStyle.progress}>
|
||||||
|
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
|
||||||
|
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
|
||||||
|
</View>
|
||||||
|
</View>}
|
||||||
|
|
||||||
|
{this.state.buffering &&
|
||||||
|
<View style={mediaPlayerStyle.loadingContainer}>
|
||||||
|
<ActivityIndicator color={Colors.LbryGreen} size="large" />
|
||||||
|
</View>}
|
||||||
|
|
||||||
|
{this.state.areControlsVisible &&
|
||||||
|
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
|
||||||
|
<View style={[mediaPlayerStyle.seekerHandle,
|
||||||
|
(this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained),
|
||||||
|
{ left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
|
||||||
|
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[mediaPlayerStyle.seekerTouchArea,
|
||||||
|
(this.state.fullscreenMode ? mediaPlayerStyle.seekerTouchAreaFs : mediaPlayerStyle.seekerTouchAreaContained)]}
|
||||||
|
onPress={this.onSeekerTouchAreaPressed} />
|
||||||
|
</View>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaPlayer;
|
4
app/src/component/navigationButton/index.js
Normal file
4
app/src/component/navigationButton/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import NavigationButton from './view';
|
||||||
|
|
||||||
|
export default connect()(NavigationButton);
|
18
app/src/component/navigationButton/view.js
Normal file
18
app/src/component/navigationButton/view.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import { TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationButton extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const { iconStyle, name, onPress, size, style } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={onPress} style={style}>
|
||||||
|
<Icon name={name} size={size} style={iconStyle} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationButton;
|
6
app/src/component/nsfwOverlay/index.js
Normal file
6
app/src/component/nsfwOverlay/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import NsfwOverlay from './view';
|
||||||
|
|
||||||
|
const perform = dispatch => ({});
|
||||||
|
|
||||||
|
export default connect(null, perform)(NsfwOverlay);
|
15
app/src/component/nsfwOverlay/view.js
Normal file
15
app/src/component/nsfwOverlay/view.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
|
import discoverStyle from '../../styles/discover';
|
||||||
|
|
||||||
|
class NsfwOverlay extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={discoverStyle.overlay} activeOpacity={0.95} onPress={this.props.onPress}>
|
||||||
|
<Text style={discoverStyle.overlayText}>This content is Not Safe For Work. To view adult content, please change your Settings.</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NsfwOverlay;
|
6
app/src/component/pageHeader/index.js
Normal file
6
app/src/component/pageHeader/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PageHeader from './view';
|
||||||
|
|
||||||
|
const perform = dispatch => ({});
|
||||||
|
|
||||||
|
export default connect(null, perform)(PageHeader);
|
52
app/src/component/pageHeader/view.js
Normal file
52
app/src/component/pageHeader/view.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Based on https://github.com/react-navigation/react-navigation/blob/master/src/views/Header/Header.js
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import NavigationButton from 'component/navigationButton';
|
||||||
|
import pageHeaderStyle from 'styles/pageHeader';
|
||||||
|
|
||||||
|
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||||
|
const AnimatedText = Animated.Text;
|
||||||
|
|
||||||
|
class PageHeader extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const { title, onBackPressed } = this.props;
|
||||||
|
const containerStyles = [
|
||||||
|
pageHeaderStyle.container,
|
||||||
|
{ height: APPBAR_HEIGHT }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={containerStyles}>
|
||||||
|
<View style={pageHeaderStyle.flexOne}>
|
||||||
|
<View style={pageHeaderStyle.header}>
|
||||||
|
<View style={pageHeaderStyle.title}>
|
||||||
|
<AnimatedText
|
||||||
|
numberOfLines={1}
|
||||||
|
style={pageHeaderStyle.titleText}
|
||||||
|
accessibilityTraits="header">
|
||||||
|
{title}
|
||||||
|
</AnimatedText>
|
||||||
|
</View>
|
||||||
|
<NavigationButton
|
||||||
|
name="arrow-left"
|
||||||
|
style={pageHeaderStyle.left}
|
||||||
|
size={24}
|
||||||
|
iconStyle={pageHeaderStyle.backIcon}
|
||||||
|
onPress={onBackPressed}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeader;
|
4
app/src/component/progressBar/index.js
Normal file
4
app/src/component/progressBar/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ProgressBar from './view';
|
||||||
|
|
||||||
|
export default connect()(ProgressBar);
|
54
app/src/component/progressBar/view.js
Normal file
54
app/src/component/progressBar/view.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
const defaultHeight = 5;
|
||||||
|
const defaultBorderRadius = 5;
|
||||||
|
const minProgress = 0;
|
||||||
|
const maxProgress = 100;
|
||||||
|
|
||||||
|
class ProgressBar extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
borderRadius: PropTypes.number,
|
||||||
|
color: PropTypes.string.isRequired,
|
||||||
|
height: PropTypes.number,
|
||||||
|
progress: function(props, propName, componentName) {
|
||||||
|
const value = parseInt(props[propName], 10);
|
||||||
|
if (isNaN(value) || props[propName] < minProgress || props[propName] > maxProgress) {
|
||||||
|
return new Error('progress should be between 0 and 100');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: PropTypes.any
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { borderRadius, color, height, progress, style } = this.props;
|
||||||
|
const currentProgress = Math.ceil(progress);
|
||||||
|
|
||||||
|
let styles = [];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
styles.push({
|
||||||
|
borderRadius: borderRadius || defaultBorderRadius,
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: height || defaultHeight,
|
||||||
|
overflow: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles}>
|
||||||
|
<View style={{ backgroundColor: color, borderRadius: borderRadius || defaultBorderRadius, flex: currentProgress }} />
|
||||||
|
<View style={{ backgroundColor: color, opacity: 0.2, flex: (100 - currentProgress) }} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgressBar;
|
25
app/src/component/relatedContent/index.js
Normal file
25
app/src/component/relatedContent/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
doSearch,
|
||||||
|
makeSelectRecommendedContentForUri,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
selectIsSearching,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import RelatedContent from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
isSearching: selectIsSearching(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
search: query => dispatch(doSearch(query, 20, undefined, true)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(RelatedContent);
|
65
app/src/component/relatedContent/view.js
Normal file
65
app/src/component/relatedContent/view.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||||
|
import { navigateToUri } from 'utils/helper';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import FileListItem from 'component/fileListItem';
|
||||||
|
import fileListStyle from 'styles/fileList';
|
||||||
|
import relatedContentStyle from 'styles/relatedContent';
|
||||||
|
|
||||||
|
export default class RelatedContent extends React.PureComponent<Props> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.didSearch = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getRecommendedContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
const { claim, uri } = this.props;
|
||||||
|
|
||||||
|
if (uri !== prevProps.uri) {
|
||||||
|
this.didSearch = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim && !this.didSearch) {
|
||||||
|
this.getRecommendedContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecommendedContent() {
|
||||||
|
const { search, title } = this.props;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
search(title);
|
||||||
|
this.didSearch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didSearch: ?boolean;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { recommendedContent, isSearching, navigation } = this.props;
|
||||||
|
|
||||||
|
if (!isSearching && (!recommendedContent || recommendedContent.length === 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={relatedContentStyle.container}>
|
||||||
|
<Text style={relatedContentStyle.title}>Related Content</Text>
|
||||||
|
{recommendedContent && recommendedContent.map(recommendedUri => (
|
||||||
|
<FileListItem
|
||||||
|
style={fileListStyle.item}
|
||||||
|
key={recommendedUri}
|
||||||
|
uri={recommendedUri}
|
||||||
|
navigation={navigation}
|
||||||
|
onPress={() => navigateToUri(navigation, recommendedUri, { autoplay: true })} />
|
||||||
|
))}
|
||||||
|
{isSearching && <ActivityIndicator size="small" color={Colors.LbryGreen} style={relatedContentStyle.loading} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
app/src/component/rewardCard/index.js
Normal file
29
app/src/component/rewardCard/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
doClaimRewardType,
|
||||||
|
doClaimRewardClearError,
|
||||||
|
makeSelectClaimRewardError,
|
||||||
|
makeSelectIsRewardClaimPending,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import RewardCard from './view';
|
||||||
|
|
||||||
|
const makeSelect = () => {
|
||||||
|
const selectIsPending = makeSelectIsRewardClaimPending();
|
||||||
|
const selectError = makeSelectClaimRewardError();
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
errorMessage: selectError(state, props),
|
||||||
|
isPending: selectIsPending(state, props),
|
||||||
|
});
|
||||||
|
|
||||||
|
return select;
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
|
||||||
|
clearError: reward => dispatch(doClaimRewardClearError(reward)),
|
||||||
|
notify: data => dispatch(doToast(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeSelect, perform)(RewardCard);
|
103
app/src/component/rewardCard/view.js
Normal file
103
app/src/component/rewardCard/view.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import Colors from '../../styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import Link from '../link';
|
||||||
|
import rewardStyle from '../../styles/reward';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
canClaim: bool,
|
||||||
|
onClaimPress: object,
|
||||||
|
reward: {
|
||||||
|
id: string,
|
||||||
|
reward_title: string,
|
||||||
|
reward_amount: number,
|
||||||
|
transaction_id: string,
|
||||||
|
created_at: string,
|
||||||
|
reward_description: string,
|
||||||
|
reward_type: string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class RewardCard extends React.PureComponent<Props> {
|
||||||
|
state = {
|
||||||
|
claimStarted: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const { errorMessage, isPending } = nextProps;
|
||||||
|
const { clearError, notify, reward } = this.props;
|
||||||
|
if (this.state.claimStarted && !isPending) {
|
||||||
|
if (errorMessage && errorMessage.trim().length > 0) {
|
||||||
|
notify({ message: errorMessage });
|
||||||
|
clearError(reward);
|
||||||
|
} else {
|
||||||
|
notify({ message: 'Reward successfully claimed!' });
|
||||||
|
}
|
||||||
|
this.setState({ claimStarted: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClaimPress = () => {
|
||||||
|
const {
|
||||||
|
canClaim,
|
||||||
|
claimReward,
|
||||||
|
notify,
|
||||||
|
reward,
|
||||||
|
showVerification
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!canClaim) {
|
||||||
|
if (showVerification) {
|
||||||
|
showVerification();
|
||||||
|
}
|
||||||
|
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ claimStarted: true }, () => {
|
||||||
|
claimReward(reward);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { canClaim, isPending, onClaimPress, reward } = this.props;
|
||||||
|
const claimed = !!reward.transaction_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={[rewardStyle.rewardCard, rewardStyle.row]} onPress={() => {
|
||||||
|
if (!isPending && !claimed) {
|
||||||
|
this.onClaimPress();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<View style={rewardStyle.leftCol}>
|
||||||
|
{!isPending && <TouchableOpacity onPress={() => {
|
||||||
|
if (!claimed) {
|
||||||
|
this.onClaimPress();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{claimed && <Icon name={claimed ? "check-circle" : "circle"}
|
||||||
|
style={claimed ? rewardStyle.claimed : (canClaim ? rewardStyle.unclaimed : rewardStyle.disabled)}
|
||||||
|
size={20} />}
|
||||||
|
</TouchableOpacity>}
|
||||||
|
{isPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
|
||||||
|
</View>
|
||||||
|
<View style={rewardStyle.midCol}>
|
||||||
|
<Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
|
||||||
|
<Text style={rewardStyle.rewardDescription}>{reward.reward_description}</Text>
|
||||||
|
{claimed && <Link style={rewardStyle.link}
|
||||||
|
href={`https://explorer.lbry.com/tx/${reward.transaction_id}`}
|
||||||
|
text={reward.transaction_id.substring(0, 7)}
|
||||||
|
error={'The transaction URL could not be opened'} />}
|
||||||
|
</View>
|
||||||
|
<View style={rewardStyle.rightCol}>
|
||||||
|
<Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
|
||||||
|
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RewardCard;
|
19
app/src/component/rewardEnrolment/index.js
Normal file
19
app/src/component/rewardEnrolment/index.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
|
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
|
||||||
|
import RewardEnrolment from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||||
|
fetching: selectFetchingRewards(state),
|
||||||
|
user: selectUser(state)
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchRewards: () => dispatch(doRewardList()),
|
||||||
|
notify: data => dispatch(doToast(data)),
|
||||||
|
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(RewardEnrolment);
|
53
app/src/component/rewardEnrolment/view.js
Normal file
53
app/src/component/rewardEnrolment/view.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Constants from 'constants';
|
||||||
|
import Link from 'component/link';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import rewardStyle from 'styles/reward';
|
||||||
|
|
||||||
|
class RewardEnrolment extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchRewards();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotInterestedPressed = () => {
|
||||||
|
const { navigation, setClientSetting } = this.props;
|
||||||
|
setClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED, true);
|
||||||
|
navigation.navigate({ routeName: 'DiscoverStack' });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnrollPressed = () => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false }});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={rewardStyle.enrollContainer} onPress>
|
||||||
|
<View style={rewardStyle.summaryRow}>
|
||||||
|
<Icon name="award" size={36} color={Colors.White} />
|
||||||
|
<Text style={rewardStyle.summaryText}>
|
||||||
|
{unclaimedRewardAmount} unclaimed credits
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={rewardStyle.onboarding}>
|
||||||
|
<Text style={rewardStyle.enrollDescText}>LBRY credits allow you to purchase content, publish content, and influence the network. You can start earning credits by watching videos on LBRY.</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={rewardStyle.buttonRow}>
|
||||||
|
<Link style={rewardStyle.notInterestedLink} text={"Not interested"} onPress={this.onNotInterestedPressed} />
|
||||||
|
<Button style={rewardStyle.enrollButton} theme={"light"} text={"Enroll"} onPress={this.onEnrollPressed} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RewardEnrolment;
|
17
app/src/component/rewardSummary/index.js
Normal file
17
app/src/component/rewardSummary/index.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
|
||||||
|
import RewardSummary from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||||
|
fetching: selectFetchingRewards(state),
|
||||||
|
user: selectUser(state)
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchRewards: () => dispatch(doRewardList()),
|
||||||
|
notify: data => dispatch(doToast(data))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(RewardSummary);
|
82
app/src/component/rewardSummary/view.js
Normal file
82
app/src/component/rewardSummary/view.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import rewardStyle from 'styles/reward';
|
||||||
|
|
||||||
|
class RewardSummary extends React.Component {
|
||||||
|
static itemKey = 'rewardSummaryDismissed';
|
||||||
|
|
||||||
|
state = {
|
||||||
|
actionsLeft: 0,
|
||||||
|
dismissed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchRewards();
|
||||||
|
|
||||||
|
AsyncStorage.getItem(RewardSummary.itemKey).then(isDismissed => {
|
||||||
|
if ('true' === isDismissed) {
|
||||||
|
this.setState({ dismissed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = this.props;
|
||||||
|
let actionsLeft = 0;
|
||||||
|
if (!user || !user.has_verified_email) {
|
||||||
|
actionsLeft++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !user.is_identity_verified) {
|
||||||
|
actionsLeft++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ actionsLeft });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDismissPressed = () => {
|
||||||
|
AsyncStorage.setItem(RewardSummary.itemKey, 'true');
|
||||||
|
this.setState({ dismissed: true });
|
||||||
|
this.props.notify({
|
||||||
|
message: 'You can always claim your rewards from the Rewards page.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSummaryPressed = () => {
|
||||||
|
const { showVerification } = this.props;
|
||||||
|
if (showVerification) {
|
||||||
|
showVerification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.dismissed ||
|
||||||
|
(user && user.is_reward_approved) ||
|
||||||
|
this.state.actionsLeft === 0 ||
|
||||||
|
unclaimedRewardAmount === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={rewardStyle.summaryContainer} onPress={this.handleSummaryPressed}>
|
||||||
|
<View style={rewardStyle.summaryRow}>
|
||||||
|
<Icon name="award" size={36} color={Colors.White} />
|
||||||
|
<Text style={rewardStyle.summaryText}>
|
||||||
|
{unclaimedRewardAmount} unclaimed credits
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Button style={rewardStyle.dismissButton} theme={"light"} text={"Dismiss"} onPress={this.onDismissPressed} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RewardSummary;
|
16
app/src/component/searchInput/index.js
Normal file
16
app/src/component/searchInput/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { NativeModules } from 'react-native';
|
||||||
|
import { doSearch, doUpdateSearchQuery } from 'lbry-redux';
|
||||||
|
import SearchInput from './view';
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
search: search => {
|
||||||
|
if (NativeModules.Firebase) {
|
||||||
|
NativeModules.Firebase.track('search', { query: search });
|
||||||
|
}
|
||||||
|
return dispatch(doSearch(search));
|
||||||
|
},
|
||||||
|
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(SearchInput);
|
40
app/src/component/searchInput/view.js
Normal file
40
app/src/component/searchInput/view.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { TextInput } from 'react-native';
|
||||||
|
|
||||||
|
class SearchInput extends React.PureComponent {
|
||||||
|
static INPUT_TIMEOUT = 500;
|
||||||
|
|
||||||
|
state = {
|
||||||
|
changeTextTimeout: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChangeText = text => {
|
||||||
|
clearTimeout(this.state.changeTextTimeout);
|
||||||
|
if (!text || text.trim().length < 2) {
|
||||||
|
// only perform a search if 2 or more characters have been input
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { search, updateSearchQuery } = this.props;
|
||||||
|
updateSearchQuery(text);
|
||||||
|
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
search(text);
|
||||||
|
}, SearchInput.INPUT_TIMEOUT);
|
||||||
|
this.setState({ changeTextTimeout: timeout });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { style, value } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
style={style}
|
||||||
|
placeholder="Search"
|
||||||
|
underlineColorAndroid="transparent"
|
||||||
|
value={value}
|
||||||
|
onChangeText={text => this.handleChangeText(text)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchInput;
|
10
app/src/component/searchRightHeaderIcon/index.js
Normal file
10
app/src/component/searchRightHeaderIcon/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SearchRightHeaderIcon from './view';
|
||||||
|
import { ACTIONS } from 'lbry-redux';
|
||||||
|
const perform = dispatch => ({
|
||||||
|
clearQuery: () => dispatch({
|
||||||
|
type: ACTIONS.HISTORY_NAVIGATE
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(SearchRightHeaderIcon);
|
20
app/src/component/searchRightHeaderIcon/view.js
Normal file
20
app/src/component/searchRightHeaderIcon/view.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { NavigationActions } from 'react-navigation';
|
||||||
|
import Feather from "react-native-vector-icons/Feather";
|
||||||
|
|
||||||
|
class SearchRightHeaderIcon extends React.PureComponent {
|
||||||
|
|
||||||
|
clearAndGoBack() {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
this.props.clearQuery();
|
||||||
|
navigation.dispatch(NavigationActions.back())
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { style } = this.props;
|
||||||
|
return <Feather name="x" size={24} style={style} onPress={() => this.clearAndGoBack()} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchRightHeaderIcon;
|
4
app/src/component/storageStatsCard/index.js
Normal file
4
app/src/component/storageStatsCard/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StorageStatsCard from './view';
|
||||||
|
|
||||||
|
export default connect()(StorageStatsCard);
|
132
app/src/component/storageStatsCard/view.js
Normal file
132
app/src/component/storageStatsCard/view.js
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
import { formatBytes } from '../../utils/helper';
|
||||||
|
import Colors from '../../styles/colors';
|
||||||
|
import storageStatsStyle from '../../styles/storageStats';
|
||||||
|
|
||||||
|
class StorageStatsCard extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
totalBytes: 0,
|
||||||
|
totalAudioBytes: 0,
|
||||||
|
totalAudioPercent: 0,
|
||||||
|
totalImageBytes: 0,
|
||||||
|
totalImagePercent: 0,
|
||||||
|
totalVideoBytes: 0,
|
||||||
|
totalVideoPercent: 0,
|
||||||
|
totalOtherBytes: 0,
|
||||||
|
totalOtherPercent: 0,
|
||||||
|
showStats: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// calculate total bytes
|
||||||
|
const { fileInfos } = this.props;
|
||||||
|
|
||||||
|
let totalBytes = 0, totalAudioBytes = 0, totalImageBytes = 0, totalVideoBytes = 0;
|
||||||
|
let totalAudioPercent = 0, totalImagePercent = 0, totalVideoPercent = 0;
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
if (fileInfo.completed) {
|
||||||
|
const bytes = fileInfo.written_bytes;
|
||||||
|
const type = fileInfo.mime_type;
|
||||||
|
totalBytes += bytes;
|
||||||
|
if (type) {
|
||||||
|
if (type.startsWith('audio/')) totalAudioBytes += bytes;
|
||||||
|
if (type.startsWith('image/')) totalImageBytes += bytes;
|
||||||
|
if (type.startsWith('video/')) totalVideoBytes += bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
totalAudioPercent = ((totalAudioBytes / totalBytes) * 100).toFixed(2);
|
||||||
|
totalImagePercent = ((totalImageBytes / totalBytes) * 100).toFixed(2);
|
||||||
|
totalVideoPercent = ((totalVideoBytes / totalBytes) * 100).toFixed(2);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
totalBytes,
|
||||||
|
totalAudioBytes,
|
||||||
|
totalAudioPercent,
|
||||||
|
totalImageBytes,
|
||||||
|
totalImagePercent,
|
||||||
|
totalVideoBytes,
|
||||||
|
totalVideoPercent,
|
||||||
|
totalOtherBytes: totalBytes - (totalAudioBytes + totalImageBytes + totalVideoBytes),
|
||||||
|
totalOtherPercent: (100 - (parseFloat(totalAudioPercent) +
|
||||||
|
parseFloat(totalImagePercent) +
|
||||||
|
parseFloat(totalVideoPercent))).toFixed(2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.totalBytes == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={storageStatsStyle.card}>
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
|
||||||
|
<View style={storageStatsStyle.summary}>
|
||||||
|
<Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text>
|
||||||
|
<Text style={storageStatsStyle.annotation}>used</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}>
|
||||||
|
<Text style={storageStatsStyle.statsText}>Stats</Text>
|
||||||
|
<Switch
|
||||||
|
style={storageStatsStyle.statsToggle}
|
||||||
|
value={this.state.showStats}
|
||||||
|
onValueChange={(value) => this.setState({ showStats: value })} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{this.state.showStats &&
|
||||||
|
<View>
|
||||||
|
<View style={storageStatsStyle.distributionBar}>
|
||||||
|
<View style={[storageStatsStyle.audioDistribution, { flex: parseFloat(this.state.totalAudioPercent) }]} />
|
||||||
|
<View style={[storageStatsStyle.imageDistribution, { flex: parseFloat(this.state.totalImagePercent) }]} />
|
||||||
|
<View style={[storageStatsStyle.videoDistribution, { flex: parseFloat(this.state.totalVideoPercent) }]} />
|
||||||
|
<View style={[storageStatsStyle.otherDistribution, { flex: parseFloat(this.state.totalOtherPercent) }]} />
|
||||||
|
</View>
|
||||||
|
<View style={storageStatsStyle.legend}>
|
||||||
|
{this.state.totalAudioBytes > 0 &&
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||||
|
<View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} />
|
||||||
|
<Text style={storageStatsStyle.legendText}>Audio</Text>
|
||||||
|
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
{this.state.totalImageBytes > 0 &&
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||||
|
<View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} />
|
||||||
|
<Text style={storageStatsStyle.legendText}>Images</Text>
|
||||||
|
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
{this.state.totalVideoBytes > 0 &&
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||||
|
<View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} />
|
||||||
|
<Text style={storageStatsStyle.legendText}>Videos</Text>
|
||||||
|
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
{this.state.totalOtherBytes > 0 &&
|
||||||
|
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||||
|
<View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} />
|
||||||
|
<Text style={storageStatsStyle.legendText}>Other</Text>
|
||||||
|
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StorageStatsCard;
|
23
app/src/component/subscribeButton/index.js
Normal file
23
app/src/component/subscribeButton/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doChannelSubscribe,
|
||||||
|
doChannelUnsubscribe,
|
||||||
|
selectSubscriptions,
|
||||||
|
makeSelectIsSubscribed,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import SubscribeButton from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
subscriptions: selectSubscriptions(state),
|
||||||
|
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
{
|
||||||
|
doChannelSubscribe,
|
||||||
|
doChannelUnsubscribe,
|
||||||
|
doToast,
|
||||||
|
}
|
||||||
|
)(SubscribeButton);
|
49
app/src/component/subscribeButton/view.js
Normal file
49
app/src/component/subscribeButton/view.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||||
|
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||||
|
import Button from '../button';
|
||||||
|
import Colors from '../../styles/colors';
|
||||||
|
|
||||||
|
class SubscribeButton extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
isSubscribed,
|
||||||
|
doChannelSubscribe,
|
||||||
|
doChannelUnsubscribe,
|
||||||
|
style
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let styles = [];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconColor = isSubscribed ? null : Colors.Red;
|
||||||
|
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||||
|
const subscriptionLabel = isSubscribed ? null : __('Subscribe');
|
||||||
|
const { claimName } = parseURI(uri);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style={styles}
|
||||||
|
theme={"light"}
|
||||||
|
icon={isSubscribed ? "heart-broken" : "heart"}
|
||||||
|
iconColor={iconColor}
|
||||||
|
solid={isSubscribed ? false : true}
|
||||||
|
text={subscriptionLabel}
|
||||||
|
onPress={() => {
|
||||||
|
subscriptionHandler({
|
||||||
|
channelName: claimName,
|
||||||
|
uri: normalizeURI(uri),
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubscribeButton;
|
25
app/src/component/subscribeNotificationButton/index.js
Normal file
25
app/src/component/subscribeNotificationButton/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doChannelSubscriptionEnableNotifications,
|
||||||
|
doChannelSubscriptionDisableNotifications,
|
||||||
|
selectEnabledChannelNotifications,
|
||||||
|
selectSubscriptions,
|
||||||
|
makeSelectIsSubscribed,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import SubscribeNotificationButton from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
enabledChannelNotifications: selectEnabledChannelNotifications(state),
|
||||||
|
subscriptions: selectSubscriptions(state),
|
||||||
|
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
{
|
||||||
|
doChannelSubscriptionEnableNotifications,
|
||||||
|
doChannelSubscriptionDisableNotifications,
|
||||||
|
doToast,
|
||||||
|
}
|
||||||
|
)(SubscribeNotificationButton);
|
55
app/src/component/subscribeNotificationButton/view.js
Normal file
55
app/src/component/subscribeNotificationButton/view.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
|
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
|
||||||
|
class SubscribeNotificationButton extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
name,
|
||||||
|
doChannelSubscriptionEnableNotifications,
|
||||||
|
doChannelSubscriptionDisableNotifications,
|
||||||
|
doToast,
|
||||||
|
enabledChannelNotifications,
|
||||||
|
isSubscribed,
|
||||||
|
style
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!isSubscribed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let styles = [];
|
||||||
|
if (style) {
|
||||||
|
if (style.length) {
|
||||||
|
styles = styles.concat(style);
|
||||||
|
} else {
|
||||||
|
styles.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
|
||||||
|
const { claimName } = parseURI(uri);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style={styles}
|
||||||
|
theme={"light"}
|
||||||
|
icon={shouldNotify ? "bell-slash" : "bell"}
|
||||||
|
solid={true}
|
||||||
|
onPress={() => {
|
||||||
|
if (shouldNotify) {
|
||||||
|
doChannelSubscriptionDisableNotifications(name);
|
||||||
|
doToast({ message: 'You will not receive notifications for new content.' });
|
||||||
|
} else {
|
||||||
|
doChannelSubscriptionEnableNotifications(name);
|
||||||
|
doToast({ message: 'You will receive all notifications for new content.' });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubscribeNotificationButton;
|
25
app/src/component/suggestedSubscriptionItem/index.js
Normal file
25
app/src/component/suggestedSubscriptionItem/index.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
makeSelectFetchingChannelClaims,
|
||||||
|
makeSelectClaimsInChannelForPage,
|
||||||
|
doFetchClaimsByChannel,
|
||||||
|
doResolveUris,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
|
import SuggestedSubscriptionItem from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claims: makeSelectClaimsInChannelForPage(props.categoryLink)(state),
|
||||||
|
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
|
||||||
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
|
||||||
|
resolveUris: uris => dispatch(doResolveUris(uris, true)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(SuggestedSubscriptionItem);
|
74
app/src/component/suggestedSubscriptionItem/view.js
Normal file
74
app/src/component/suggestedSubscriptionItem/view.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { buildURI, normalizeURI } from 'lbry-redux';
|
||||||
|
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
import FileItem from 'component/fileItem';
|
||||||
|
import subscriptionsStyle from 'styles/subscriptions';
|
||||||
|
|
||||||
|
class SuggestedSubscriptionItem extends React.PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetching, categoryLink, fetchChannel, resolveUris, claims } = this.props;
|
||||||
|
if (!fetching && categoryLink && (!claims || claims.length)) {
|
||||||
|
fetchChannel(categoryLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uriForClaim = (claim) => {
|
||||||
|
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = claim;
|
||||||
|
const uriParams = {};
|
||||||
|
|
||||||
|
// This is unfortunate
|
||||||
|
// https://github.com/lbryio/lbry/issues/1159
|
||||||
|
const name = claimName || claimNameDownloaded;
|
||||||
|
uriParams.contentName = name;
|
||||||
|
uriParams.claimId = claimId;
|
||||||
|
const uri = buildURI(uriParams);
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { categoryLink, fetching, obscureNsfw, claims, navigation } = this.props;
|
||||||
|
|
||||||
|
if (!claims || !claims.length) {
|
||||||
|
return (
|
||||||
|
<View style={subscriptionsStyle.busyContainer}>
|
||||||
|
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claims && claims.length > 0) {
|
||||||
|
return (
|
||||||
|
<View style={subscriptionsStyle.suggestedContainer}>
|
||||||
|
<FileItem
|
||||||
|
style={subscriptionsStyle.compactMainFileItem}
|
||||||
|
mediaStyle={subscriptionsStyle.fileItemMedia}
|
||||||
|
uri={this.uriForClaim(claims[0])}
|
||||||
|
navigation={navigation} />
|
||||||
|
{(claims.length > 1) &&
|
||||||
|
<FlatList style={subscriptionsStyle.compactItems}
|
||||||
|
horizontal={true}
|
||||||
|
renderItem={ ({item}) => (
|
||||||
|
<FileItem
|
||||||
|
style={subscriptionsStyle.compactFileItem}
|
||||||
|
mediaStyle={subscriptionsStyle.compactFileItemMedia}
|
||||||
|
key={item}
|
||||||
|
uri={normalizeURI(item)}
|
||||||
|
navigation={navigation}
|
||||||
|
compactView={true} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data={claims.slice(1, 4).map(claim => this.uriForClaim(claim))}
|
||||||
|
keyExtractor={(item, index) => item}
|
||||||
|
/>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SuggestedSubscriptionItem;
|
13
app/src/component/suggestedSubscriptions/index.js
Normal file
13
app/src/component/suggestedSubscriptions/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
|
||||||
|
import SuggestedSubscriptions from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
suggested: selectSuggestedChannels(state),
|
||||||
|
loading: selectIsFetchingSuggested(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
null
|
||||||
|
)(SuggestedSubscriptions);
|
55
app/src/component/suggestedSubscriptions/view.js
Normal file
55
app/src/component/suggestedSubscriptions/view.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, SectionList, Text, View } from 'react-native';
|
||||||
|
import { normalizeURI } from 'lbry-redux';
|
||||||
|
import { navigateToUri } from 'utils/helper';
|
||||||
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
|
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
|
||||||
|
import Colors from 'styles/colors';
|
||||||
|
import discoverStyle from 'styles/discover';
|
||||||
|
import subscriptionsStyle from 'styles/subscriptions';
|
||||||
|
import Link from 'component/link';
|
||||||
|
|
||||||
|
class SuggestedSubscriptions extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const { suggested, loading, navigation } = this.props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggested ? (
|
||||||
|
<SectionList style={subscriptionsStyle.scrollContainer}
|
||||||
|
renderItem={ ({item, index, section}) => (
|
||||||
|
<SuggestedSubscriptionItem
|
||||||
|
key={item}
|
||||||
|
categoryLink={normalizeURI(item)}
|
||||||
|
navigation={navigation} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderSectionHeader={
|
||||||
|
({section: {title}}) => {
|
||||||
|
const titleParts = title.split(';');
|
||||||
|
const channelName = titleParts[0];
|
||||||
|
const channelUri = normalizeURI(titleParts[1]);
|
||||||
|
return (
|
||||||
|
<View style={subscriptionsStyle.titleRow}>
|
||||||
|
<Link style={subscriptionsStyle.channelTitle} text={channelName} onPress={() => {
|
||||||
|
navigateToUri(navigation, normalizeURI(channelUri));
|
||||||
|
}} />
|
||||||
|
<SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections={suggested.map(({ uri, label }) => ({ title: (label + ';' + uri), data: [uri] }))}
|
||||||
|
keyExtractor={(item, index) => item}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SuggestedSubscriptions;
|
11
app/src/component/transactionList/index.js
Normal file
11
app/src/component/transactionList/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
//import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards';
|
||||||
|
import { selectAllMyClaimsByOutpoint } from 'lbry-redux';
|
||||||
|
import TransactionList from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
//rewards: selectClaimedRewardsByTransactionId(state),
|
||||||
|
myClaims: selectAllMyClaimsByOutpoint(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, null)(TransactionList);
|
|
@ -0,0 +1,56 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View, Linking } from 'react-native';
|
||||||
|
import { buildURI, formatCredits } from 'lbry-redux';
|
||||||
|
import { navigateToUri } from '../../../utils/helper';
|
||||||
|
import Link from '../../link';
|
||||||
|
import moment from 'moment';
|
||||||
|
import transactionListStyle from '../../../styles/transactionList';
|
||||||
|
|
||||||
|
class TransactionListItem extends React.PureComponent {
|
||||||
|
capitalize(string: string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { transaction, navigation } = this.props;
|
||||||
|
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={transactionListStyle.listItem}>
|
||||||
|
<View style={[transactionListStyle.row, transactionListStyle.topRow]}>
|
||||||
|
<View style={transactionListStyle.col}>
|
||||||
|
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
|
||||||
|
{name && claimId && (
|
||||||
|
<Link
|
||||||
|
style={transactionListStyle.link}
|
||||||
|
onPress={() => navigateToUri(navigation, buildURI({ claimName: name, claimId }))}
|
||||||
|
text={name} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={transactionListStyle.col}>
|
||||||
|
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>{formatCredits(amount, 8)}</Text>
|
||||||
|
{ fee !== 0 && (<Text style={[transactionListStyle.amount, transactionListStyle.text]}>fee {formatCredits(fee, 8)}</Text>) }
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={transactionListStyle.row}>
|
||||||
|
<View style={transactionListStyle.col}>
|
||||||
|
<Link style={transactionListStyle.smallLink}
|
||||||
|
text={txid.substring(0, 8)}
|
||||||
|
href={`https://explorer.lbry.com/tx/${txid}`}
|
||||||
|
error={'The transaction URL could not be opened'} />
|
||||||
|
</View>
|
||||||
|
<View style={transactionListStyle.col}>
|
||||||
|
{date ? (
|
||||||
|
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={transactionListStyle.smallText}>Pending</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionListItem;
|
70
app/src/component/transactionList/view.js
Normal file
70
app/src/component/transactionList/view.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import TransactionListItem from './internal/transaction-list-item';
|
||||||
|
import transactionListStyle from '../../styles/transactionList';
|
||||||
|
|
||||||
|
export type Transaction = {
|
||||||
|
amount: number,
|
||||||
|
claim_id: string,
|
||||||
|
claim_name: string,
|
||||||
|
fee: number,
|
||||||
|
nout: number,
|
||||||
|
txid: string,
|
||||||
|
type: string,
|
||||||
|
date: Date,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TransactionList extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
filter: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
|
||||||
|
(this: any).filterTransaction = this.filterTransaction.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterChanged(event: React.SyntheticInputEvent<*>) {
|
||||||
|
this.setState({
|
||||||
|
filter: event.target.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterTransaction(transaction: Transaction) {
|
||||||
|
const { filter } = this.state;
|
||||||
|
|
||||||
|
return filter === 'all' || filter === transaction.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { emptyMessage, rewards, transactions, navigation } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
const transactionList = transactions.filter(this.filterTransaction);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{!transactionList.length && (
|
||||||
|
<Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!transactionList.length && (
|
||||||
|
<View>
|
||||||
|
{transactionList.map(t => (
|
||||||
|
<TransactionListItem
|
||||||
|
key={`${t.txid}:${t.nout}`}
|
||||||
|
transaction={t}
|
||||||
|
navigation={navigation}
|
||||||
|
reward={rewards && rewards[t.txid]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionList;
|
20
app/src/component/transactionListRecent/index.js
Normal file
20
app/src/component/transactionListRecent/index.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doFetchTransactions,
|
||||||
|
selectRecentTransactions,
|
||||||
|
selectHasTransactions,
|
||||||
|
selectIsFetchingTransactions,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import TransactionListRecent from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||||
|
transactions: selectRecentTransactions(state),
|
||||||
|
hasTransactions: selectHasTransactions(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(TransactionListRecent);
|
50
app/src/component/transactionListRecent/view.js
Normal file
50
app/src/component/transactionListRecent/view.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
//import BusyIndicator from 'component/common/busy-indicator';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import Button from '../button';
|
||||||
|
import Link from '../link';
|
||||||
|
import TransactionList from '../transactionList';
|
||||||
|
import type { Transaction } from '../transactionList/view';
|
||||||
|
import walletStyle from '../../styles/wallet';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fetchTransactions: () => void,
|
||||||
|
fetchingTransactions: boolean,
|
||||||
|
hasTransactions: boolean,
|
||||||
|
transactions: Array<Transaction>,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TransactionListRecent extends React.PureComponent<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { fetchingTransactions, hasTransactions, transactions, navigation } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={walletStyle.transactionsCard}>
|
||||||
|
<View style={[walletStyle.row, walletStyle.transactionsHeader]}>
|
||||||
|
<Text style={walletStyle.transactionsTitle}>Recent Transactions</Text>
|
||||||
|
<Link style={walletStyle.link}
|
||||||
|
navigation={navigation}
|
||||||
|
text={'View All'}
|
||||||
|
href={'#TransactionHistory'} />
|
||||||
|
</View>
|
||||||
|
{fetchingTransactions && (
|
||||||
|
<Text style={walletStyle.infoText}>Fetching transactions...</Text>
|
||||||
|
)}
|
||||||
|
{!fetchingTransactions && (
|
||||||
|
<TransactionList
|
||||||
|
navigation={navigation}
|
||||||
|
transactions={transactions}
|
||||||
|
emptyMessage={"Looks like you don't have any recent transactions."}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionListRecent;
|
26
app/src/component/uriBar/index.js
Normal file
26
app/src/component/uriBar/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doUpdateSearchQuery,
|
||||||
|
selectSearchState as selectSearch,
|
||||||
|
selectSearchValue,
|
||||||
|
selectSearchSuggestions
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { selectCurrentRoute } from 'redux/selectors/drawer';
|
||||||
|
import UriBar from './view';
|
||||||
|
|
||||||
|
const select = state => {
|
||||||
|
const { ...searchState } = selectSearch(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...searchState,
|
||||||
|
query: selectSearchValue(state),
|
||||||
|
currentRoute: selectCurrentRoute(state),
|
||||||
|
suggestions: selectSearchSuggestions(state)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(UriBar);
|
45
app/src/component/uriBar/internal/uri-bar-item.js
Normal file
45
app/src/component/uriBar/internal/uri-bar-item.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { SEARCH_TYPES, normalizeURI } from 'lbry-redux';
|
||||||
|
import { Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import uriBarStyle from '../../../styles/uriBar';
|
||||||
|
|
||||||
|
class UriBarItem extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
const { item, onPress } = this.props;
|
||||||
|
const { shorthand, type, value } = item;
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
switch (type) {
|
||||||
|
case SEARCH_TYPES.CHANNEL:
|
||||||
|
icon = <Icon name="at" size={18} />
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEARCH_TYPES.SEARCH:
|
||||||
|
icon = <Icon name="search" size={18} />
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEARCH_TYPES.FILE:
|
||||||
|
default:
|
||||||
|
icon = <Icon name="file" size={18} />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
|
||||||
|
{icon}
|
||||||
|
<View style={uriBarStyle.itemContent}>
|
||||||
|
<Text style={uriBarStyle.itemText} numberOfLines={1}>{shorthand || value} - {type === SEARCH_TYPES.SEARCH ? 'Search' : value}</Text>
|
||||||
|
<Text style={uriBarStyle.itemDesc} numberOfLines={1}>
|
||||||
|
{type === SEARCH_TYPES.SEARCH && `Search for '${value}'`}
|
||||||
|
{type === SEARCH_TYPES.CHANNEL && `View the @${shorthand} channel`}
|
||||||
|
{type === SEARCH_TYPES.FILE && `View content at ${value}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UriBarItem;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue