Compare commits

..

No commits in common. "master" and "0.7.2" have entirely different histories.

2040 changed files with 213093 additions and 63024 deletions

View file

@ -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. -->

View file

@ -1 +0,0 @@

80
.gitignore vendored
View file

@ -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

View file

@ -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
View file

@ -1 +0,0 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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
View 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
View 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`

View file

@ -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
View 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.

View file

@ -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
View 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
View 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
View file

@ -1 +0,0 @@
/build

View file

@ -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
View 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.

View file

@ -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
View file

@ -0,0 +1,3 @@
import LBRYApp from './src/index';
export default LBRYApp;

8189
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
app/package.json Normal file
View 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"
}
}

View file

@ -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

View file

@ -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());
}
}

View file

@ -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 {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 354 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View 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);

View 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);

View 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>
);
}
}

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import Button from './view';
export default connect(null, null)(Button);

View 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>
);
}
};

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import CategoryList from './view';
export default connect(null, null)(CategoryList);

View 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;

View 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);

View 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;

View 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);

View 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;

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import DrawerContent from './view';
export default connect()(DrawerContent);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import FileRewardsDriver from './view';
export default connect()(FileRewardsDriver);

View 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;

View 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);

View 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;

View 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);

View 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>
);
}
};

View 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);

View 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;

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import NavigationButton from './view';
export default connect()(NavigationButton);

View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import NsfwOverlay from './view';
const perform = dispatch => ({});
export default connect(null, perform)(NsfwOverlay);

View 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;

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import PageHeader from './view';
const perform = dispatch => ({});
export default connect(null, perform)(PageHeader);

View 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;

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import ProgressBar from './view';
export default connect()(ProgressBar);

View 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;

View 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);

View 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>
);
}
}

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import StorageStatsCard from './view';
export default connect()(StorageStatsCard);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View 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);

View file

@ -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;

View 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;

View 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);

View 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;

View 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);

View 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