Compare commits
614 commits
Author | SHA1 | Date | |
---|---|---|---|
|
fdd759b241 | ||
|
57cd649c02 | ||
|
7250435d7f | ||
|
b81abf74a1 | ||
|
208e2c2d42 | ||
|
a5bdd1c042 | ||
|
1e3a74cae1 | ||
|
ca08f71a72 | ||
|
b60ca39df1 | ||
|
696bc86b7c | ||
|
4c163c6244 | ||
|
2ad49ca281 | ||
|
56caeef72b | ||
|
84bb014557 | ||
|
9278e74e85 | ||
|
cf6f09c60d | ||
|
f2cbed48d9 | ||
|
f1ead0c247 | ||
|
5f2c72ec4d | ||
|
a6869eb2e6 | ||
|
cbae6c476a | ||
|
48d257ceaf | ||
|
5355456498 | ||
|
1436895ace | ||
|
6e32f7724f | ||
|
3f5104d60a | ||
|
d8fdb3b818 | ||
|
5c187e7a8d | ||
|
24862550a1 | ||
|
146fced44e | ||
|
d535ed8c98 | ||
|
b675dbad9b | ||
|
dabe9fe691 | ||
|
9ac216504d | ||
|
911ca998e7 | ||
|
36c105d3a7 | ||
|
a655d0112b | ||
|
493c771e94 | ||
|
e9d70dbf87 | ||
|
0849ce2b66 | ||
|
d0a8b3b218 | ||
|
a8cdc4a771 | ||
|
67b883660f | ||
|
f328efb831 | ||
|
b2f56364d6 | ||
|
2761857fe8 | ||
|
d439260d69 | ||
|
f0f0a5028b | ||
|
13e170cb61 | ||
|
127d8052ca | ||
|
1b3086572f | ||
|
6fa308ef96 | ||
|
f3e513fc2d | ||
|
d72a8faec4 | ||
|
fdb4578349 | ||
|
c08237d399 | ||
|
bdf8a58818 | ||
|
e1d51c881a | ||
|
28212808f8 | ||
|
74f08f8f98 | ||
|
cb5c29fbce | ||
|
4d775d3a17 | ||
|
fd94ef9fa7 | ||
|
2e0331305a | ||
|
fb560f8f01 | ||
|
ef80c9f7fd | ||
|
99a4a0a22f | ||
|
136853853a | ||
|
ebf3299c30 | ||
|
036b49a871 | ||
|
67d5f88d34 | ||
|
b1e0b9af33 | ||
|
c30b012787 | ||
|
c8c6305757 | ||
|
ee1d090e62 | ||
|
9e56a86492 | ||
|
e6b83877f1 | ||
|
b567e39aef | ||
|
ae62dba0a6 | ||
|
df1e8abf50 | ||
|
31cfb26c3b | ||
|
e5f34dc464 | ||
|
601031e55d | ||
|
a9aadbe6a8 | ||
|
f9a4b71037 | ||
|
0f10e9dc1f | ||
|
0647deb06c | ||
|
b2f5fec293 | ||
|
daf4e5aca2 | ||
|
c1324efb41 | ||
|
6221de2d3c | ||
|
983bc68af2 | ||
|
f1b167693d | ||
|
68ac64b534 | ||
|
6819ae46f9 | ||
|
6c406c5a85 | ||
|
c179243d22 | ||
|
d0f5504c80 | ||
|
896c566a02 | ||
|
da9352cc68 | ||
|
b8d2375e20 | ||
|
dd52ff9d07 | ||
|
d9891f8a8a | ||
|
ea5fe6842d | ||
|
fc649187df | ||
|
b0f7c41885 | ||
|
45935717c8 | ||
|
ac5e666369 | ||
|
ffeb72a383 | ||
|
0ec09d751c | ||
|
b8e4fcff92 | ||
|
b79f0f4820 | ||
|
a1cd58a214 | ||
|
cfca8facbe | ||
|
066a0a099c | ||
|
afeee2e5df | ||
|
76036acfc7 | ||
|
897bfdaffd | ||
|
7475ae323c | ||
|
faf7f3ccbf | ||
|
4940d1ca33 | ||
|
3d48fa5741 | ||
|
f9887cffae | ||
|
6644907665 | ||
|
5f850685d6 | ||
|
5f9674e49c | ||
|
0d185c6db3 | ||
|
53d22dd22d | ||
|
ff8ffda3c6 | ||
|
64bd540322 | ||
|
919f9a48f3 | ||
|
746d442051 | ||
|
1877b75188 | ||
|
6e8c38cace | ||
|
eab7bab267 | ||
|
aab648c3cf | ||
|
f79ff509bd | ||
|
1197e990ca | ||
|
dce1c0715e | ||
|
6a0263c5bc | ||
|
ca64db0499 | ||
|
a5d4eda4d1 | ||
|
5d210961c1 | ||
|
1b88f565af | ||
|
991a98b571 | ||
|
0073277d6e | ||
|
eeca602f7a | ||
|
6c171560fd | ||
|
66c4c00215 | ||
|
9b9ef9ab74 | ||
|
39a289e7f1 | ||
|
722c829502 | ||
|
7ecb80d136 | ||
|
ea19af04d4 | ||
|
8196b69211 | ||
|
dc861caf6c | ||
|
adb5ffa8d0 | ||
|
d918cb28bd | ||
|
7ce7314ab9 | ||
|
13e5caa0ef | ||
|
ff59a7a89f | ||
|
c3efa4d004 | ||
|
ed50e1300a | ||
|
86dbfd54d1 | ||
|
535120eebd | ||
|
c1106d7186 | ||
|
45be7f2c9b | ||
|
d6baf3d1c8 | ||
|
08c38c1723 | ||
|
af4fa454d3 | ||
|
0620582a4e | ||
|
b11c07e3d1 | ||
|
593b34079c | ||
|
becb533624 | ||
|
8efc0522f2 | ||
|
cead924ca5 | ||
|
f83a043664 | ||
|
e8a0bca5ea | ||
|
1198c2298a | ||
|
7205acb9d6 | ||
|
d21cfd55ab | ||
|
47cbc3624c | ||
|
1f9a0886a0 | ||
|
dd5ea68915 | ||
|
b3bba4d273 | ||
|
10c123ba6a | ||
|
a8ba45b941 | ||
|
3652cdf6bc | ||
|
f2a7a8c439 | ||
|
78d5a99441 | ||
|
6931dbe79c | ||
|
4d024c06cc | ||
|
ddcb190457 | ||
|
dd14b90d7e | ||
|
4b86444478 | ||
|
feb7f260dc | ||
|
74c7a1de4d | ||
|
88a43dc679 | ||
|
1bb5ce72fa | ||
|
b4ce544965 | ||
|
1ec6f6173a | ||
|
8ee19b3f5c | ||
|
1055392b7f | ||
|
c772cf2ead | ||
|
481c50f465 | ||
|
71bc969f2a | ||
|
e4edebfed7 | ||
|
e8a5ab8307 | ||
|
3738f3af21 | ||
|
74d10e4199 | ||
|
080e00becf | ||
|
049d905d7d | ||
|
33094f8c88 | ||
|
6c44e503db | ||
|
fa78c80592 | ||
|
07630ca97b | ||
|
a2b6d4e570 | ||
|
9d6b3ddf81 | ||
|
0a2769859a | ||
|
c4a1ed801a | ||
|
453394bf1b | ||
|
4b7dfba5a1 | ||
|
e699fcf0b3 | ||
|
7e80f707e0 | ||
|
467f037170 | ||
|
89d5b40e5f | ||
|
34fba010e1 | ||
|
d7396bb044 | ||
|
fda0817ad1 | ||
|
895ca75506 | ||
|
a04448ebe8 | ||
|
26ccbf2709 | ||
|
7d6c11a88c | ||
|
8742913c33 | ||
|
8e6e0f0099 | ||
|
a4585de807 | ||
|
6c24749ad5 | ||
|
c73509a4ca | ||
|
673b85b2aa | ||
|
fe6e60cd67 | ||
|
8bcee90d68 | ||
|
084407e129 | ||
|
4a3db5ae20 | ||
|
b36813ebe6 | ||
|
cc1ddfa16e | ||
|
5f1775d478 | ||
|
288e35dd64 | ||
|
d4af28d1c5 | ||
|
37cf85a5f6 | ||
|
c2cbd76f49 | ||
|
3d874435c3 | ||
|
9ed8864f23 | ||
|
61263dd59d | ||
|
2707e135c6 | ||
|
f9de0d7937 | ||
|
f03d58d648 | ||
|
f49a3570e9 | ||
|
1d97f9008d | ||
|
e85ca9114c | ||
|
85ed2da518 | ||
|
dcc91f75cc | ||
|
a40c160ac6 | ||
|
e6861c8436 | ||
|
e6a5d97fb8 | ||
|
ddeb209d51 | ||
|
3283b3a607 | ||
|
83a41ca6ce | ||
|
6c63eb7d66 | ||
|
7cf9de8c2a | ||
|
a859682954 | ||
|
c844c4f896 | ||
|
a49cfe91da | ||
|
2bacba2c87 | ||
|
e473981063 | ||
|
34ea4f216c | ||
|
52e34b3027 | ||
|
c6a6ea0445 | ||
|
59ae6340e1 | ||
|
cbf2aa2311 | ||
|
94b0c7bc01 | ||
|
4c50ffac19 | ||
|
551e736651 | ||
|
0380e35966 | ||
|
5de0906fc1 | ||
|
c2f17b6230 | ||
|
6a3bbe6c0d | ||
|
82307c9f98 | ||
|
d2ec0e2aa1 | ||
|
3951b1080d | ||
|
ef7caeeead | ||
|
7ef8e2029e | ||
|
aaec6ac6a3 | ||
|
731960da7c | ||
|
ac282d2667 | ||
|
7a79601cfc | ||
|
d1167e4d2b | ||
|
613634adcf | ||
|
acbe33c66d | ||
|
053ebbd70b | ||
|
f5dc4fa4e7 | ||
|
343270b757 | ||
|
65f5626aba | ||
|
a051a73c2b | ||
|
6840997793 | ||
|
2c98ed2d8d | ||
|
6a083c4152 | ||
|
b60b5d16c3 | ||
|
52cfe8dc12 | ||
|
737afca031 | ||
|
456f41d28d | ||
|
238f59ad71 | ||
|
cf6d567193 | ||
|
20f8f3852d | ||
|
4bb9f5cb43 | ||
|
540a841255 | ||
|
3544637405 | ||
|
651448dfa1 | ||
|
4bac266b8b | ||
|
f86b61741d | ||
|
6c60d299af | ||
|
08582238c3 | ||
|
b931d0ce7d | ||
|
5e1fc9dbd8 | ||
|
de4e6eee25 | ||
|
d5b4e6990f | ||
|
26703815b2 | ||
|
40c36df414 | ||
|
6eb20d0e08 | ||
|
cc3055f1c9 | ||
|
25a1eac43b | ||
|
ff30e7f6a4 | ||
|
3fcc01597d | ||
|
cbc4a662e6 | ||
|
5aa0513324 | ||
|
4d1f142f9c | ||
|
e9c8f9432f | ||
|
56c375f344 | ||
|
5b165a2339 | ||
|
bfd3c711ab | ||
|
4627e284fa | ||
|
ea8ac783a8 | ||
|
966dd2bf2c | ||
|
364de592fa | ||
|
d38241f0d1 | ||
|
e217be6b67 | ||
|
37f84f0399 | ||
|
b618b9fa2b | ||
|
37b893103d | ||
|
e2d4852df7 | ||
|
1e0298d73a | ||
|
64c13295d6 | ||
|
0261ece709 | ||
|
a5799347ad | ||
|
6c7c4d1a88 | ||
|
f5390316a7 | ||
|
da1ea77aa9 | ||
|
8ea48977ae | ||
|
6721db54e2 | ||
|
6a7079c8b2 | ||
|
8cc7e9d6c9 | ||
|
d0ac41c242 | ||
|
bc8f476ef1 | ||
|
2d01b63cfe | ||
|
88dc31e9af | ||
|
8d1e5eb64d | ||
|
8113ef3f8a | ||
|
a74ed0e93b | ||
|
11f42bf71e | ||
|
8eb96025ec | ||
|
1ece7ff433 | ||
|
e312c451f8 | ||
|
bb64e91648 | ||
|
69398f6a2c | ||
|
bef7aa0ae9 | ||
|
2776696501 | ||
|
fa075df278 | ||
|
05311fd3a1 | ||
|
79001239d9 | ||
|
e344733bf0 | ||
|
282fdbb903 | ||
|
ee7c571101 | ||
|
87e42d6b7e | ||
|
8fcf135280 | ||
|
ae9364ad7e | ||
|
6aa44d8a9e | ||
|
3f013d1272 | ||
|
94d1f5a4eb | ||
|
1d8a7d838b | ||
|
c5d441fa1f | ||
|
4276a78a81 | ||
|
36c24a448f | ||
|
4bf1051549 | ||
|
fc4ab3128c | ||
|
65d32ea13e | ||
|
4cd8c80226 | ||
|
7adcdc43cf | ||
|
77852dc135 | ||
|
746ca62ca7 | ||
|
5e993f0fcc | ||
|
168dbba62c | ||
|
4db7bf814c | ||
|
21a3ff318b | ||
|
116de67d4c | ||
|
c6b8dfd539 | ||
|
37f33e9943 | ||
|
4bae7fdf94 | ||
|
c455bde154 | ||
|
5aa4321787 | ||
|
27a2100c72 | ||
|
4fc82c6527 | ||
|
56486ad274 | ||
|
99a38e655b | ||
|
262050147d | ||
|
5ff2e608c2 | ||
|
4e305faf5b | ||
|
0bdbb843fb | ||
|
c8736ad26e | ||
|
57d6b72f5a | ||
|
d0117b14db | ||
|
6729842ed8 | ||
|
723ade8ba6 | ||
|
83066232b6 | ||
|
4dc0b19c5d | ||
|
a58cc820ef | ||
|
2b3397bc85 | ||
|
a519b5c0b1 | ||
|
fb3a34c871 | ||
|
a1bc3fcd7e | ||
|
7daf3c6f55 | ||
|
7d032179ef | ||
|
156507f8f7 | ||
|
032a1927db | ||
|
f24bf570ef | ||
|
75c2ece016 | ||
|
0c1c1a6c79 | ||
|
5af25a9d43 | ||
|
55fec0f578 | ||
|
b61fc3a148 | ||
|
e3cd0fc2b1 | ||
|
fe01f50c65 | ||
|
cd378d16e3 | ||
|
e951fabf1c | ||
|
48543a48fb | ||
|
2699893581 | ||
|
21914a0bd7 | ||
|
d83667682d | ||
|
a5dc01f589 | ||
|
fb497cebfe | ||
|
9ffaba4358 | ||
|
be85307fe8 | ||
|
b55961bd83 | ||
|
f4911394ba | ||
|
42cf11358e | ||
|
6e6ed07890 | ||
|
22327bf33e | ||
|
57d8bc3311 | ||
|
6589799a2a | ||
|
7d6370163b | ||
|
9a5d63b7fc | ||
|
2d872a57ee | ||
|
f9c0850acd | ||
|
994ec732a4 | ||
|
b44770c77f | ||
|
7ea836e249 | ||
|
16d2401e38 | ||
|
259dc4c7cc | ||
|
3bcd67b8fb | ||
|
648d9103b7 | ||
|
faac434d33 | ||
|
f558cc49f7 | ||
|
5add227e55 | ||
|
4519a5d174 | ||
|
095bed4482 | ||
|
07dc95b639 | ||
|
edc3ebcff1 | ||
|
0f7055ca5b | ||
|
f426321e15 | ||
|
70d5cb3e2b | ||
|
15e4dca984 | ||
|
3a7221d71b | ||
|
e8cb3afb34 | ||
|
2b7a580001 | ||
|
b5d327581b | ||
|
c0ca45e949 | ||
|
4acc107d3a | ||
|
1070dba0ae | ||
|
7ae5d48767 | ||
|
e5c05fac48 | ||
|
f7c4b94546 | ||
|
a58af97216 | ||
|
3759a9703a | ||
|
2d3ed191fd | ||
|
425817b896 | ||
|
aaf3be2f7b | ||
|
bc810c80d6 | ||
|
e7a6f6f921 | ||
|
26542abc4d | ||
|
46fd7f40d4 | ||
|
40eaa80f4b | ||
|
ea8f93061d | ||
|
2ec2593704 | ||
|
c4949c7794 | ||
|
7572a0da5a | ||
|
678b9c3184 | ||
|
2900b18cce | ||
|
580e78afab | ||
|
ace430abe6 | ||
|
54c6089bdc | ||
|
8b308abfb8 | ||
|
53696970a7 | ||
|
18e2d8ad5e | ||
|
46cf9ad4ad | ||
|
a11f560183 | ||
|
23fbde6df4 | ||
|
512e4988d2 | ||
|
eb41a5e529 | ||
|
cefc126ffe | ||
|
29286efbd4 | ||
|
ea30ad5974 | ||
|
5f30cca79b | ||
|
d76cf4457e | ||
|
88d58f4344 | ||
|
93819b3afd | ||
|
7b438db34f | ||
|
f526d797f8 | ||
|
05f7b07449 | ||
|
ddf7c66933 | ||
|
715a52b291 | ||
|
660adb4a6f | ||
|
e47a1e0bf2 | ||
|
7b2044d2a4 | ||
|
1d3c961c3c | ||
|
dc59a1270f | ||
|
7452efa047 | ||
|
8517f74c6d | ||
|
637a62ea21 | ||
|
16f270fb9f | ||
|
c4f016c912 | ||
|
e63bc44055 | ||
|
99c8e14773 | ||
|
d84fb51f6f | ||
|
a8309ffaf5 | ||
|
fd97ba3ae9 | ||
|
305e62746c | ||
|
bf57de9d96 | ||
|
07e52d2a83 | ||
|
36a0450c1d | ||
|
f49e8134dd | ||
|
a54037270b | ||
|
4a6b091496 | ||
|
03addf41c7 | ||
|
9440855bf2 | ||
|
d53b1b94e5 | ||
|
16ebf7bb11 | ||
|
c8e7194a94 | ||
|
c6527fc57a | ||
|
bf906479d8 | ||
|
938512f826 | ||
|
bbc068b889 | ||
|
0ae9fd44d2 | ||
|
d266b9f55b | ||
|
57691fcd99 | ||
|
8cd41aae27 | ||
|
8ac3efeedc | ||
|
642c80779a | ||
|
17d43fcb7e | ||
|
6bc67dd303 | ||
|
2926225133 | ||
|
8fe078cc29 | ||
|
b721a52584 | ||
|
e7eeb0bacd | ||
|
710abb1206 | ||
|
82c3efba07 | ||
|
12a3b50e35 | ||
|
48884bc2de | ||
|
d02689fc67 | ||
|
9751b18c54 | ||
|
f6a4ca42b1 | ||
|
0f2d6bfa9b | ||
|
84a7b930a8 | ||
|
7549ecbf45 | ||
|
99f6150bdc | ||
|
dab8aec81c | ||
|
b0ddacbe89 | ||
|
530923640c | ||
|
c977d2ed86 | ||
|
8459d10dc7 | ||
|
60836ec5ec | ||
|
20cd4affee | ||
|
120c3ca52f | ||
|
455dadd30d | ||
|
01534caf13 | ||
|
4186081fdc | ||
|
88a7a9b6b1 | ||
|
b4267d8d8b | ||
|
fdac63a299 | ||
|
0a210b6e09 | ||
|
945308dfbc | ||
|
aa285f90ae | ||
|
64bf12718a | ||
|
dd9677ea3b | ||
|
ea60df592f | ||
|
c140aa32b6 | ||
|
9d6cf369d6 | ||
|
41c124e042 | ||
|
eca962ad68 | ||
|
89c653e613 | ||
|
964ed2d129 | ||
|
6a1287d1b0 | ||
|
d3f4abc50e | ||
|
20da1b6461 | ||
|
044947d4ae | ||
|
a2c0851ac7 | ||
|
7e136faea5 |
2040 changed files with 63024 additions and 213093 deletions
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
## PR Checklist
|
||||
|
||||
<!-- For the checkbox formatting to work properly, make sure there are no spaces on either side of the "x" -->
|
||||
|
||||
Please check all that apply to this PR using "x":
|
||||
|
||||
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
|
||||
- [ ] I have checked that this PR does not introduce a breaking change
|
||||
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
|
||||
|
||||
## PR Type
|
||||
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting)
|
||||
- [ ] Refactoring (no functional changes)
|
||||
- [ ] Documentation changes
|
||||
- [ ] Other - Please describe:
|
||||
|
||||
## Fixes
|
||||
|
||||
Issue Number:
|
||||
|
||||
## What is the current behavior?
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
## Other information
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and solution strategy for existing applications below. -->
|
1
.github/workflows/deploy.yml
vendored
Normal file
1
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
|
80
.gitignore
vendored
80
.gitignore
vendored
|
@ -1,15 +1,69 @@
|
|||
.buildozer
|
||||
app/node_modules/
|
||||
bin
|
||||
buildozer.spec
|
||||
build.log
|
||||
recipes/**/*.pyc
|
||||
src/main/assets/index.android.bundle
|
||||
src/main/assets/index.android.bundle.meta
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!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
|
||||
.vagrant
|
||||
|
||||
lbry-android.keystore
|
||||
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
||||
.gitsecret/keys/random_seed
|
||||
|
||||
*.hprof
|
||||
app/build
|
||||
bin
|
||||
app/debuglib
|
||||
|
|
|
@ -3,56 +3,73 @@ stages:
|
|||
- deploy
|
||||
- release
|
||||
|
||||
|
||||
build apk:
|
||||
stage: build
|
||||
image: lbry/android-base:latest
|
||||
image: lbry/android-base:platform-28
|
||||
before_script:
|
||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||
- 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 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:
|
||||
paths:
|
||||
- bin/browser-*-release.apk
|
||||
- bin/browser-*-release__arm.apk
|
||||
- bin/browser-*-release__arm64.apk
|
||||
expire_in: 1 week
|
||||
script:
|
||||
- export PATH=/usr/bin:$PATH
|
||||
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
|
||||
- cd app
|
||||
- npm install
|
||||
- cd ..
|
||||
- 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
|
||||
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
|
||||
- chmod u+x ./release.sh
|
||||
- ./release.sh
|
||||
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
|
||||
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
|
||||
|
||||
deploy build.lbry.io:
|
||||
image: python:latest
|
||||
image: python:stretch
|
||||
stage: deploy
|
||||
dependencies:
|
||||
- build apk
|
||||
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
|
||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||
- export BUILD_APK_FILENAME=browser-$BUILD_VERSION-release.apk
|
||||
- chmod u+x $CI_PROJECT_DIR/gradlew
|
||||
- git secret reveal
|
||||
- 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:
|
||||
- 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 s3://build.lbry.io/android/push.apk
|
||||
- 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__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__64 s3://build.lbry.io/android/push.apk
|
||||
|
||||
release apk:
|
||||
image: python:latest
|
||||
image: python:stretch
|
||||
stage: release
|
||||
only:
|
||||
- tags
|
||||
dependencies:
|
||||
- build apk
|
||||
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
|
||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||
- export BUILD_APK_FILENAME=browser-$BUILD_VERSION-release.apk
|
||||
- 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)
|
||||
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
|
||||
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
|
||||
script:
|
||||
- githubrelease release lbryio/lbry-android create $CI_COMMIT_TAG --publish bin/$BUILD_APK_FILENAME
|
||||
- aws s3 cp bin/$BUILD_APK_FILENAME s3://build.lbry.io/android/latest.apk
|
||||
- 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 edit $CI_COMMIT_TAG --draft
|
||||
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/latest.apk
|
||||
|
|
1
.gitmodules
vendored
Normal file
1
.gitmodules
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
|
Binary file not shown.
Binary file not shown.
BIN
.gitsecret/keys/random_seed
Normal file
BIN
.gitsecret/keys/random_seed
Normal file
Binary file not shown.
|
@ -1,2 +1,3 @@
|
|||
lbry-android.keystore:
|
||||
p4a/pythonforandroid/bootstraps/lbry/build/templates/google-services.json
|
||||
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
|
||||
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
|
||||
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033
|
||||
|
|
95
.travis.yml
95
.travis.yml
|
@ -1,95 +0,0 @@
|
|||
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
117
BUILD.md
|
@ -1,117 +0,0 @@
|
|||
## Linux Build Instructions
|
||||
|
||||
This app has currently only been built on Ubuntu 14.04, 16.04, 17.10, and 18.04, but these instructions, or an analog of them, should work on most Linux or macOS environments.
|
||||
|
||||
### Install Prerequisites
|
||||
|
||||
#### Requirements
|
||||
* JDK 1.8
|
||||
* Android SDK
|
||||
* Crystax Android NDK
|
||||
* Buildozer
|
||||
* Node.js
|
||||
* npm
|
||||
* yarn
|
||||
|
||||
#### apt Packages
|
||||
Based on the quick-start instructions at http://buildozer.readthedocs.io/en/latest/installation.html
|
||||
```
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-get update
|
||||
sudo apt-get install autoconf autogen build-essential curl libtool libffi-dev python python-pip python-openssl python3.7 python3.7-dev python3-pip ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
|
||||
```
|
||||
Alternatively, the JDK available from http://www.oracle.com/technetwork/java/javase/downloads/index.html can be installed instead of the `openjdk-8-jdk` package.
|
||||
|
||||
#### Install Cython and Setuptools
|
||||
```
|
||||
sudo pip install --upgrade cython==0.28.1 setuptools
|
||||
```
|
||||
|
||||
#### Install buildozer
|
||||
A forked version of `buildozer` needs to be installed in order to copy the React Native UI source files into the corresponding directories.
|
||||
```
|
||||
git clone https://github.com/lbryio/buildozer.git
|
||||
cd buildozer
|
||||
sudo python2.7 setup.py install
|
||||
```
|
||||
|
||||
#### Create buildozer.spec
|
||||
Assuming `lbry-android` as the current working folder:
|
||||
* Copy `buildozer.spec.sample` to `buildozer.spec` in the `lbry-android` folder. Running `buildozer init` instead will create a new `buildozer.spec` file.
|
||||
* Update `buildozer.spec` settings to match your environment. The basic recommended settings are outlined below.
|
||||
|
||||
|
||||
| Setting | Description |
|
||||
|:------------------- |:-----------------------------|
|
||||
| title | application title |
|
||||
| package.name | package name (e.g. browser) |
|
||||
| package.domain | package domain (e.g. io.lbry) |
|
||||
| source.dir | the location of the application main.py |
|
||||
| version | application version |
|
||||
| requirements | the Python module requirements for building the application |
|
||||
| services | list of Android background services and their corresponding Python entry points |
|
||||
| android.permissions | Android manifest permissions required by the application. This should be set to `INTERNET` at the very least to enable internet connectivity |
|
||||
| android.api | Android API version (Should be at least 23 for Gradle build support) |
|
||||
| android.sdk | Android SDK version (Should be at least 23 for Gradle build support) |
|
||||
| android.ndk | Android NDK version (not required when using crystax Android NDK) |
|
||||
| android.ndk_path | Android NDK path. This should be set to the crystax Android NDK path) |
|
||||
| android.sdk_path | Android SDK path. This should be set to the path where the Android SDK is manually set up (if not set up in the `.buildozer` path). |
|
||||
| p4a.source_dir | Path to the python-for-android repository folder. Currently set to the included `p4a` folder |
|
||||
| p4a.local_recipes | Path to a folder containing python_for_android recipes to be used in the build. The included `recipes` folder includes recipes for a successful build |
|
||||
|
||||
#### Setup Android SDK for buildozer
|
||||
Download the Android SDK, platform and build tools archives.
|
||||
* Android API 23 SDK - https://dl.google.com/android/android-sdk_r23-linux.tgz
|
||||
* Android API 27 platform - https://dl.google.com/android/repository/platform-27_r01.zip
|
||||
* Android build tools 26.0.1 - https://dl.google.com/android/repository/build-tools_r26.0.1-linux.
|
||||
|
||||
Create the `.buildozer` path (and the `android` sub-path) in your home folder if it doesn't already exist.
|
||||
`mkdir ~/.buildozer`
|
||||
`mkdir ~/.buildozer/android`
|
||||
|
||||
Extract the API 23 SDK to the `~/.buildozer/android` path and rename the extracted folder.
|
||||
```
|
||||
tar -xf android-sdk_r23-linux.tgz ~/.buildozer/android/platform/
|
||||
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
|
||||
```
|
||||
|
||||
Extract the API 27 platform archive into the `android-sdk-23` folder and rename the extracted folder.
|
||||
```
|
||||
unzip platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
|
||||
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
|
||||
```
|
||||
|
||||
Extract the build tools 26.0.1 build tools into the `android-sdk-23` folder and rename the extracted folder.
|
||||
```
|
||||
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||
unzip ~/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||
mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
|
||||
```
|
||||
|
||||
Finally, create the Android SDK license file. This prevents being prompted to accept the SDK license during the build process.
|
||||
```
|
||||
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
|
||||
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||
```
|
||||
|
||||
#### Setup Crystax Android NDK for buildozer
|
||||
* Download the Crystax Android NDK from https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz and extract. Remember to update `android.ndk_path` in your `buildozer.spec` to the path of the extracted Crystax NDK archive.
|
||||
* Copy `build-target-python.sh` from the `scripts` folder in the cloned `lbry-android` repository to the `crystax-ndk-10.3.2/build/tools/` folder.
|
||||
* Delete the `android-9` folder in `crystax-ndk-10.3.2/platforms`, and create a symbolic link named `android-9` to the `android-21` folder.
|
||||
|
||||
#### Build and Deploy
|
||||
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface.
|
||||
|
||||
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
|
||||
|
||||
To build and deploy, you can run `./deploy.sh`. This requires a connected device or running Android emulator.
|
||||
|
||||
#### Development
|
||||
If you already installed `Android SDK` and `adb`
|
||||
|
||||
* Run `adb reverse tcp:8081 tcp:8081`
|
||||
* Then go to the `lbry-android/app` folder and run `npm start`
|
||||
|
||||
Note: You need to have your device connected with USB debugging.
|
||||
|
||||
Once the bundler is ready, run the LBRY Browser app on your device and then shake the device violently until you see the React Native dev menu. You can enable "Live Reloading" and "Hot Reloading" from this menu, so any changes you make to the React Native code will be visible as you save. This will only reload React Native Javascript files. Native Java code needs to be redeployed by running the command `./deploy.sh`
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2018 LBRY Inc
|
||||
Copyright (c) 2017-2020 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:
|
||||
|
||||
|
|
110
QUICKSTART.md
110
QUICKSTART.md
|
@ -1,110 +0,0 @@
|
|||
### Introduction
|
||||
If you would like to contribute to the Android app, but find the build documentation a little daunting, this guide lets you copy-paste your way to a successful APK build.
|
||||
|
||||
#### Estimated build time
|
||||
25 - 40 minutes (depending on Internet connection speeds)
|
||||
|
||||
#### What do you need?
|
||||
* A computer running Ubuntu 18.04
|
||||
* Internet access to download modules and packages.
|
||||
* At least 15GB of free disk space.
|
||||
* Alternatively, Docker. You can skip steps 1 through 5 if you make use of the `lbry/android-base` Docker base image. Scroll down to Fast track if you would prefer to use Docker.
|
||||
|
||||
### Step 1 of 10
|
||||
Install all the apt packages required by running the following commands. You can copy-paste directly to your terminal.
|
||||
```
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y curl ca-certificates software-properties-common gpg-agent wget
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa -y && \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get -y update && apt-get -y install autoconf autogen automake libtool libffi-dev \
|
||||
build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 \
|
||||
libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev \
|
||||
python-pip openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 yarn gawk nodejs npm
|
||||
```
|
||||
|
||||
### Step 2 of 10
|
||||
Install a couple of packages using the Python package installer
|
||||
```
|
||||
sudo -H pip install --upgrade cython==0.28.1 setuptools
|
||||
```
|
||||
|
||||
### Step 3 of 10
|
||||
Install buildozer, a tool for creating the apk package using the python for android toolcahin.
|
||||
```
|
||||
git clone https://github.com/lbryio/buildozer.git
|
||||
cd buildozer && python2.7 setup.py install && cd ..
|
||||
```
|
||||
|
||||
### Step 4 of 10
|
||||
The Android SDK needs to be setup for buildozer. This requires creating a few directories and downloading a number of files. Run the following commands to create the buildozer directory, download the required archives and extract them into their proper destination folders.
|
||||
|
||||
```
|
||||
mkdir -p ~/.buildozer/android/platform
|
||||
wget 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/ && \
|
||||
wget 'https://dl.google.com/android/repository/platform-28_r06.zip' -P ~/.buildozer/android/platform/ && \
|
||||
wget 'https://dl.google.com/android/repository/build-tools_r26.0.2-linux.zip' -P ~/.buildozer/android/platform/
|
||||
tar -xvf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/ && \
|
||||
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23 && \
|
||||
unzip ~/.buildozer/android/platform/platform-28_r06.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms && \
|
||||
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-9 ~/.buildozer/android/platform/android-sdk-23/platforms/android-28 && \
|
||||
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools && \
|
||||
unzip ~/.buildozer/android/platform/build-tools_r26.0.2-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools && \
|
||||
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses && \
|
||||
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
|
||||
```
|
||||
|
||||
### Step 5 of 10
|
||||
Install the react-native-cli npm package.
|
||||
```
|
||||
sudo npm install -g react-native-cli
|
||||
```
|
||||
|
||||
### Step 6 of 10
|
||||
Install the Crystax NDK which is required for building Python 3.7 for the mobile app, and a number of native C / C++ modules and packages used by the app. Run the following commands to download and extract the NDK.
|
||||
```
|
||||
wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/ && \
|
||||
tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/ && \
|
||||
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 && \
|
||||
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||
```
|
||||
|
||||
### Step 7 of 10
|
||||
Clone the lbryio/lbry-android git repository and create your buildozer.spec file. The provide buildozer.spec.sample contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to.
|
||||
```
|
||||
git clone https://github.com/lbryio/lbry-android
|
||||
cd lbry-android
|
||||
cp buildozer.spec.sample buildozer.spec
|
||||
```
|
||||
|
||||
### Step 8 of 10
|
||||
Install the npm packages required for the app's React Native code.
|
||||
```
|
||||
cd app
|
||||
npm install
|
||||
cd ..
|
||||
```
|
||||
|
||||
### Step 9 of 10
|
||||
Copy a couple of required files from the repository for the build to be successful.
|
||||
```
|
||||
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
|
||||
```
|
||||
|
||||
### Step 10 of 10
|
||||
If you made it this far, you're finally ready to build the package! You just have to run a single command to generate the APK.
|
||||
```
|
||||
buildozer android debug
|
||||
```
|
||||
|
||||
### Fast Track
|
||||
Install Docker and start a container using the `lbry/android-base` image, which is about 1.72GB in size. Run the following commands for Ubuntu and then follow steps 6 through 10 in the container's bash prompt.
|
||||
```
|
||||
sudo apt-get install docker-ce
|
||||
docker run -it lbry/android-base:latest /bin/bash
|
||||
```
|
||||
|
||||
**Protip:** You can also make use of Docker to run your builds on macOS or Windows.
|
25
README.md
25
README.md
|
@ -1,9 +1,12 @@
|
|||
# LBRY Android
|
||||
[![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. 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.
|
||||
An Android browser and wallet for the [LBRY](https://lbry.com) network.
|
||||
|
||||
|
||||
<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
|
||||
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
||||
|
@ -12,10 +15,22 @@ 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.
|
||||
|
||||
## Usage
|
||||
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.
|
||||
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.
|
||||
|
||||
## Running from Source
|
||||
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.
|
||||
Clone the repository and open the project in Android Studio. Android Studio will automatically run the initial build process.
|
||||
|
||||
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
|
||||
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
||||
|
@ -24,7 +39,7 @@ Contributions to this project are welcome, encouraged, and compensated. For more
|
|||
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
||||
|
||||
## Security
|
||||
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.
|
||||
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.
|
||||
|
||||
## Contact
|
||||
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
||||
|
|
100
Vagrantfile
vendored
100
Vagrantfile
vendored
|
@ -1,100 +0,0 @@
|
|||
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
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"presets": ["module:metro-react-native-babel-preset"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
["module-resolver", {
|
||||
root: ["./src"],
|
||||
}],
|
||||
]
|
||||
}
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
144
app/build.gradle
Normal file
144
app/build.gradle
Normal file
|
@ -0,0 +1,144 @@
|
|||
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
|
|
@ -1,2 +0,0 @@
|
|||
#!/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/
|
BIN
app/google-services.json.secret
Normal file
BIN
app/google-services.json.secret
Normal file
Binary file not shown.
41
app/google-services.sample.json
Normal file
41
app/google-services.sample.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import LBRYApp from './src/index';
|
||||
|
||||
export default LBRYApp;
|
8189
app/package-lock.json
generated
8189
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,47 +0,0 @@
|
|||
{
|
||||
"name": "LBRYApp",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start"
|
||||
},
|
||||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"@expo/vector-icons": "^8.1.0",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbryinc": "lbryio/lbryinc",
|
||||
"lodash": ">=4.17.11",
|
||||
"merge": ">=1.2.1",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.8.6",
|
||||
"react-native": "0.59.3",
|
||||
"@react-native-community/async-storage": "^1.2.2",
|
||||
"react-native-country-picker-modal": "^0.6.2",
|
||||
"react-native-exception-handler": "2.9.0",
|
||||
"react-native-fast-image": "^5.0.3",
|
||||
"react-native-gesture-handler": "^1.1.0",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-password-strength-meter": "^0.0.2",
|
||||
"react-native-phone-input": "lbryio/react-native-phone-input",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||
"react-navigation": "^3.11.0",
|
||||
"react-navigation-redux-helpers": "^3.0.2",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^4.10.2",
|
||||
"redux-persist-filesystem-storage": "^1.3.2",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.18",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-fetch-blob": "^0.10.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react-native": "5.0.2",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"flow-babel-webpack-plugin": "^1.1.1"
|
||||
}
|
||||
}
|
21
app/proguard-rules.pro
vendored
Normal file
21
app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# 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
|
|
@ -0,0 +1,27 @@
|
|||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
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.
Before Width: | Height: | Size: 29 KiB |
|
@ -1,392 +0,0 @@
|
|||
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);
|
|
@ -1,7 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(null, {
|
||||
doToast,
|
||||
})(Address);
|
|
@ -1,28 +0,0 @@
|
|||
// @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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect(null, null)(Button);
|
|
@ -1,58 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import CategoryList from './view';
|
||||
|
||||
export default connect(null, null)(CategoryList);
|
|
@ -1,39 +0,0 @@
|
|||
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;
|
|
@ -1,26 +0,0 @@
|
|||
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);
|
|
@ -1,88 +0,0 @@
|
|||
// @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;
|
|
@ -1,16 +0,0 @@
|
|||
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);
|
|
@ -1,55 +0,0 @@
|
|||
// @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;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import DrawerContent from './view';
|
||||
|
||||
export default connect()(DrawerContent);
|
|
@ -1,38 +0,0 @@
|
|||
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;
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
|
@ -1,103 +0,0 @@
|
|||
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;
|
|
@ -1,32 +0,0 @@
|
|||
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);
|
|
@ -1,104 +0,0 @@
|
|||
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;
|
|
@ -1,7 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileItemMedia from './view';
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(FileItemMedia);
|
|
@ -1,108 +0,0 @@
|
|||
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;
|
|
@ -1,11 +0,0 @@
|
|||
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);
|
|
@ -1,196 +0,0 @@
|
|||
// @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;
|
|
@ -1,29 +0,0 @@
|
|||
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);
|
|
@ -1,134 +0,0 @@
|
|||
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;
|
|
@ -1,16 +0,0 @@
|
|||
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);
|
|
@ -1,120 +0,0 @@
|
|||
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;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileRewardsDriver from './view';
|
||||
|
||||
export default connect()(FileRewardsDriver);
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
|
@ -1,14 +0,0 @@
|
|||
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);
|
|
@ -1,38 +0,0 @@
|
|||
// @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;
|
|
@ -1,9 +0,0 @@
|
|||
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);
|
|
@ -1,68 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
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);
|
|
@ -1,472 +0,0 @@
|
|||
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;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NavigationButton from './view';
|
||||
|
||||
export default connect()(NavigationButton);
|
|
@ -1,18 +0,0 @@
|
|||
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;
|
|
@ -1,6 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NsfwOverlay from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(NsfwOverlay);
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
|
@ -1,6 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PageHeader from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(null, perform)(PageHeader);
|
|
@ -1,52 +0,0 @@
|
|||
// 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;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ProgressBar from './view';
|
||||
|
||||
export default connect()(ProgressBar);
|
|
@ -1,54 +0,0 @@
|
|||
// @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;
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
|
@ -1,65 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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);
|
|
@ -1,103 +0,0 @@
|
|||
// @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;
|
|
@ -1,19 +0,0 @@
|
|||
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);
|
|
@ -1,53 +0,0 @@
|
|||
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;
|
|
@ -1,17 +0,0 @@
|
|||
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);
|
|
@ -1,82 +0,0 @@
|
|||
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;
|
|
@ -1,16 +0,0 @@
|
|||
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);
|
|
@ -1,40 +0,0 @@
|
|||
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;
|
|
@ -1,10 +0,0 @@
|
|||
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);
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StorageStatsCard from './view';
|
||||
|
||||
export default connect()(StorageStatsCard);
|
|
@ -1,132 +0,0 @@
|
|||
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;
|
|
@ -1,23 +0,0 @@
|
|||
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);
|
|
@ -1,49 +0,0 @@
|
|||
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;
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
|
@ -1,55 +0,0 @@
|
|||
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;
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
|
@ -1,74 +0,0 @@
|
|||
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;
|
|
@ -1,13 +0,0 @@
|
|||
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);
|
|
@ -1,55 +0,0 @@
|
|||
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;
|
|
@ -1,11 +0,0 @@
|
|||
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);
|
|
@ -1,56 +0,0 @@
|
|||
// @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;
|
|
@ -1,70 +0,0 @@
|
|||
// @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;
|
|
@ -1,20 +0,0 @@
|
|||
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);
|
|
@ -1,50 +0,0 @@
|
|||
// @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;
|
|
@ -1,26 +0,0 @@
|
|||
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);
|
|
@ -1,45 +0,0 @@
|
|||
// @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;
|
|
@ -1,197 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, isNameValid, isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import { FlatList, Keyboard, TextInput, View } from 'react-native';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import Constants from 'constants';
|
||||
import UriBarItem from './internal/uri-bar-item';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import discoverStyle from 'styles/discover';
|
||||
import uriBarStyle from 'styles/uriBar';
|
||||
|
||||
class UriBar extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 2500; // 2.5 seconds
|
||||
|
||||
textInput = null;
|
||||
|
||||
keyboardDidHideListener = null;
|
||||
|
||||
componentDidMount () {
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||
this.setSelection();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidHideListener) {
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { currentRoute, query } = nextProps;
|
||||
const { currentRoute: prevRoute } = this.props;
|
||||
|
||||
if (Constants.DRAWER_ROUTE_SEARCH === currentRoute && currentRoute !== prevRoute) {
|
||||
this.setState({ currentValue: query, inputText: query });
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
changeTextTimeout: null,
|
||||
currentValue: null,
|
||||
inputText: null,
|
||||
focused: false,
|
||||
// TODO: Add a setting to enable / disable direct search?
|
||||
directSearch: true
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeText = text => {
|
||||
const newValue = text ? text : '';
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
const { updateSearchQuery, onSearchSubmitted, navigation } = this.props;
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
if (text.trim().length === 0) {
|
||||
// don't do anything if the text is empty
|
||||
return;
|
||||
}
|
||||
|
||||
updateSearchQuery(text);
|
||||
|
||||
if (!text.startsWith('lbry://')) {
|
||||
// not a URI input, so this is a search, perform a direct search
|
||||
if (onSearchSubmitted) {
|
||||
onSearchSubmitted(text);
|
||||
} else {
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: text }});
|
||||
}
|
||||
}
|
||||
|
||||
}, UriBar.INPUT_TIMEOUT);
|
||||
this.setState({ inputText: newValue, currentValue: newValue, changeTextTimeout: timeout });
|
||||
}
|
||||
|
||||
handleItemPress = (item) => {
|
||||
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
|
||||
const { type, value } = item;
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
if (SEARCH_TYPES.SEARCH === type) {
|
||||
this.setState({ currentValue: value });
|
||||
updateSearchQuery(value);
|
||||
|
||||
if (onSearchSubmitted) {
|
||||
onSearchSubmitted(value);
|
||||
return;
|
||||
}
|
||||
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value }});
|
||||
} else {
|
||||
const uri = normalizeURI(value);
|
||||
navigateToUri(navigation, uri);
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardDidHide = () => {
|
||||
if (this.textInput) {
|
||||
this.textInput.blur();
|
||||
}
|
||||
this.setState({ focused: false });
|
||||
}
|
||||
|
||||
setSelection() {
|
||||
if (this.textInput) {
|
||||
this.textInput.setNativeProps({ selection: { start: 0, end: 0 }});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmitEditing = () => {
|
||||
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
|
||||
if (this.state.inputText) {
|
||||
let inputText = this.state.inputText;
|
||||
if (inputText.startsWith('lbry://') && isURIValid(inputText)) {
|
||||
// if it's a URI (lbry://...), open the file page
|
||||
const uri = normalizeURI(inputText);
|
||||
navigateToUri(navigation, uri);
|
||||
} else {
|
||||
updateSearchQuery(inputText);
|
||||
// Not a URI, default to a search request
|
||||
if (onSearchSubmitted) {
|
||||
// Only the search page sets the onSearchSubmitted prop, so call this prop if set
|
||||
onSearchSubmitted(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the search page with the query populated
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: inputText }});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchPageBlurred() {
|
||||
this.setState({ currenValueSet: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation, suggestions, query, value } = this.props;
|
||||
if (this.state.currentValue === null) {
|
||||
this.setState({ currentValue: value });
|
||||
}
|
||||
|
||||
let style = [uriBarStyle.overlay];
|
||||
|
||||
// TODO: Add optional setting to enable URI / search bar suggestions
|
||||
/*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={uriBarStyle.uriContainer}>
|
||||
<NavigationButton
|
||||
name="bars"
|
||||
size={24}
|
||||
style={uriBarStyle.drawerMenuButton}
|
||||
iconStyle={discoverStyle.drawerHamburger}
|
||||
onPress={() => navigation.openDrawer() } />
|
||||
<TextInput ref={(ref) => { this.textInput = ref }}
|
||||
style={uriBarStyle.uriText}
|
||||
onLayout={() => { this.setSelection(); }}
|
||||
selectTextOnFocus={true}
|
||||
placeholder={'Search movies, music, and more'}
|
||||
underlineColorAndroid={'transparent'}
|
||||
numberOfLines={1}
|
||||
clearButtonMode={'while-editing'}
|
||||
value={this.state.currentValue}
|
||||
returnKeyType={'go'}
|
||||
inlineImageLeft={'baseline_search_black_24'}
|
||||
inlineImagePadding={16}
|
||||
onFocus={() => this.setState({ focused: true })}
|
||||
onBlur={() => {
|
||||
this.setState({ focused: false });
|
||||
this.setSelection();
|
||||
}}
|
||||
onChangeText={this.handleChangeText}
|
||||
onSubmitEditing={this.handleSubmitEditing}/>
|
||||
{(this.state.focused && !this.state.directSearch) && (
|
||||
<View style={uriBarStyle.suggestions}>
|
||||
<FlatList style={uriBarStyle.suggestionList}
|
||||
data={suggestions}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={(item, value) => item.value}
|
||||
renderItem={({item}) => (
|
||||
<UriBarItem
|
||||
item={item}
|
||||
navigation={navigation}
|
||||
onPress={() => this.handleItemPress(item)}
|
||||
/>)} />
|
||||
</View>)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBar;
|
|
@ -1,20 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doCheckAddressIsMine,
|
||||
doGetNewAddress,
|
||||
selectReceiveAddress,
|
||||
selectGettingNewAddress,
|
||||
} from 'lbry-redux';
|
||||
import WalletAddress from './view';
|
||||
|
||||
const select = state => ({
|
||||
receiveAddress: selectReceiveAddress(state),
|
||||
gettingNewAddress: selectGettingNewAddress(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)),
|
||||
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletAddress);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue