Compare commits
944 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 | ||
|
309b1e70e6 | ||
|
e2399c257d | ||
|
c892b51ff8 | ||
|
8b3a0d22be | ||
|
f4fe9294f0 | ||
|
67a2b76e41 | ||
|
499e406c5d | ||
|
90ccf18fbf | ||
|
7f57447e57 | ||
|
10beb9935f | ||
|
5fc2e1247b | ||
|
67d4e97582 | ||
|
abeadd858e | ||
|
5a737ce38d | ||
|
97f1b530b2 | ||
|
f1393ae707 | ||
|
0c274617cb | ||
|
f33a8c3829 | ||
|
c243645124 | ||
|
4655678481 | ||
|
cec8ddeeee | ||
|
8d987a02aa | ||
|
a6d87d570f | ||
|
f3b07be01f | ||
|
07f23ba927 | ||
|
7a7e96388b | ||
|
73d91f7268 | ||
|
6371296b28 | ||
|
cdeadf9d6d | ||
|
c332acb2c1 | ||
|
0d68f1a24b | ||
|
1666ea88fc | ||
|
ff163a31bf | ||
|
73d469dc8d | ||
|
d0226ab4cc | ||
|
3d1b07f11c | ||
|
f98d7c26dc | ||
|
c5fca74e39 | ||
|
f79ad3dabc | ||
|
c5ebfa5021 | ||
|
534a966ee8 | ||
|
7484596fb4 | ||
|
1637752b02 | ||
|
4ce424d4db | ||
|
916af88c53 | ||
|
b21776c517 | ||
|
6bb128e38a | ||
|
8b2694efb7 | ||
|
f853132e9b | ||
|
39e8d5513a | ||
|
6feadf73dd | ||
|
d683c48338 | ||
|
4a684b0bec | ||
|
6d7768e904 | ||
|
a645c4530f | ||
|
4d61462290 | ||
|
877ce8d008 | ||
|
fbff91774e | ||
|
53fb072373 | ||
|
f30f1f47d5 | ||
|
d8b0e89c6c | ||
|
a353f13ebb | ||
|
0d3415441f | ||
|
860d509d0f | ||
|
c86fdec8e6 | ||
|
ca6fedd05c | ||
|
07209a79de | ||
|
4132416f0d | ||
|
e62d5741c2 | ||
|
81dca20b17 | ||
|
f203f63c36 | ||
|
26cd218ec0 | ||
|
e402a0376a | ||
|
bac202d4ec | ||
|
bb8d856d17 | ||
|
42075291ce | ||
|
32b128fe40 | ||
|
df63ab5f91 | ||
|
88e3670493 | ||
|
eceeed8290 | ||
|
caa9c80d44 | ||
|
7e76e47d77 | ||
|
ba76c9c17e | ||
|
9555c6fa76 | ||
|
c37a6f5c27 | ||
|
b5af71d798 | ||
|
8f609fa900 | ||
|
5329154121 | ||
|
3a9187d795 | ||
|
e4e23e8017 | ||
|
9638ad4f06 | ||
|
c8de700460 | ||
|
e0f746b5ae | ||
|
9efcc36e15 | ||
|
918a7e64bd | ||
|
7f6874b791 | ||
|
1731462b41 | ||
|
654d50cd1b | ||
|
f631dd5c7b | ||
|
4c5558fa29 | ||
|
2892abc7a8 | ||
|
529385bc5b | ||
|
e3bb5d0fb5 | ||
|
2da930cd41 | ||
|
f41983a7b6 | ||
|
198791ca27 | ||
|
5b02ed9b0b | ||
|
760bad821b | ||
|
b8b2aab9bb | ||
|
a2b08606f1 | ||
|
5d40bd6f8e | ||
|
91639d887f | ||
|
6d234672b8 | ||
|
4e22b53907 | ||
|
f389e315a6 | ||
|
7f54c02a1c | ||
|
eaaa2b97b4 | ||
|
d1362f2ea3 | ||
|
2c56c78467 | ||
|
ec928c943d | ||
|
53bfba0320 | ||
|
847c3e85fa | ||
|
c96b4afcc7 | ||
|
4c741b9ac9 | ||
|
6b184c49ad | ||
|
6991b99ea9 | ||
|
0cc2b4e368 | ||
|
c51f834636 | ||
|
9a567ff5d0 | ||
|
bb420542ef | ||
|
14ec13a2e6 | ||
|
bf2b038632 | ||
|
b2fa6bb3a9 | ||
|
53166d9bba | ||
|
1560e44d36 | ||
|
1dbc8847b1 | ||
|
8bbfd1007d | ||
|
851e117381 | ||
|
2e31a7fbdb | ||
|
f3316160fb | ||
|
48b92ed2a6 | ||
|
4d075095ad | ||
|
9c1985bab3 | ||
|
e11e1a0d5b | ||
|
f202797954 | ||
|
f1ee7586f9 | ||
|
6cc74488a3 | ||
|
dbf9760780 | ||
|
6de1e41628 | ||
|
9977aac9f6 | ||
|
eb4995da86 | ||
|
dffff47a9f | ||
|
96cc982e01 | ||
|
116ab4fcfb | ||
|
c9c0249d4f | ||
|
85833f91e3 | ||
|
b76498852e | ||
|
e75b0e51d9 | ||
|
6b746bc0b3 | ||
|
dd31a59524 | ||
|
ca75b44d3b | ||
|
d9a9a310a8 | ||
|
8ece8cc14d | ||
|
425a83faec | ||
|
f034b313b3 | ||
|
b600cbdc34 | ||
|
ad3fce5b17 | ||
|
25604657b6 | ||
|
996ff027e5 | ||
|
71c595df15 | ||
|
70d00b7d62 | ||
|
e8828d8658 | ||
|
e08f6ee73c | ||
|
f12460e83a | ||
|
baef12a26d | ||
|
79400dd2b1 | ||
|
4409849143 | ||
|
3d77756da0 | ||
|
d45dac4587 | ||
|
38292bcbcc | ||
|
8b8cf9cd7a | ||
|
7c969cbe93 | ||
|
3283cc447e | ||
|
810ba74e75 | ||
|
5e1a20c5de | ||
|
975c59a114 | ||
|
51fcf04b0b | ||
|
d921347d73 | ||
|
5e25b542fb | ||
|
9b4ef98a3d | ||
|
8a862439cc | ||
|
6719953cf1 | ||
|
2bf73eb961 | ||
|
fe5d711fea | ||
|
33c8aaf36f | ||
|
d5366b10fa | ||
|
50d07fd31f | ||
|
792d64a33a | ||
|
adc9550e58 | ||
|
0638b9f133 | ||
|
05531e0bc5 | ||
|
44ec2270f2 | ||
|
ffd4f474ed | ||
|
2c22780c91 | ||
|
1d03d09119 | ||
|
404647c4cb | ||
|
c0b464ae36 | ||
|
e8185e0f9c | ||
|
0592c42df0 | ||
|
87a50c494f | ||
|
8257d7b41d | ||
|
77e471d671 | ||
|
92f5e6e77c | ||
|
4b7be60c27 | ||
|
e9abbf256e | ||
|
002d8c7430 | ||
|
346c7a027d | ||
|
7313e2d1c4 | ||
|
a34a8136c2 | ||
|
3b76dca8f1 | ||
|
30682bf8f5 | ||
|
e7b83326d0 | ||
|
ef55bd2719 | ||
|
0c29785a7e | ||
|
a5b67d4923 | ||
|
34a4df96fc | ||
|
eeacbbb979 | ||
|
2a010e2643 | ||
|
c25785ccf4 | ||
|
050fc2b29e | ||
|
e1e8fa410a | ||
|
becf21b7ea | ||
|
9c3f2f44da | ||
|
793541474a | ||
|
94f9ad6cc4 | ||
|
9a89d4c44c | ||
|
b99ffa0395 | ||
|
f926dc28be | ||
|
5b92d477e2 | ||
|
df561cc582 | ||
|
da60aeb71b | ||
|
684f4867c3 | ||
|
82a7846e6d | ||
|
a9199661a4 | ||
|
d07fc29a96 | ||
|
066919259c | ||
|
a094ba7d5c | ||
|
76a9485f0b | ||
|
413ef66701 | ||
|
1e91a53a8a | ||
|
0db7e51d69 | ||
|
cdf7f75058 | ||
|
84e4151f42 | ||
|
3a9729de06 | ||
|
93e8882433 | ||
|
56a49e323a | ||
|
de97703128 | ||
|
f820d19f55 | ||
|
a2094e9953 | ||
|
e5000f6502 | ||
|
9d63c50a2d | ||
|
ee90951945 | ||
|
8ed87d0739 | ||
|
1ebac0df21 | ||
|
a4e6025607 | ||
|
6ef03b8921 | ||
|
fdd6c87a31 | ||
|
89bfc845f6 | ||
|
9af5c0e0f4 | ||
|
8e92b21d60 | ||
|
9882d9d1c9 | ||
|
55275e48de | ||
|
3da896d369 | ||
|
745aa61466 | ||
|
2672144dbe | ||
|
ccbad4be1b | ||
|
dad3b86ba7 | ||
|
cfb81fc530 | ||
|
56afde24db | ||
|
88fdc1b503 | ||
|
338cd20132 | ||
|
ad38a09052 | ||
|
078e010c54 | ||
|
4ae4a7366a | ||
|
bfefc53040 | ||
|
f8af04ba4b | ||
|
e8a572032a | ||
|
5ef300b4a4 | ||
|
5e3275e547 | ||
|
5a55beee7b | ||
|
b8e52e2117 | ||
|
541f824909 | ||
|
4e763be6ec | ||
|
afaa00d635 | ||
|
1aa03edf6e | ||
|
cafada6ede | ||
|
36e0127e2b | ||
|
0af369bf23 | ||
|
a133f7d82d | ||
|
f66b8affb1 | ||
|
45ab233d01 | ||
|
676617ef19 | ||
|
af3baa99b8 | ||
|
33abefdd14 | ||
|
1cbebd0c9e | ||
|
245942ff14 | ||
|
608a08d2e8 | ||
|
fbb0d7090b | ||
|
40c890e644 | ||
|
35cf4255a6 | ||
|
0da47c409f | ||
|
541f582ac1 | ||
|
191bc33fa3 | ||
|
31168d9ed8 | ||
|
468c41c526 | ||
|
bc684b6ae3 | ||
|
399ec8d108 | ||
|
1820710f06 | ||
|
51d86148e8 | ||
|
e8f973531b | ||
|
4a01cf0608 | ||
|
929a057e1e | ||
|
d24a18023d | ||
|
4d8085a0db | ||
|
8c3ca2161d | ||
|
26720dead0 | ||
|
cda7e81485 | ||
|
0cffb26dfc | ||
|
d21e4aa830 | ||
|
a5dfe0c586 |
1311 changed files with 63004 additions and 52718 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. -->
|
21
.github/issue_template.md
vendored
21
.github/issue_template.md
vendored
|
@ -5,7 +5,7 @@ To make it possible for us to help you, please fill out below information carefu
|
|||
|
||||
Before reporting any issues, please make sure that you're using the latest version.
|
||||
|
||||
We are also available on live chat at https://chat.lbry.io
|
||||
We are also available on live chat at https://chat.lbry.com
|
||||
-->
|
||||
|
||||
|
||||
|
@ -34,3 +34,22 @@ Tell us what happens instead
|
|||
|
||||
## Screenshots
|
||||
<!-- If a screenshot would help explain the bug, please include one or two here -->
|
||||
|
||||
## Internal Use
|
||||
|
||||
### Acceptance Criteria
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
### Definition of Done
|
||||
- [ ] Tested against acceptance criteria
|
||||
- [ ] Tested against the assumptions of the user story
|
||||
- [ ] The project builds without errors
|
||||
- [ ] Unit tests are written and passing
|
||||
- [ ] Tests on devices/browsers listed in the issue have passed
|
||||
- [ ] QA performed & issues resolved
|
||||
- [ ] Refactoring completed
|
||||
- [ ] Any configuration or build changes documented
|
||||
- [ ] Documentation updated
|
||||
- [ ] Peer Code Review performed
|
||||
|
|
1
.github/workflows/deploy.yml
vendored
Normal file
1
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
|
76
.gitignore
vendored
76
.gitignore
vendored
|
@ -1,9 +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
|
||||
*.hprof
|
||||
app/build
|
||||
bin
|
||||
app/debuglib
|
||||
|
|
75
.gitlab-ci.yml
Normal file
75
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,75 @@
|
|||
stages:
|
||||
- build
|
||||
- deploy
|
||||
- release
|
||||
|
||||
|
||||
build apk:
|
||||
stage: build
|
||||
image: lbry/android-base:platform-28
|
||||
before_script:
|
||||
- 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__arm.apk
|
||||
- bin/browser-*-release__arm64.apk
|
||||
expire_in: 1 week
|
||||
script:
|
||||
- export PATH=/usr/bin:$PATH
|
||||
- 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: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
|
||||
- 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__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: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
|
||||
- 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__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 @@
|
|||
|
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx
Normal file
Binary file not shown.
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
Binary file not shown.
BIN
.gitsecret/keys/random_seed
Normal file
BIN
.gitsecret/keys/random_seed
Normal file
Binary file not shown.
BIN
.gitsecret/keys/trustdb.gpg
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
Binary file not shown.
3
.gitsecret/paths/mapping.cfg
Normal file
3
.gitsecret/paths/mapping.cfg
Normal file
|
@ -0,0 +1,3 @@
|
|||
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
|
||||
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
|
||||
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033
|
77
.travis.yml
77
.travis.yml
|
@ -1,77 +0,0 @@
|
|||
sudo: required
|
||||
dist: trusty
|
||||
language: python
|
||||
python:
|
||||
- '2.7'
|
||||
install:
|
||||
- deactivate
|
||||
- export PATH=/usr/bin:$PATH
|
||||
- sudo dpkg --add-architecture i386
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install 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
|
||||
- sudo pip install --upgrade cython==0.25.2 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://dl.google.com/android/repository/android-ndk-r13b-linux-x86_64.zip' -P ~/.buildozer/android/platform/
|
||||
- wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/
|
||||
- wget -q 'https://dl.google.com/android/repository/android-23_r02.zip' -P ~/.buildozer/android/platform/
|
||||
- wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P ~/.buildozer/android/platform/
|
||||
- unzip -qq ~/.buildozer/android/platform/android-ndk-r13b-linux-x86_64.zip -d ~/.buildozer/android/platform/
|
||||
- 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/android-23_r02.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
|
||||
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-6.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-23
|
||||
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
|
||||
- unzip -qq ~/.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
|
||||
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
|
||||
- echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-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
|
||||
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
|
76
BUILD.md
76
BUILD.md
|
@ -1,76 +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
|
||||
* Android NDK
|
||||
* Buildozer
|
||||
* Node.js
|
||||
* npm
|
||||
|
||||
#### Apt Packages
|
||||
Based on the quickstart instructions at http://buildozer.readthedocs.io/en/latest/installation.html
|
||||
```
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-get update
|
||||
sudo apt-get install 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
|
||||
```
|
||||
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
|
||||
```
|
||||
sudo pip install --upgrade cython==0.25.2 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 (eg. browser) |
|
||||
| package.domain | package domain (eg. 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 (13b has been tested to result in a successful build) |
|
||||
| android.ndk_path | Android NDK path. buildozer will automatically download this if not set |
|
||||
| android.sdk_path | Android SDK path. buildozer will automatically download this if not set |
|
||||
| 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 |
|
||||
|
||||
#### 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:
|
||||
|
||||
|
|
31
README.md
31
README.md
|
@ -1,19 +1,36 @@
|
|||
# LBRY Android
|
||||
[![Build Status](https://travis-ci.org/lbryio/lbry-android.svg?branch=master)](https://travis-ci.org/lbryio/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.
|
||||
|
||||
|
||||
<img src="https://spee.ch/@lbry:3f/android-08-homepage.gif" alt="LBRY Android GIF" width="384px" />
|
||||
|
||||
An Android browser and wallet for the [LBRY](https://lbry.io) 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.
|
||||
|
||||
## Installation
|
||||
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
||||
|
||||
1. Via the Google Play Store. While in alpha, it will not show in the Play Store unless you sign up [here](https://lbry.io/android-alpha).
|
||||
1. Via the Google Play Store. Anyone can join the [open beta](https://play.google.com/apps/testing/io.lbry.browser) in order to install the app from the Play Store.
|
||||
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 homescreen if that was created upon install.
|
||||
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
|
||||
|
@ -22,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.io 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.io)
|
||||
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
||||
|
|
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;
|
6829
app/package-lock.json
generated
6829
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "LBRYApp",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start"
|
||||
},
|
||||
"dependencies": {
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.2.0",
|
||||
"react-native": "0.55.3",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-vector-icons": "^4.5.0",
|
||||
"react-native-video": "2.0.0",
|
||||
"react-navigation": "^1.5.12",
|
||||
"react-navigation-redux-helpers": "^1.0.1",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-persist": "^4.8.0",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.10",
|
||||
"redux-thunk": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"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 {
|
||||
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
import React from 'react';
|
||||
import AboutPage from '../page/about';
|
||||
import DiscoverPage from '../page/discover';
|
||||
import FilePage from '../page/file';
|
||||
import SearchPage from '../page/search';
|
||||
import SettingsPage from '../page/settings';
|
||||
import SplashScreen from '../page/splash';
|
||||
import TransactionHistoryPage from '../page/transactionHistory';
|
||||
import WalletPage from '../page/wallet';
|
||||
import SearchInput from '../component/searchInput';
|
||||
import {
|
||||
addNavigationHelpers,
|
||||
DrawerNavigator,
|
||||
StackNavigator,
|
||||
NavigationActions
|
||||
} from 'react-navigation';
|
||||
import { connect } from 'react-redux';
|
||||
import { addListener } from '../utils/redux';
|
||||
import {
|
||||
AppState,
|
||||
AsyncStorage,
|
||||
BackHandler,
|
||||
Linking,
|
||||
NativeModules,
|
||||
TextInput,
|
||||
ToastAndroid
|
||||
} from 'react-native';
|
||||
import { SETTINGS, doHideNotification, selectNotification } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../redux/selectors/settings';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import discoverStyle from '../styles/discover';
|
||||
import searchStyle from '../styles/search';
|
||||
import SearchRightHeaderIcon from "../component/searchRightHeaderIcon";
|
||||
|
||||
const discoverStack = StackNavigator({
|
||||
Discover: {
|
||||
screen: DiscoverPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Discover',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
})
|
||||
},
|
||||
File: {
|
||||
screen: FilePage,
|
||||
navigationOptions: {
|
||||
header: null,
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Search: {
|
||||
screen: SearchPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
drawerLockMode: 'locked-closed'
|
||||
})
|
||||
}
|
||||
}, {
|
||||
headerMode: 'screen',
|
||||
});
|
||||
|
||||
const walletStack = StackNavigator({
|
||||
Wallet: {
|
||||
screen: WalletPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Wallet',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
})
|
||||
},
|
||||
TransactionHistory: {
|
||||
screen: TransactionHistoryPage,
|
||||
navigationOptions: {
|
||||
title: 'Transaction History',
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
headerMode: 'screen'
|
||||
});
|
||||
|
||||
const drawer = DrawerNavigator({
|
||||
DiscoverStack: { screen: discoverStack },
|
||||
WalletStack: { screen: walletStack },
|
||||
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
|
||||
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
|
||||
}, {
|
||||
drawerWidth: 300,
|
||||
headerMode: 'none'
|
||||
});
|
||||
|
||||
export const AppNavigator = new StackNavigator({
|
||||
Splash: {
|
||||
screen: SplashScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Main: {
|
||||
screen: drawer
|
||||
}
|
||||
}, {
|
||||
headerMode: 'none'
|
||||
});
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static supportedDisplayTypes = ['toast'];
|
||||
|
||||
componentWillMount() {
|
||||
AppState.addEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.addEventListener('hardwareBackPress', function() {
|
||||
const { dispatch, nav } = this.props;
|
||||
// There should be a better way to check this
|
||||
if (nav.routes.length > 0) {
|
||||
const subRoutes = nav.routes[0].routes[0].routes;
|
||||
const lastRoute = subRoutes[subRoutes.length - 1];
|
||||
if (nav.routes[0].routes[0].index > 0 &&
|
||||
['About', 'Settings'].indexOf(lastRoute.key) > -1) {
|
||||
dispatch(NavigationActions.back());
|
||||
return true;
|
||||
}
|
||||
if (nav.routes[0].routeName === 'Main') {
|
||||
if (nav.routes[0].routes[0].routes[0].index > 0) {
|
||||
dispatch(NavigationActions.back());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Linking.addEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.removeEventListener('hardwareBackPress');
|
||||
Linking.removeEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { notification } = nextProps;
|
||||
if (notification) {
|
||||
const { displayType, message } = notification;
|
||||
let currentDisplayType;
|
||||
if (displayType.length) {
|
||||
for (let i = 0; i < displayType.length; i++) {
|
||||
const type = displayType[i];
|
||||
if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
|
||||
currentDisplayType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (AppWithNavigationState.supportedDisplayTypes.indexOf(displayType) > -1) {
|
||||
currentDisplayType = displayType;
|
||||
}
|
||||
|
||||
if ('toast' === currentDisplayType) {
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
}
|
||||
|
||||
dispatch(doHideNotification());
|
||||
}
|
||||
}
|
||||
|
||||
_handleAppStateChange = (nextAppState) => {
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleUrl = (evt) => {
|
||||
const { dispatch } = this.props;
|
||||
if (evt.url) {
|
||||
const navigateAction = NavigationActions.navigate({
|
||||
routeName: 'File',
|
||||
key: 'filePage',
|
||||
params: { uri: evt.url }
|
||||
});
|
||||
dispatch(navigateAction);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, nav } = this.props;
|
||||
return (
|
||||
<AppNavigator
|
||||
navigation={addNavigationHelpers({
|
||||
dispatch,
|
||||
state: nav,
|
||||
addListener,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
nav: state.nav,
|
||||
notification: selectNotification(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
|
@ -1,7 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(null, {
|
||||
doNotify,
|
||||
})(Address);
|
|
@ -1,29 +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,
|
||||
doNotify: ({ message: string, displayType: Array<string> }) => void,
|
||||
};
|
||||
|
||||
export default class Address extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { address, doNotify, 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);
|
||||
doNotify({
|
||||
message: 'Address copied',
|
||||
displayType: ['toast'],
|
||||
});
|
||||
}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect(null, null)(Button);
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import buttonStyle from '../../styles/button';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
export default class Button extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
style,
|
||||
text,
|
||||
icon,
|
||||
onPress
|
||||
} = 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);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress}>
|
||||
{icon && <Icon name={icon} size={18} color='#ffffff' class={buttonStyle.icon} /> }
|
||||
{text && (text.trim().length > 0) && <Text style={textStyles}>{text}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectDownloadingForUri,
|
||||
makeSelectLoadingForUri,
|
||||
makeSelectCostInfoForUri
|
||||
} from 'lbry-redux';
|
||||
import { doPurchaseUri, 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 => dispatch(doPurchaseUri(uri)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileDownloadButton);
|
|
@ -1,86 +0,0 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
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,
|
||||
loading,
|
||||
doPause,
|
||||
style,
|
||||
openFile
|
||||
} = this.props;
|
||||
|
||||
if (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 === null && !downloading) {
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={() => {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Purchase Uri', { Uri: uri });
|
||||
}
|
||||
purchaseUri(uri);
|
||||
}}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Download</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<TouchableOpacity style={[style, fileDownloadButtonStyle.container]} onPress={openFile}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Open</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileDownloadButton;
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
selectRewardContentClaimIds
|
||||
} from 'lbry-redux';
|
||||
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)
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileItem);
|
|
@ -1,79 +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 FileItemMedia from '../fileItemMedia';
|
||||
import FilePrice from '../filePrice';
|
||||
import NsfwOverlay from '../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);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
rewardedContentClaimIds,
|
||||
style,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const title = metadata && metadata.title ? metadata.title : uri;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const channelName = claim ? claim.channel_name : null;
|
||||
|
||||
let description = '';
|
||||
if (isResolvingUri && !claim) {
|
||||
description = 'Loading...';
|
||||
} else if (metadata && metadata.description) {
|
||||
description = metadata.description;
|
||||
} else if (claim === null) {
|
||||
description = 'This address contains no content.';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={discoverStyle.container} onPress={() => {
|
||||
if (NativeModules.Mixpanel) {
|
||||
NativeModules.Mixpanel.track('Discover Tap', { Uri: uri });
|
||||
}
|
||||
navigation.navigate({ routeName: 'File', key: 'filePage', params: { uri } });
|
||||
}
|
||||
}>
|
||||
<FileItemMedia title={title} thumbnail={thumbnail} blurRadius={obscureNsfw ? 15 : 0} resizeMode="cover" />
|
||||
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
|
||||
<Text style={discoverStyle.fileItemName}>{title}</Text>
|
||||
{channelName &&
|
||||
<Text style={discoverStyle.channelName}>{channelName}</Text>}
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
||||
</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,59 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, Image, View } from 'react-native';
|
||||
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,
|
||||
];
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
autoThumbStyle:
|
||||
FileItemMedia.AUTO_THUMB_STYLES[
|
||||
Math.floor(Math.random() * FileItemMedia.AUTO_THUMB_STYLES.length)
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let style = this.props.style;
|
||||
const { title, thumbnail, blurRadius, resizeMode } = this.props;
|
||||
const atStyle = this.state.autoThumbStyle;
|
||||
|
||||
if (thumbnail && ((typeof thumbnail) === 'string')) {
|
||||
if (style == null) {
|
||||
style = fileItemMediaStyle.thumbnail;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image source={{uri: thumbnail}}
|
||||
blurRadius={blurRadius}
|
||||
resizeMode={resizeMode ? resizeMode : "cover"}
|
||||
style={style} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
|
||||
<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,184 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI } from 'lbry-redux';
|
||||
import { FlatList } from 'react-native';
|
||||
import FileItem from '../fileItem';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
|
||||
// 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 { fileInfos, hideFilter, checkPending, navigation, 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}
|
||||
data={items}
|
||||
keyExtractor={(item, index) => item}
|
||||
renderItem={({item}) => (
|
||||
<FileItem style={discoverStyle.fileItem}
|
||||
uri={item}
|
||||
navigation={navigation} />
|
||||
)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileList;
|
|
@ -1,20 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchCostInfoForUri,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFetchingCostInfoForUri,
|
||||
makeSelectClaimForUri
|
||||
} from 'lbry-redux';
|
||||
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={costInfo.cost}
|
||||
isEstimate={isEstimate}
|
||||
showFree
|
||||
showFullPrice={showFullPrice}>???</CreditAmount>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilePrice;
|
|
@ -1,9 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import Link from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: (data) => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
export default connect(null, perform)(Link);
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Linking, Text, TouchableOpacity } from 'react-native';
|
||||
|
||||
export default class Link extends React.PureComponent {
|
||||
handlePress = () => {
|
||||
const { error, href, navigation, notify } = this.props;
|
||||
|
||||
if (navigation && href.startsWith('#')) {
|
||||
navigation.navigate(href.substring(1));
|
||||
} else {
|
||||
Linking.openURL(href).catch(err => notify({
|
||||
message: error,
|
||||
displayType: ['toast']
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
style,
|
||||
text
|
||||
} = this.props;
|
||||
|
||||
let styles = [];
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress ? onPress : this.handlePress}>
|
||||
<Text style={styles}>{text}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../../redux/selectors/settings';
|
||||
import MediaPlayer from './view';
|
||||
|
||||
const select = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(MediaPlayer);
|
|
@ -1,319 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
NativeModules,
|
||||
PanResponder,
|
||||
Text,
|
||||
View,
|
||||
ScrollView,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import FileItemMedia from '../fileItemMedia';
|
||||
import mediaPlayerStyle from '../../styles/mediaPlayer';
|
||||
|
||||
class MediaPlayer extends React.PureComponent {
|
||||
static ControlsTimeout = 3000;
|
||||
|
||||
seekResponder = null;
|
||||
|
||||
seekerWidth = 0;
|
||||
|
||||
trackingOffset = 0;
|
||||
|
||||
tracking = null;
|
||||
|
||||
video = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: 'stretch',
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: true,
|
||||
fullscreenMode: false,
|
||||
areControlsVisible: true,
|
||||
controlsTimeout: -1,
|
||||
seekerOffset: 0,
|
||||
seekerPosition: 0,
|
||||
firstPlay: true
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
if (this.props.onMediaLoaded) {
|
||||
this.props.onMediaLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
onProgress = (data) => {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
|
||||
if (!this.state.seeking) {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
}
|
||||
|
||||
if (this.state.firstPlay) {
|
||||
if (NativeModules.Mixpanel) {
|
||||
const { uri } = this.props;
|
||||
NativeModules.Mixpanel.track('Play', { Uri: uri });
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
hidePlayerControls() {
|
||||
const player = this;
|
||||
let timeout = setTimeout(() => {
|
||||
player.setState({ areControlsVisible: false });
|
||||
}, MediaPlayer.ControlsTimeout);
|
||||
player.setState({ controlsTimeout: timeout });
|
||||
}
|
||||
|
||||
togglePlay = () => {
|
||||
this.showPlayerControls();
|
||||
this.setState({ paused: !this.state.paused });
|
||||
}
|
||||
|
||||
toggleFullscreenMode = () => {
|
||||
this.showPlayerControls();
|
||||
const { onFullscreenToggled } = this.props;
|
||||
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
|
||||
this.setState({ resizeMode: this.state.fullscreenMode ? 'contain' : 'stretch' });
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(this.state.fullscreenMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true });
|
||||
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) {
|
||||
const offset = this.getTrackingOffset();
|
||||
if (val < offset) {
|
||||
val = offset;
|
||||
} else if (val >= (offset + this.seekerWidth)) {
|
||||
return offset + 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();
|
||||
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.onEnd();
|
||||
} else {
|
||||
this.seekTo(time);
|
||||
this.setState({ seeking: false });
|
||||
}
|
||||
this.hidePlayerControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTrackingOffset() {
|
||||
return this.state.fullscreenMode ? this.trackingOffset : 0;
|
||||
}
|
||||
|
||||
getCurrentTimeForSeekerPosition() {
|
||||
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
|
||||
}
|
||||
|
||||
calculateSeekerPosition() {
|
||||
if (this.state.fullscreenMode) {
|
||||
return this.getTrackingOffset() + (this.seekerWidth * this.getCurrentTimePercentage());
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearControlsTimeout();
|
||||
this.setState({ paused: true, fullscreenMode: false });
|
||||
const { onFullscreenToggled } = this.props;
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(false);
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayerControls() {
|
||||
if (this.state.areControlsVisible) {
|
||||
return (
|
||||
<View style={mediaPlayerStyle.playerControlsContainer}>
|
||||
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
|
||||
onPress={this.togglePlay}>
|
||||
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
|
||||
{!this.state.paused && <Icon name="pause" size={32} 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;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { backgroundPlayEnabled, fileInfo, thumbnail, style } = this.props;
|
||||
const flexCompleted = this.getCurrentTimePercentage() * 100;
|
||||
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
|
||||
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}>
|
||||
<Video source={{ uri: 'file:///' + fileInfo.download_path }}
|
||||
ref={(ref: Video) => { this.video = ref }}
|
||||
resizeMode={this.state.resizeMode}
|
||||
playInBackground={backgroundPlayEnabled}
|
||||
style={mediaPlayerStyle.player}
|
||||
rate={this.state.rate}
|
||||
volume={this.state.volume}
|
||||
paused={this.state.paused}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
/>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.showPlayerControls}>
|
||||
{this.renderPlayerControls()}
|
||||
</TouchableOpacity>
|
||||
|
||||
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
|
||||
<View style={trackingStyle} onLayout={(evt) => {
|
||||
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||
}}>
|
||||
<View style={mediaPlayerStyle.progress}>
|
||||
<View style={[mediaPlayerStyle.innerProgressCompleted, { flex: flexCompleted }]} />
|
||||
<View style={[mediaPlayerStyle.innerProgressRemaining, { flex: flexRemaining }]} />
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
{this.state.areControlsVisible &&
|
||||
<View style={[mediaPlayerStyle.seekerHandle, { left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
|
||||
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaPlayer;
|
|
@ -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,47 +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 Feather from 'react-native-vector-icons/Feather';
|
||||
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>
|
||||
<TouchableOpacity style={pageHeaderStyle.left}>
|
||||
<Feather name="arrow-left" size={24} onPress={onBackPressed} style={pageHeaderStyle.backIcon} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PageHeader;
|
|
@ -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.Mixpanel) {
|
||||
NativeModules.Mixpanel.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,24 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
} from 'lbry-redux';
|
||||
import { selectShowNsfw } from '../../redux/selectors/settings';
|
||||
import SearchResultItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state)
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SearchResultItem);
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import FileItemMedia from '../fileItemMedia';
|
||||
import NsfwOverlay from '../../component/nsfwOverlay';
|
||||
import searchStyle from '../../styles/search';
|
||||
|
||||
class SearchResultItem extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
showUri,
|
||||
isDownloaded,
|
||||
style,
|
||||
onPress,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const title = metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
|
||||
|
||||
let name;
|
||||
let channel;
|
||||
if (claim) {
|
||||
name = claim.name;
|
||||
channel = claim.channel_name;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={style} onPress={onPress}>
|
||||
<View style={searchStyle.thumbnailContainer}>
|
||||
<FileItemMedia style={searchStyle.thumbnail}
|
||||
blurRadius={obscureNsfw ? 15 : 0}
|
||||
title={title}
|
||||
thumbnail={metadata ? metadata.thumbnail : null} />
|
||||
</View>
|
||||
<View style={searchStyle.detailsContainer}>
|
||||
{isResolvingUri && <Text style={searchStyle.loading}>Loading...</Text>}
|
||||
{!isResolvingUri && <Text style={searchStyle.title}>{title || name}</Text>}
|
||||
{!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResultItem;
|
|
@ -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,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,59 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View, Linking } from 'react-native';
|
||||
import { buildURI, formatCredits } from 'lbry-redux';
|
||||
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={() => navigation && navigation.navigate({
|
||||
routeName: 'File',
|
||||
key: 'filePage',
|
||||
params: { uri: 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.io/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,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doUpdateSearchQuery, selectSearchState as selectSearch } from 'lbry-redux';
|
||||
import UriBar from './view';
|
||||
|
||||
const select = state => {
|
||||
const { ...searchState } = selectSearch(state);
|
||||
|
||||
return {
|
||||
...searchState
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(UriBar);
|
|
@ -1,38 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, normalizeURI } from 'lbry-redux';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import uriBarStyle from '../../../styles/uriBar';
|
||||
|
||||
class UriBarItem extends React.PureComponent {
|
||||
render() {
|
||||
const { item, onPress } = this.props;
|
||||
const { type, value } = item;
|
||||
|
||||
let icon;
|
||||
switch (type) {
|
||||
case SEARCH_TYPES.CHANNEL:
|
||||
icon = <Feather name="at-sign" size={18} />
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.SEARCH:
|
||||
icon = <Feather name="search" size={18} />
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.FILE:
|
||||
default:
|
||||
icon = <Feather name="compass" size={18} />
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
|
||||
{icon}
|
||||
<Text style={uriBarStyle.itemText} numberOfLines={1}>{value}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBarItem;
|
|
@ -1,121 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, isNameValid, normalizeURI } from 'lbry-redux';
|
||||
import { FlatList, Keyboard, TextInput, View } from 'react-native';
|
||||
import UriBarItem from './internal/uri-bar-item';
|
||||
import uriBarStyle from '../../styles/uriBar';
|
||||
|
||||
class UriBar extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 500;
|
||||
|
||||
textInput = null;
|
||||
|
||||
keyboardDidHideListener = null;
|
||||
|
||||
componentDidMount () {
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidHideListener) {
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
changeTextTimeout: null,
|
||||
currentValue: null,
|
||||
inputText: null,
|
||||
focused: false
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeText = text => {
|
||||
const newValue = text ? text : '';
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
const { updateSearchQuery } = this.props;
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
updateSearchQuery(text);
|
||||
}, UriBar.INPUT_TIMEOUT);
|
||||
this.setState({ inputText: newValue, currentValue: newValue, changeTextTimeout: timeout });
|
||||
}
|
||||
|
||||
handleItemPress = (item) => {
|
||||
const { navigation, updateSearchQuery } = this.props;
|
||||
const { type, value } = item;
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
if (SEARCH_TYPES.SEARCH === type) {
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value }});
|
||||
} else {
|
||||
navigation.navigate({ routeName: 'File', key: 'filePage', params: { uri: normalizeURI(value) }});
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardDidHide = () => {
|
||||
if (this.textInput) {
|
||||
this.textInput.blur();
|
||||
}
|
||||
this.setState({ focused: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation, suggestions, updateSearchQuery, value } = this.props;
|
||||
if (this.state.currentValue === null) {
|
||||
this.setState({ currentValue: value });
|
||||
}
|
||||
|
||||
let style = [uriBarStyle.overlay];
|
||||
if (this.state.focused) {
|
||||
style.push(uriBarStyle.inFocus);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{this.state.focused && (
|
||||
<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 style={uriBarStyle.uriContainer}>
|
||||
<TextInput ref={(ref) => { this.textInput = ref }}
|
||||
style={uriBarStyle.uriText}
|
||||
selectTextOnFocus={true}
|
||||
placeholder={'Search for videos, music, games 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 })}
|
||||
onChangeText={this.handleChangeText}
|
||||
onSubmitEditing={() => {
|
||||
if (this.state.inputText) {
|
||||
let inputText = this.state.inputText;
|
||||
if (isNameValid(inputText)) {
|
||||
navigation.navigate({ routeName: 'File', key: 'filePage', params: { uri: normalizeURI(inputText) }});
|
||||
} else {
|
||||
// Open the search page with the query populated
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: inputText }});
|
||||
}
|
||||
}
|
||||
}}/>
|
||||
</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);
|
|
@ -1,47 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
checkAddressIsMine: string => void,
|
||||
receiveAddress: string,
|
||||
getNewAddress: () => void,
|
||||
gettingNewAddress: boolean,
|
||||
};
|
||||
|
||||
class WalletAddress extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
} else {
|
||||
checkAddressIsMine(receiveAddress);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Receive Credits</Text>
|
||||
<Text style={[walletStyle.text, walletStyle.bottomMarginMedium]}>Use this wallet address to receive credits sent by another user (or yourself).</Text>
|
||||
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
|
||||
<Button style={[walletStyle.button, walletStyle.bottomMarginLarge]}
|
||||
icon={'refresh'}
|
||||
text={'Get New Address'}
|
||||
onPress={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
<Text style={walletStyle.smallText}>
|
||||
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletAddress;
|
|
@ -1,9 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import WalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(WalletBalance);
|
|
@ -1,29 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux'
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletBalance extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
return (
|
||||
<View style={walletStyle.balanceCard}>
|
||||
<Image style={walletStyle.balanceBackground} resizeMode={'cover'} source={require('../../assets/stripe.png')} />
|
||||
<Text style={walletStyle.balanceTitle}>Balance</Text>
|
||||
<Text style={walletStyle.balanceCaption}>You currently have</Text>
|
||||
<Text style={walletStyle.balance}>
|
||||
{(balance || balance === 0) && (formatCredits(balance, 2) + ' LBC')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletBalance;
|
|
@ -1,22 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doNotify,
|
||||
doSendDraftTransaction,
|
||||
selectDraftTransaction,
|
||||
selectDraftTransactionError,
|
||||
selectBalance
|
||||
} from 'lbry-redux';
|
||||
import WalletSend from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
sendToAddress: (address, amount) => dispatch(doSendDraftTransaction(address, amount)),
|
||||
notify: (data) => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
draftTransaction: selectDraftTransaction(state),
|
||||
transactionError: selectDraftTransactionError(state),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletSend);
|
|
@ -1,89 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { regexAddress } from 'lbry-redux';
|
||||
import { TextInput, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type DraftTransaction = {
|
||||
address: string,
|
||||
amount: ?number, // So we can use a placeholder in the input
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sendToAddress: (string, number) => void,
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletSend extends React.PureComponent<Props> {
|
||||
state = {
|
||||
amount: null,
|
||||
address: null
|
||||
};
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { draftTransaction, transactionError } = nextProps;
|
||||
if (transactionError && transactionError.trim().length > 0) {
|
||||
this.setState({ address: draftTransaction.address, amount: draftTransaction.amount });
|
||||
}
|
||||
}
|
||||
|
||||
handleSend = () => {
|
||||
const { balance, sendToAddress, notify } = this.props;
|
||||
const { address, amount } = this.state;
|
||||
if (address && !regexAddress.test(address)) {
|
||||
notify({
|
||||
message: 'The recipient address is not a valid LBRY address.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > balance) {
|
||||
notify({
|
||||
message: 'Insufficient credits',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount && address) {
|
||||
sendToAddress(address, parseFloat(amount));
|
||||
this.setState({ address: null, amount: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
const canSend = this.state.address &&
|
||||
this.state.amount > 0 &&
|
||||
this.state.address.trim().length > 0;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Send Credits</Text>
|
||||
<Text style={walletStyle.text}>Amount</Text>
|
||||
<View style={[walletStyle.amountRow, walletStyle.bottomMarginMedium]}>
|
||||
<TextInput onChangeText={value => this.setState({amount: value})}
|
||||
keyboardType={'numeric'}
|
||||
value={this.state.amount}
|
||||
style={[walletStyle.input, walletStyle.amountInput]} />
|
||||
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>
|
||||
</View>
|
||||
<Text style={walletStyle.text}>Recipient address</Text>
|
||||
<View style={walletStyle.row}>
|
||||
<TextInput onChangeText={value => this.setState({address: value})}
|
||||
placeholder={'bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs'}
|
||||
value={this.state.address}
|
||||
style={[walletStyle.input, walletStyle.addressInput, walletStyle.bottomMarginMedium]} />
|
||||
<Button text={'Send'}
|
||||
style={[walletStyle.button, walletStyle.sendButton]}
|
||||
disabled={!canSend}
|
||||
onPress={this.handleSend} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletSend;
|
|
@ -1,5 +0,0 @@
|
|||
const Constants = {
|
||||
SETTING_ALPHA_UNDERSTANDS_RISKS: "ALPHA_UNDERSTANDS_RISKS"
|
||||
};
|
||||
|
||||
export default Constants;
|
140
app/src/index.js
140
app/src/index.js
|
@ -1,140 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Provider, connect } from 'react-redux';
|
||||
import DiscoverPage from './page/discover';
|
||||
import {
|
||||
AppRegistry,
|
||||
AppState,
|
||||
AsyncStorage,
|
||||
Text,
|
||||
View,
|
||||
NativeModules
|
||||
} from 'react-native';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import {
|
||||
StackNavigator, addNavigationHelpers
|
||||
} from 'react-navigation';
|
||||
import { AppNavigator } from './component/AppNavigator';
|
||||
import AppWithNavigationState from './component/AppNavigator';
|
||||
import { persistStore, autoRehydrate } from 'redux-persist';
|
||||
import createCompressor from 'redux-persist-transform-compress';
|
||||
import createFilter from 'redux-persist-transform-filter';
|
||||
import thunk from 'redux-thunk';
|
||||
import {
|
||||
Lbry,
|
||||
claimsReducer,
|
||||
costInfoReducer,
|
||||
fileInfoReducer,
|
||||
notificationsReducer,
|
||||
searchReducer,
|
||||
walletReducer
|
||||
} from 'lbry-redux';
|
||||
import settingsReducer from './redux/reducers/settings';
|
||||
import moment from 'moment';
|
||||
import { reactNavigationMiddleware } from './utils/redux';
|
||||
|
||||
function isFunction(object) {
|
||||
return typeof object === 'function';
|
||||
}
|
||||
|
||||
function isNotFunction(object) {
|
||||
return !isFunction(object);
|
||||
}
|
||||
|
||||
function createBulkThunkMiddleware() {
|
||||
return ({ dispatch, getState }) => next => action => {
|
||||
if (action.type === 'BATCH_ACTIONS') {
|
||||
action.actions.filter(isFunction).map(actionFn => actionFn(dispatch, getState));
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
function enableBatching(reducer) {
|
||||
return function batchingReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'BATCH_ACTIONS':
|
||||
return action.actions.filter(isNotFunction).reduce(batchingReducer, state);
|
||||
default:
|
||||
return reducer(state, action);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const router = AppNavigator.router;
|
||||
const navAction = router.getActionForPathAndParams('Splash');
|
||||
const initialNavState = router.getStateForAction(navAction);
|
||||
const navigatorReducer = (state = initialNavState, action) => {
|
||||
const nextState = AppNavigator.router.getStateForAction(action, state);
|
||||
return nextState || state;
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
claims: claimsReducer,
|
||||
costInfo: costInfoReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
notifications: notificationsReducer,
|
||||
search: searchReducer,
|
||||
wallet: walletReducer,
|
||||
nav: navigatorReducer,
|
||||
settings: settingsReducer
|
||||
});
|
||||
|
||||
const bulkThunk = createBulkThunkMiddleware();
|
||||
const middleware = [thunk, bulkThunk, reactNavigationMiddleware];
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const composeEnhancers = compose;
|
||||
|
||||
const store = createStore(
|
||||
enableBatching(reducers),
|
||||
{}, // initial state,
|
||||
composeEnhancers(
|
||||
autoRehydrate(),
|
||||
applyMiddleware(...middleware)
|
||||
)
|
||||
);
|
||||
|
||||
const compressor = createCompressor();
|
||||
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||
const settingsFilter = createFilter('settings', ['clientSettings']);
|
||||
const walletFilter = createFilter('wallet', ['receiveAddress']);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ['claims', 'subscriptions', 'settings', 'wallet'],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
|
||||
debounce: 10000,
|
||||
storage: AsyncStorage
|
||||
};
|
||||
|
||||
persistStore(store, persistOptions, err => {
|
||||
if (err) {
|
||||
console.log('Unable to load saved SETTINGS');
|
||||
}
|
||||
});
|
||||
|
||||
class LBRYApp extends React.Component {
|
||||
componentDidMount() {
|
||||
AsyncStorage.getItem('hasLaunched').then(value => {
|
||||
if (value == null || value !== 'true') {
|
||||
AsyncStorage.setItem('hasLaunched', 'true');
|
||||
// only set firstLaunchTime since we've determined that this is the first app launch ever
|
||||
AsyncStorage.setItem('firstLaunchTime', String(moment().unix()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppWithNavigationState />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppRegistry.registerComponent('LBRYApp', () => LBRYApp);
|
||||
|
||||
export default LBRYApp;
|
127
app/src/main/AndroidManifest.xml
Normal file
127
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.lbry.browser"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_lbry" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_color"
|
||||
android:resource="@color/lbryGreen" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="@string/default_notification_channel_id"/>
|
||||
|
||||
<meta-data android:name="wakelock" android:value="0"/>
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="lbry" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" android:host="open.lbry.com"/>
|
||||
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/..*/*" />
|
||||
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/.*:.*" />
|
||||
<data android:scheme="https" android:host="lbry.tv" android:pathPattern="/.*#.*" />
|
||||
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/..*/*" />
|
||||
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/.*:.*" />
|
||||
<data android:scheme="https" android:host="lbry.lat" android:pathPattern="/.*#.*" />
|
||||
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/..*/*" />
|
||||
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/.*:.*" />
|
||||
<data android:scheme="https" android:host="lbry.fr" android:pathPattern="/.*#.*" />
|
||||
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/..*/*" />
|
||||
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/.*:.*" />
|
||||
<data android:scheme="https" android:host="lbry.in" android:pathPattern="/.*#.*" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".FirstRunActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBarTranslucent" />
|
||||
<activity
|
||||
android:name=".VerificationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBarTranslucent"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
tools:replace="screenOrientation" />
|
||||
|
||||
<service
|
||||
android:name="io.lbry.browser.LbrynetMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="io.lbry.browser.LocalFileProvider"
|
||||
android:authorities="io.lbry.browser.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
BIN
app/src/main/assets/font_awesome_5_free_regular.otf
Normal file
Binary file not shown.
BIN
app/src/main/assets/font_awesome_5_free_solid.otf
Normal file
BIN
app/src/main/assets/font_awesome_5_free_solid.otf
Normal file
Binary file not shown.
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
252
app/src/main/java/io/lbry/browser/FirstRunActivity.java
Normal file
252
app/src/main/java/io/lbry/browser/FirstRunActivity.java
Normal file
|
@ -0,0 +1,252 @@
|
|||
package io.lbry.browser;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import io.lbry.browser.exceptions.AuthTokenInvalidatedException;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.Lbry;
|
||||
import io.lbry.browser.utils.LbryAnalytics;
|
||||
import io.lbry.browser.utils.Lbryio;
|
||||
import io.lbry.lbrysdk.LbrynetService;
|
||||
import io.lbry.lbrysdk.ServiceHelper;
|
||||
import io.lbry.lbrysdk.Utils;
|
||||
|
||||
public class FirstRunActivity extends AppCompatActivity {
|
||||
|
||||
private BroadcastReceiver sdkReceiver;
|
||||
private BroadcastReceiver authReceiver;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_first_run);
|
||||
|
||||
TextView welcomeTos = findViewById(R.id.welcome_text_view_tos);
|
||||
welcomeTos.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
welcomeTos.setText(HtmlCompat.fromHtml(getString(R.string.welcome_tos), HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||
|
||||
findViewById(R.id.welcome_link_use_lbry).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
finishFirstRun();
|
||||
}
|
||||
});
|
||||
|
||||
registerAuthReceiver();
|
||||
findViewById(R.id.welcome_wait_container).setVisibility(View.VISIBLE);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(MainActivity.ACTION_SDK_READY);
|
||||
filter.addAction(LbrynetService.ACTION_STOP_SERVICE);
|
||||
sdkReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (MainActivity.ACTION_SDK_READY.equals(action)) {
|
||||
// authenticate after we receive the sdk ready event
|
||||
authenticate();
|
||||
} else if (LbrynetService.ACTION_STOP_SERVICE.equals(action)) {
|
||||
finish();
|
||||
if (MainActivity.instance != null) {
|
||||
MainActivity.instance.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(sdkReceiver, filter);
|
||||
|
||||
CheckInstallIdTask task = new CheckInstallIdTask(this, new CheckInstallIdTask.InstallIdHandler() {
|
||||
@Override
|
||||
public void onInstallIdChecked(boolean result) {
|
||||
// start the sdk from FirstRun
|
||||
boolean serviceRunning = MainActivity.isServiceRunning(MainActivity.instance, LbrynetService.class);
|
||||
if (!serviceRunning) {
|
||||
Lbry.SDK_READY = false;
|
||||
ServiceHelper.start(MainActivity.instance, "", LbrynetService.class, "lbrynetservice");
|
||||
}
|
||||
|
||||
if (result) {
|
||||
// install_id generated and validated, authenticate now
|
||||
authenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
// we weren't able to generate the install_id ourselves, depend on the sdk for that
|
||||
if (Lbry.SDK_READY) {
|
||||
authenticate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LbryAnalytics.setCurrentScreen(this, "First Run", "FirstRun");
|
||||
}
|
||||
|
||||
private void registerAuthReceiver() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS);
|
||||
filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_FAILED);
|
||||
authReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS.equals(intent.getAction())) {
|
||||
handleAuthenticationSuccess();
|
||||
} else {
|
||||
handleAuthenticationFailed();
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(authReceiver, filter);
|
||||
}
|
||||
|
||||
private void handleAuthenticationSuccess() {
|
||||
// first_auth completed event
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean firstAuthCompleted = sp.getBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, false);
|
||||
if (!firstAuthCompleted) {
|
||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_USER_AUTH);
|
||||
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, true).apply();
|
||||
}
|
||||
|
||||
findViewById(R.id.welcome_wait_container).setVisibility(View.GONE);
|
||||
findViewById(R.id.welcome_display).setVisibility(View.VISIBLE);
|
||||
findViewById(R.id.welcome_link_use_lbry).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void handleAuthenticationFailed() {
|
||||
findViewById(R.id.welcome_progress_bar).setVisibility(View.GONE);
|
||||
((TextView) findViewById(R.id.welcome_wait_text)).setText(R.string.startup_failed);
|
||||
}
|
||||
|
||||
private void authenticate() {
|
||||
new AuthenticateTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void finishFirstRun() {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, true).apply();
|
||||
|
||||
// first_run_completed event
|
||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_RUN_COMPLETED);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Helper.unregisterReceiver(authReceiver, this);
|
||||
Helper.unregisterReceiver(sdkReceiver, this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void generateIdAndAuthenticate() {
|
||||
|
||||
}
|
||||
|
||||
private static class CheckInstallIdTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Context context;
|
||||
private final InstallIdHandler handler;
|
||||
public CheckInstallIdTask(Context context, InstallIdHandler handler) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
}
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
// Load the installation id from the file system
|
||||
String lbrynetDir = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet");
|
||||
File dir = new File(lbrynetDir);
|
||||
boolean dirExists = dir.isDirectory();
|
||||
if (!dirExists) {
|
||||
dirExists = dir.mkdirs();
|
||||
}
|
||||
|
||||
if (!dirExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String installIdPath = String.format("%s/install_id", lbrynetDir);
|
||||
File file = new File(installIdPath);
|
||||
String installId = null;
|
||||
if (!file.exists()) {
|
||||
// generate the install_id
|
||||
installId = Lbry.generateId();
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(file));
|
||||
writer.write(installId);
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
} finally {
|
||||
Helper.closeCloseable(writer);
|
||||
}
|
||||
} else {
|
||||
// read the installation id from the file
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new BufferedReader(new InputStreamReader(new FileInputStream(installIdPath)));
|
||||
installId = reader.readLine();
|
||||
} catch (IOException ex) {
|
||||
return false;
|
||||
} finally {
|
||||
Helper.closeCloseable(reader);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Helper.isNullOrEmpty(installId)) {
|
||||
Lbry.INSTALLATION_ID = installId;
|
||||
}
|
||||
return !Helper.isNullOrEmpty(installId);
|
||||
}
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (handler != null) {
|
||||
handler.onInstallIdChecked(result);
|
||||
}
|
||||
}
|
||||
|
||||
public interface InstallIdHandler {
|
||||
void onInstallIdChecked(boolean result);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthenticateTask extends AsyncTask<Void, Void, Void> {
|
||||
private final Context context;
|
||||
public AuthenticateTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
Lbryio.authenticate(context);
|
||||
} catch (AuthTokenInvalidatedException ex) {
|
||||
// pass
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
191
app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
Normal file
191
app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
Normal file
|
@ -0,0 +1,191 @@
|
|||
package io.lbry.browser;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.analytics.FirebaseAnalytics;
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import io.lbry.browser.data.DatabaseHelper;
|
||||
import io.lbry.browser.model.lbryinc.LbryNotification;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.LbryAnalytics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class LbrynetMessagingService extends FirebaseMessagingService {
|
||||
public static final String ACTION_NOTIFICATION_RECEIVED = "io.lbry.browser.Broadcast.NotificationReceived";
|
||||
|
||||
private static final String TAG = "LbrynetMessagingService";
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL";
|
||||
private static final String TYPE_COMMENT = "comment";
|
||||
private static final String TYPE_SUBSCRIPTION = "subscription";
|
||||
private static final String TYPE_REWARD = "reward";
|
||||
private static final String TYPE_INTERESTS = "interests";
|
||||
private static final String TYPE_CREATOR = "creator";
|
||||
private FirebaseAnalytics firebaseAnalytics;
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
if (firebaseAnalytics == null) {
|
||||
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
|
||||
}
|
||||
|
||||
Map<String, String> payload = remoteMessage.getData();
|
||||
if (payload != null) {
|
||||
String type = payload.get("type");
|
||||
String url = payload.get("target");
|
||||
String title = payload.get("title");
|
||||
String body = payload.get("body");
|
||||
String name = payload.get("name"); // notification name
|
||||
String hash = payload.get("hash"); // comment hash
|
||||
|
||||
if (type != null && getEnabledTypes().contains(type) && body != null && body.trim().length() > 0) {
|
||||
// only log the receive event for valid notifications received
|
||||
if (firebaseAnalytics != null) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("name", name);
|
||||
firebaseAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_RECEIVE, bundle);
|
||||
}
|
||||
|
||||
if (!Helper.isNullOrEmpty(hash)) {
|
||||
url = String.format("%s?comment_hash=%s", url, hash);
|
||||
}
|
||||
|
||||
sendNotification(title, body, type, url, name);
|
||||
}
|
||||
|
||||
// persist the notification data
|
||||
try {
|
||||
DatabaseHelper helper = DatabaseHelper.getInstance();
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
LbryNotification lnotification = new LbryNotification();
|
||||
lnotification.setTitle(title);
|
||||
lnotification.setDescription(body);
|
||||
lnotification.setTargetUrl(url);
|
||||
lnotification.setTimestamp(new Date());
|
||||
DatabaseHelper.createOrUpdateNotification(lnotification, db);
|
||||
|
||||
// send a broadcast
|
||||
Intent intent = new Intent(ACTION_NOTIFICATION_RECEIVED);
|
||||
intent.putExtra("title", title);
|
||||
intent.putExtra("body", body);
|
||||
intent.putExtra("url", url);
|
||||
intent.putExtra("timestamp", lnotification.getTimestamp().getTime());
|
||||
sendBroadcast(intent);
|
||||
} catch (Exception ex) {
|
||||
// don't fail if any error occurs while saving a notification
|
||||
Log.e(TAG, "could not save notification", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewToken(String token) {
|
||||
//Log.d(TAG, "Refreshed token: " + token);
|
||||
|
||||
// If you want to send messages to this application instance or
|
||||
// manage this apps subscriptions on the server side, send the
|
||||
// Instance ID token to your app server.
|
||||
sendRegistrationToServer(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist token to third-party servers.
|
||||
*
|
||||
* Modify this method to associate the user's FCM InstanceID token with any server-side account
|
||||
* maintained by your application.
|
||||
*
|
||||
* @param token The new token.
|
||||
*/
|
||||
private void sendRegistrationToServer(String token) {
|
||||
// TODO: Implement this method to send token to your app server.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a simple notification containing the received FCM message.
|
||||
*
|
||||
* @param messageBody FCM message body received.
|
||||
*/
|
||||
private void sendNotification(String title, String messageBody, String type, String url, String name) {
|
||||
//Intent intent = new Intent(this, MainActivity.class);
|
||||
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
if (url == null) {
|
||||
if (TYPE_REWARD.equals(type)) {
|
||||
url = "lbry://?rewards";
|
||||
} else {
|
||||
// default to home page
|
||||
url = "lbry://?discover";
|
||||
}
|
||||
}
|
||||
|
||||
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
launchIntent.putExtra("notification_name", name);
|
||||
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||
|
||||
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
NotificationCompat.Builder notificationBuilder =
|
||||
new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setColor(ContextCompat.getColor(this, R.color.lbryGreen))
|
||||
.setSmallIcon(R.drawable.ic_lbry)
|
||||
.setContentTitle(title)
|
||||
.setContentText(messageBody)
|
||||
.setAutoCancel(true)
|
||||
.setSound(defaultSoundUri)
|
||||
.setContentIntent(pendingIntent);
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// Since android Oreo notification channel is needed.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
notificationManager.notify(3, notificationBuilder.build());
|
||||
}
|
||||
|
||||
public List<String> getEnabledTypes() {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
List<String> enabledTypes = new ArrayList<String>();
|
||||
|
||||
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_COMMENTS, true)) {
|
||||
enabledTypes.add(TYPE_COMMENT);
|
||||
}
|
||||
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) {
|
||||
enabledTypes.add(TYPE_SUBSCRIPTION);
|
||||
}
|
||||
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_REWARDS, true)) {
|
||||
enabledTypes.add(TYPE_REWARD);
|
||||
}
|
||||
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS, true)) {
|
||||
enabledTypes.add(TYPE_INTERESTS);
|
||||
}
|
||||
if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CREATOR, true)) {
|
||||
enabledTypes.add(TYPE_CREATOR);
|
||||
}
|
||||
|
||||
return enabledTypes;
|
||||
}
|
||||
}
|
7
app/src/main/java/io/lbry/browser/LocalFileProvider.java
Normal file
7
app/src/main/java/io/lbry/browser/LocalFileProvider.java
Normal file
|
@ -0,0 +1,7 @@
|
|||
package io.lbry.browser;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
public class LocalFileProvider extends FileProvider {
|
||||
|
||||
}
|
3882
app/src/main/java/io/lbry/browser/MainActivity.java
Normal file
3882
app/src/main/java/io/lbry/browser/MainActivity.java
Normal file
File diff suppressed because it is too large
Load diff
469
app/src/main/java/io/lbry/browser/VerificationActivity.java
Normal file
469
app/src/main/java/io/lbry/browser/VerificationActivity.java
Normal file
|
@ -0,0 +1,469 @@
|
|||
package io.lbry.browser;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Color;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.adapter.VerificationPagerAdapter;
|
||||
import io.lbry.browser.listener.SdkStatusListener;
|
||||
import io.lbry.browser.listener.SignInListener;
|
||||
import io.lbry.browser.listener.WalletSyncListener;
|
||||
import io.lbry.browser.model.lbryinc.RewardVerified;
|
||||
import io.lbry.browser.model.lbryinc.User;
|
||||
import io.lbry.browser.tasks.RewardVerifiedHandler;
|
||||
import io.lbry.browser.tasks.lbryinc.FetchCurrentUserTask;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.LbryAnalytics;
|
||||
import io.lbry.browser.utils.Lbryio;
|
||||
import io.lbry.lbrysdk.LbrynetService;
|
||||
|
||||
public class VerificationActivity extends FragmentActivity implements SignInListener, WalletSyncListener {
|
||||
|
||||
public static final int VERIFICATION_FLOW_SIGN_IN = 1;
|
||||
public static final int VERIFICATION_FLOW_REWARDS = 2;
|
||||
public static final int VERIFICATION_FLOW_WALLET = 3;
|
||||
|
||||
private List<SdkStatusListener> sdkStatusListeners;
|
||||
private BillingClient billingClient;
|
||||
private BroadcastReceiver sdkReceiver;
|
||||
private String email;
|
||||
private boolean signedIn;
|
||||
private int flow;
|
||||
|
||||
private final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
|
||||
@Override
|
||||
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null)
|
||||
{
|
||||
for (Purchase purchase : purchases) {
|
||||
if (MainActivity.SKU_SKIP.equalsIgnoreCase(purchase.getSku())) {
|
||||
showLoading();
|
||||
MainActivity.handleBillingPurchase(
|
||||
purchase,
|
||||
billingClient,
|
||||
VerificationActivity.this, null, new RewardVerifiedHandler() {
|
||||
@Override
|
||||
public void onSuccess(RewardVerified rewardVerified) {
|
||||
if (Lbryio.currentUser != null) {
|
||||
Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
|
||||
}
|
||||
|
||||
if (!rewardVerified.isRewardApproved()) {
|
||||
// show pending purchase message (possible slow card tx)
|
||||
Snackbar.make(findViewById(R.id.verification_pager), R.string.purchase_request_pending, Snackbar.LENGTH_LONG).show();
|
||||
} else {
|
||||
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
setResult(RESULT_OK);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception error) {
|
||||
showFetchUserError(getString(R.string.purchase_request_failed_error));
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sdkStatusListeners = new ArrayList<>();
|
||||
|
||||
signedIn = Lbryio.isSignedIn();
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
flow = intent.getIntExtra("flow", -1);
|
||||
if (flow == -1 || (flow == VERIFICATION_FLOW_SIGN_IN && signedIn)) {
|
||||
// no flow specified (or user is already signed in), just exit
|
||||
setResult(signedIn ? RESULT_OK : RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Arrays.asList(VERIFICATION_FLOW_SIGN_IN, VERIFICATION_FLOW_REWARDS, VERIFICATION_FLOW_WALLET).contains(flow)) {
|
||||
// invalid flow specified
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(LbrynetService.ACTION_STOP_SERVICE);
|
||||
filter.addAction(MainActivity.ACTION_SDK_READY);
|
||||
sdkReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (MainActivity.ACTION_SDK_READY.equals(action)) {
|
||||
for (SdkStatusListener listener : sdkStatusListeners) {
|
||||
if (listener != null) {
|
||||
listener.onSdkReady();
|
||||
}
|
||||
}
|
||||
} else if (LbrynetService.ACTION_STOP_SERVICE.equals(action)) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(sdkReceiver, filter);
|
||||
|
||||
billingClient = BillingClient.newBuilder(this)
|
||||
.setListener(purchasesUpdatedListener)
|
||||
.enablePendingPurchases()
|
||||
.build();
|
||||
establishBillingClientConnection();
|
||||
|
||||
setContentView(R.layout.activity_verification);
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
viewPager.setUserInputEnabled(false);
|
||||
viewPager.setSaveEnabled(false);
|
||||
viewPager.setAdapter(new VerificationPagerAdapter(this));
|
||||
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
findViewById(R.id.verification_close_button).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void establishBillingClientConnection() {
|
||||
if (billingClient != null) {
|
||||
billingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
// no need to do anything here. purchases are always checked server-side
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
establishBillingClientConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
LbryAnalytics.setCurrentScreen(this, "Verification", "Verification");
|
||||
checkFlow();
|
||||
}
|
||||
|
||||
public void checkFlow() {
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
if (Lbryio.isSignedIn()) {
|
||||
boolean flowHandled = false;
|
||||
if (flow == VERIFICATION_FLOW_WALLET) {
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
|
||||
flowHandled = true;
|
||||
} else if (flow == VERIFICATION_FLOW_REWARDS) {
|
||||
User user = Lbryio.currentUser;
|
||||
// disable phone verification for now
|
||||
if (!user.isIdentityVerified()) {
|
||||
// phone number verification required
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
|
||||
flowHandled = true;
|
||||
} else {
|
||||
if (!user.isRewardApproved()) {
|
||||
// manual verification required
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
|
||||
flowHandled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!flowHandled) {
|
||||
// user has already been verified and or reward approved
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPhoneVerification() {
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
|
||||
}
|
||||
|
||||
public void showLoading() {
|
||||
findViewById(R.id.verification_loading_progress).setVisibility(View.VISIBLE);
|
||||
findViewById(R.id.verification_pager).setVisibility(View.INVISIBLE);
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void hideLoading() {
|
||||
findViewById(R.id.verification_loading_progress).setVisibility(View.GONE);
|
||||
findViewById(R.id.verification_pager).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
|
||||
if (viewPager.getCurrentItem() != VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL)
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL);
|
||||
else
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
public void onEmailAdded(String email) {
|
||||
this.email = email;
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("email", email);
|
||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_ADDED, bundle);
|
||||
}
|
||||
public void onEmailEdit() {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
}
|
||||
public void onEmailVerified() {
|
||||
Snackbar.make(findViewById(R.id.verification_pager), R.string.sign_in_successful, Snackbar.LENGTH_LONG).show();
|
||||
sendBroadcast(new Intent(MainActivity.ACTION_USER_SIGN_IN_SUCCESS));
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("email", email);
|
||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_VERIFIED, bundle);
|
||||
|
||||
if (flow == VERIFICATION_FLOW_SIGN_IN) {
|
||||
final Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra("flow", VERIFICATION_FLOW_SIGN_IN);
|
||||
resultIntent.putExtra("email", email);
|
||||
|
||||
// only sign in required, don't do anything else
|
||||
showLoading();
|
||||
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
|
||||
@Override
|
||||
public void onSuccess(User user) {
|
||||
Lbryio.currentUser = user;
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception error) {
|
||||
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else {
|
||||
// change pager view depending on flow
|
||||
showLoading();
|
||||
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
|
||||
@Override
|
||||
public void onSuccess(User user) {
|
||||
hideLoading();
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
|
||||
Lbryio.currentUser = user;
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
// for rewards, (show phone verification if not done, or manual verification if required)
|
||||
if (flow == VERIFICATION_FLOW_REWARDS) {
|
||||
if (!user.isIdentityVerified()) {
|
||||
// phone number verification required
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
|
||||
} else {
|
||||
if (!user.isRewardApproved()) {
|
||||
// manual verification required
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
|
||||
} else {
|
||||
// fully verified
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
} else if (flow == VERIFICATION_FLOW_WALLET) {
|
||||
// for wallet sync, if password unlock is required, show password entry page
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onError(Exception error) {
|
||||
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneAdded(String countryCode, String phoneNumber) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneVerified() {
|
||||
showLoading();
|
||||
FetchCurrentUserTask task = new FetchCurrentUserTask(this, new FetchCurrentUserTask.FetchUserTaskHandler() {
|
||||
@Override
|
||||
public void onSuccess(User user) {
|
||||
Lbryio.currentUser = user;
|
||||
if (user.isIdentityVerified() && user.isRewardApproved()) {
|
||||
// verified for rewards
|
||||
LbryAnalytics.logEvent(LbryAnalytics.EVENT_REWARD_ELIGIBILITY_COMPLETED);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
// show manual verification page if the user is still not reward approved
|
||||
ViewPager2 viewPager = findViewById(R.id.verification_pager);
|
||||
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception error) {
|
||||
showFetchUserError(error != null ? error.getMessage() : getString(R.string.fetch_current_user_error));
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void showFetchUserError(String message) {
|
||||
Snackbar.make(findViewById(R.id.verification_pager), message, Snackbar.LENGTH_LONG).
|
||||
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onManualVerifyContinue() {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWalletSyncProcessing() {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
|
||||
}
|
||||
@Override
|
||||
public void onWalletSyncWaitingForInput() {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWalletSyncEnabled() {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWalletSyncFailed(Exception error) {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipQueueAction() {
|
||||
if (billingClient != null) {
|
||||
List<String> skuList = new ArrayList<>();
|
||||
skuList.add(MainActivity.SKU_SKIP);
|
||||
|
||||
SkuDetailsParams detailsParams = SkuDetailsParams.newBuilder().
|
||||
setType(BillingClient.SkuType.INAPP).
|
||||
setSkusList(skuList).build();
|
||||
billingClient.querySkuDetailsAsync(detailsParams, new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
|
||||
if (list != null && list.size() > 0) {
|
||||
// we only queried one product, so it should be the first item in the list
|
||||
SkuDetails skuDetails = list.get(0);
|
||||
|
||||
// launch the billing flow for skip queue
|
||||
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().
|
||||
setSkuDetails(skuDetails).build();
|
||||
billingClient.launchBillingFlow(VerificationActivity.this, billingFlowParams);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTwitterVerified() {
|
||||
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
|
||||
|
||||
setResult(RESULT_OK);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onManualProgress(boolean progress) {
|
||||
if (progress) {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
|
||||
} else {
|
||||
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Helper.unregisterReceiver(sdkReceiver, this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public void addSdkStatusListener(SdkStatusListener listener) {
|
||||
if (!sdkStatusListeners.contains(listener)) {
|
||||
sdkStatusListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSdkStatusListener(SdkStatusListener listener) {
|
||||
sdkStatusListeners.remove(listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.listener.ChannelItemSelectionListener;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ChannelFilterListAdapter extends RecyclerView.Adapter<ChannelFilterListAdapter.ViewHolder> {
|
||||
private final Context context;
|
||||
private List<Claim> items;
|
||||
@Getter
|
||||
@Setter
|
||||
private Claim selectedItem;
|
||||
@Setter
|
||||
private ChannelItemSelectionListener listener;
|
||||
|
||||
public ChannelFilterListAdapter(Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>();
|
||||
|
||||
// Always list the placeholder as the first item
|
||||
Claim claim = new Claim();
|
||||
claim.setPlaceholder(true);
|
||||
items.add(claim);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final View mediaContainer;
|
||||
protected final View alphaContainer;
|
||||
protected final View allView;
|
||||
protected final ImageView thumbnailView;
|
||||
protected final TextView alphaView;
|
||||
protected final TextView titleView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
mediaContainer = v.findViewById(R.id.channel_filter_media_container);
|
||||
alphaContainer = v.findViewById(R.id.channel_filter_no_thumbnail);
|
||||
alphaView = v.findViewById(R.id.channel_filter_alpha_view);
|
||||
thumbnailView = v.findViewById(R.id.channel_filter_thumbnail);
|
||||
titleView = v.findViewById(R.id.channel_filter_title);
|
||||
allView = v.findViewById(R.id.channel_filter_all_container);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public boolean isClaimSelected(Claim claim) {
|
||||
return claim.equals(selectedItem);
|
||||
}
|
||||
|
||||
public void clearClaims() {
|
||||
items = new ArrayList<>(items.subList(0, 1));
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addClaims(List<Claim> claims) {
|
||||
for (Claim claim : claims) {
|
||||
if (!items.contains(claim)) {
|
||||
items.add(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFilterListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_channel_filter, root, false);
|
||||
return new ChannelFilterListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ChannelFilterListAdapter.ViewHolder vh, int position) {
|
||||
Claim claim = items.get(position);
|
||||
vh.alphaView.setVisibility(claim.isPlaceholder() ? View.GONE : View.VISIBLE);
|
||||
vh.titleView.setVisibility(claim.isPlaceholder() ? View.INVISIBLE : View.VISIBLE);
|
||||
vh.allView.setVisibility(claim.isPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
|
||||
vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
|
||||
String thumbnailUrl = claim.getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85);
|
||||
if (!Helper.isNullOrEmpty(thumbnailUrl) && context != null) {
|
||||
Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
|
||||
}
|
||||
vh.alphaContainer.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
|
||||
vh.thumbnailView.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.GONE : View.VISIBLE);
|
||||
vh.alphaView.setText(claim.isPlaceholder() ? null : claim.getName() != null ? claim.getName().substring(1, 2).toUpperCase() : "");
|
||||
|
||||
int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
|
||||
Helper.setIconViewBackgroundColor(vh.alphaContainer, bgColor, claim.isPlaceholder(), context);
|
||||
|
||||
vh.itemView.setSelected(isClaimSelected(claim));
|
||||
vh.itemView.setOnClickListener(view -> {
|
||||
if (claim.isPlaceholder()) {
|
||||
selectedItem = null;
|
||||
if (listener != null) {
|
||||
listener.onChannelSelectionCleared();
|
||||
}
|
||||
} else if (!claim.equals(selectedItem)) {
|
||||
selectedItem = claim;
|
||||
if (listener != null) {
|
||||
listener.onChannelItemSelected(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
}
|
||||
}
|
525
app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
Normal file
525
app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
Normal file
|
@ -0,0 +1,525 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.listener.SelectionModeListener;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.model.LbryFile;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.LbryUri;
|
||||
import io.lbry.browser.utils.Lbryio;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.ViewHolder> {
|
||||
private static final int VIEW_TYPE_STREAM = 1;
|
||||
private static final int VIEW_TYPE_CHANNEL = 2;
|
||||
private static final int VIEW_TYPE_FEATURED = 3; // featured search result
|
||||
|
||||
private final Map<String, Claim> quickClaimIdMap;
|
||||
private final Map<String, Claim> quickClaimUrlMap;
|
||||
private final Map<String, Boolean> notFoundClaimIdMap;
|
||||
private final Map<String, Boolean> notFoundClaimUrlMap;
|
||||
|
||||
@Setter
|
||||
private boolean hideFee;
|
||||
@Setter
|
||||
private boolean canEnterSelectionMode;
|
||||
private final Context context;
|
||||
private List<Claim> items;
|
||||
private final List<Claim> selectedItems;
|
||||
@Setter
|
||||
private ClaimListItemListener listener;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean inSelectionMode;
|
||||
@Setter
|
||||
private SelectionModeListener selectionModeListener;
|
||||
private float scale;
|
||||
|
||||
public ClaimListAdapter(List<Claim> items, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>();
|
||||
for (Claim item : items) {
|
||||
if (item != null) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedItems = new ArrayList<>();
|
||||
quickClaimIdMap = new HashMap<>();
|
||||
quickClaimUrlMap = new HashMap<>();
|
||||
notFoundClaimIdMap = new HashMap<>();
|
||||
notFoundClaimUrlMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
scale = context.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Claim> getSelectedItems() {
|
||||
return this.selectedItems;
|
||||
}
|
||||
public int getSelectedCount() {
|
||||
return selectedItems != null ? selectedItems.size() : 0;
|
||||
}
|
||||
public void clearSelectedItems() {
|
||||
this.selectedItems.clear();
|
||||
}
|
||||
public boolean isClaimSelected(Claim claim) {
|
||||
return selectedItems.contains(claim);
|
||||
}
|
||||
|
||||
public Claim getFeaturedItem() {
|
||||
for (Claim claim : items) {
|
||||
if (claim.isFeatured()) {
|
||||
return claim;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeFeaturedItem() {
|
||||
int featuredIndex = -1;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i).isFeatured()) {
|
||||
featuredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (featuredIndex > -1) {
|
||||
items.remove(featuredIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Claim> getItems() {
|
||||
return new ArrayList<>(this.items);
|
||||
}
|
||||
|
||||
public void updateSigningChannelForClaim(Claim resolvedClaim) {
|
||||
for (Claim claim : items) {
|
||||
if (claim.getClaimId().equalsIgnoreCase(resolvedClaim.getClaimId())) {
|
||||
claim.setSigningChannel(resolvedClaim.getSigningChannel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearItems() {
|
||||
clearSelectedItems();
|
||||
this.items.clear();
|
||||
quickClaimIdMap.clear();
|
||||
quickClaimUrlMap.clear();
|
||||
notFoundClaimIdMap.clear();
|
||||
notFoundClaimUrlMap.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Claim getLastItem() {
|
||||
return items.size() > 0 ? items.get(items.size() - 1) : null;
|
||||
}
|
||||
|
||||
public void addFeaturedItem(Claim claim) {
|
||||
items.add(0, claim);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Claim> claims) {
|
||||
for (Claim claim : claims) {
|
||||
if (claim != null && !items.contains(claim)) {
|
||||
items.add(claim);
|
||||
}
|
||||
}
|
||||
|
||||
notFoundClaimUrlMap.clear();
|
||||
notFoundClaimIdMap.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public void setItems(List<Claim> claims) {
|
||||
items = new ArrayList<>();
|
||||
for (Claim claim : claims) {
|
||||
if (claim != null) {
|
||||
items.add(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void removeItems(List<Claim> claims) {
|
||||
items.removeAll(claims);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void removeItem(Claim claim) {
|
||||
items.remove(claim);
|
||||
selectedItems.remove(claim);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final View feeContainer;
|
||||
protected final TextView feeView;
|
||||
protected final ImageView thumbnailView;
|
||||
protected final View noThumbnailView;
|
||||
protected final TextView alphaView;
|
||||
protected final TextView vanityUrlView;
|
||||
protected final TextView durationView;
|
||||
protected final TextView titleView;
|
||||
protected final TextView publisherView;
|
||||
protected final TextView publishTimeView;
|
||||
protected final TextView pendingTextView;
|
||||
protected final View repostInfoView;
|
||||
protected final TextView repostChannelView;
|
||||
protected final View selectedOverlayView;
|
||||
protected final TextView fileSizeView;
|
||||
protected final ProgressBar downloadProgressView;
|
||||
protected final TextView deviceView;
|
||||
|
||||
protected final View loadingImagePlaceholder;
|
||||
protected final View loadingTextPlaceholder1;
|
||||
protected final View loadingTextPlaceholder2;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
feeContainer = v.findViewById(R.id.claim_fee_container);
|
||||
feeView = v.findViewById(R.id.claim_fee);
|
||||
alphaView = v.findViewById(R.id.claim_thumbnail_alpha);
|
||||
noThumbnailView = v.findViewById(R.id.claim_no_thumbnail);
|
||||
thumbnailView = v.findViewById(R.id.claim_thumbnail);
|
||||
vanityUrlView = v.findViewById(R.id.claim_vanity_url);
|
||||
durationView = v.findViewById(R.id.claim_duration);
|
||||
titleView = v.findViewById(R.id.claim_title);
|
||||
publisherView = v.findViewById(R.id.claim_publisher);
|
||||
publishTimeView = v.findViewById(R.id.claim_publish_time);
|
||||
pendingTextView = v.findViewById(R.id.claim_pending_text);
|
||||
repostInfoView = v.findViewById(R.id.claim_repost_info);
|
||||
repostChannelView = v.findViewById(R.id.claim_repost_channel);
|
||||
selectedOverlayView = v.findViewById(R.id.claim_selected_overlay);
|
||||
fileSizeView = v.findViewById(R.id.claim_file_size);
|
||||
downloadProgressView = v.findViewById(R.id.claim_download_progress);
|
||||
deviceView = v.findViewById(R.id.claim_view_device);
|
||||
|
||||
loadingImagePlaceholder = v.findViewById(R.id.claim_thumbnail_placeholder);
|
||||
loadingTextPlaceholder1 = v.findViewById(R.id.claim_text_loading_placeholder_1);
|
||||
loadingTextPlaceholder2 = v.findViewById(R.id.claim_text_loading_placeholder_2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (items.get(position).isFeatured()) {
|
||||
return VIEW_TYPE_FEATURED;
|
||||
}
|
||||
|
||||
Claim claim = items.get(position);
|
||||
String valueType = items.get(position).getValueType();
|
||||
Claim actualClaim = Claim.TYPE_REPOST.equalsIgnoreCase(valueType) ? claim.getRepostedClaim() : claim;
|
||||
|
||||
return Claim.TYPE_CHANNEL.equalsIgnoreCase(actualClaim.getValueType()) ? VIEW_TYPE_CHANNEL : VIEW_TYPE_STREAM;
|
||||
}
|
||||
|
||||
public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url) {
|
||||
updateFileForClaimByIdOrUrl(file, claimId, url, false);
|
||||
}
|
||||
public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url, boolean skipNotFound) {
|
||||
if (!skipNotFound) {
|
||||
if (notFoundClaimIdMap.containsKey(claimId) && notFoundClaimUrlMap.containsKey(url)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (quickClaimIdMap.containsKey(claimId)) {
|
||||
quickClaimIdMap.get(claimId).setFile(file);
|
||||
notifyDataSetChanged();
|
||||
return;
|
||||
}
|
||||
if (quickClaimUrlMap.containsKey(claimId)) {
|
||||
quickClaimUrlMap.get(claimId).setFile(file);
|
||||
notifyDataSetChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean claimFound = false;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Claim claim = items.get(i);
|
||||
if (claimId.equalsIgnoreCase(claim.getClaimId()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
|
||||
quickClaimIdMap.put(claimId, claim);
|
||||
quickClaimUrlMap.put(url, claim);
|
||||
claim.setFile(file);
|
||||
notifyDataSetChanged();
|
||||
claimFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!claimFound) {
|
||||
notFoundClaimIdMap.put(claimId, true);
|
||||
notFoundClaimUrlMap.put(url, true);
|
||||
}
|
||||
}
|
||||
public void clearFileForClaimOrUrl(String outpoint, String url) {
|
||||
clearFileForClaimOrUrl(outpoint, url, false);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
public void clearFileForClaimOrUrl(String outpoint, String url, boolean remove) {
|
||||
int claimIndex = -1;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Claim claim = items.get(i);
|
||||
if (outpoint.equalsIgnoreCase(claim.getOutpoint()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
|
||||
claimIndex = i;
|
||||
claim.setFile(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remove && claimIndex > -1) {
|
||||
Claim removed = items.remove(claimIndex);
|
||||
selectedItems.remove(removed);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClaimListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
int viewResourceId = -1;
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_FEATURED: viewResourceId = R.layout.list_item_featured_search_result; break;
|
||||
case VIEW_TYPE_CHANNEL: viewResourceId = R.layout.list_item_channel; break;
|
||||
case VIEW_TYPE_STREAM: default: viewResourceId = R.layout.list_item_stream; break;
|
||||
}
|
||||
|
||||
View v = LayoutInflater.from(context).inflate(viewResourceId, parent, false);
|
||||
return new ClaimListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ClaimListAdapter.ViewHolder vh, int position) {
|
||||
int type = getItemViewType(position);
|
||||
int paddingTop = position == 0 ? 16 : 8;
|
||||
int paddingBottom = position == getItemCount() - 1 ? 16 : 8;
|
||||
int paddingTopScaled = Helper.getScaledValue(paddingTop, scale);
|
||||
int paddingBottomScaled = Helper.getScaledValue(paddingBottom, scale);
|
||||
vh.itemView.setPadding(vh.itemView.getPaddingStart(), paddingTopScaled, vh.itemView.getPaddingEnd(), paddingBottomScaled);
|
||||
|
||||
Claim original = items.get(position);
|
||||
boolean isRepost = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType());
|
||||
final Claim item = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType()) ?
|
||||
(original.getRepostedClaim() != null ? original.getRepostedClaim() : original): original;
|
||||
Claim.GenericMetadata metadata = item.getValue();
|
||||
Claim signingChannel = item.getSigningChannel();
|
||||
Claim.StreamMetadata streamMetadata = null;
|
||||
if (metadata instanceof Claim.StreamMetadata) {
|
||||
streamMetadata = (Claim.StreamMetadata) metadata;
|
||||
}
|
||||
|
||||
String thumbnailUrl = item.getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85);
|
||||
long publishTime = (streamMetadata != null && streamMetadata.getReleaseTime() > 0) ? streamMetadata.getReleaseTime() * 1000 : item.getTimestamp() * 1000;
|
||||
int bgColor = Helper.generateRandomColorForValue(item.getClaimId());
|
||||
if (bgColor == 0) {
|
||||
bgColor = Helper.generateRandomColorForValue(item.getName());
|
||||
}
|
||||
|
||||
boolean isPending = item.getConfirmations() == 0;
|
||||
boolean isSelected = isClaimSelected(original);
|
||||
vh.itemView.setSelected(isSelected);
|
||||
vh.selectedOverlayView.setVisibility(isSelected ? View.VISIBLE : View.GONE);
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (isPending) {
|
||||
Snackbar snackbar = Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG);
|
||||
TextView snackbarText = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
|
||||
snackbarText.setMaxLines(5);
|
||||
snackbar.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inSelectionMode) {
|
||||
toggleSelectedClaim(original);
|
||||
} else {
|
||||
if (listener != null) {
|
||||
listener.onClaimClicked(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (!canEnterSelectionMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
Snackbar snackbar = Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG);
|
||||
TextView snackbarText = snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text);
|
||||
snackbarText.setMaxLines(5);
|
||||
snackbar.show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!inSelectionMode) {
|
||||
inSelectionMode = true;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onEnterSelectionMode();
|
||||
}
|
||||
}
|
||||
toggleSelectedClaim(original);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
vh.publisherView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null && signingChannel != null) {
|
||||
listener.onClaimClicked(signingChannel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.publishTimeView.setVisibility(!isPending ? View.VISIBLE : View.GONE);
|
||||
vh.pendingTextView.setVisibility(isPending && !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
vh.repostInfoView.setVisibility(isRepost && type != VIEW_TYPE_FEATURED ? View.VISIBLE : View.GONE);
|
||||
vh.repostChannelView.setText(isRepost && original.getSigningChannel() != null ? original.getSigningChannel().getName() : null);
|
||||
vh.repostChannelView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
listener.onClaimClicked(original.getSigningChannel());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.titleView.setText(Helper.isNullOrEmpty(item.getTitle()) ? item.getName() : item.getTitle());
|
||||
if (type == VIEW_TYPE_FEATURED) {
|
||||
LbryUri vanityUrl = new LbryUri();
|
||||
vanityUrl.setClaimName(item.getName());
|
||||
vh.vanityUrlView.setText(vanityUrl.toString());
|
||||
}
|
||||
|
||||
vh.feeContainer.setVisibility(item.isUnresolved() || !Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType()) ? View.GONE : View.VISIBLE);
|
||||
vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
|
||||
Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
|
||||
|
||||
Helper.setViewVisibility(vh.loadingImagePlaceholder, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
Helper.setViewVisibility(vh.loadingTextPlaceholder1, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
Helper.setViewVisibility(vh.loadingTextPlaceholder2, item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
Helper.setViewVisibility(vh.titleView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
Helper.setViewVisibility(vh.publisherView, !item.isLoadingPlaceholder() ? View.VISIBLE : View.GONE);
|
||||
Helper.setViewVisibility(vh.publishTimeView, !item.isLoadingPlaceholder() && !isPending ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (type == VIEW_TYPE_FEATURED && item.isUnresolved()) {
|
||||
vh.durationView.setVisibility(View.GONE);
|
||||
vh.titleView.setText("Nothing here. Publish something!");
|
||||
vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
|
||||
} else {
|
||||
if (Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType())) {
|
||||
long duration = item.getDuration();
|
||||
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
|
||||
Glide.with(context.getApplicationContext()).
|
||||
asBitmap().
|
||||
load(thumbnailUrl).
|
||||
centerCrop().
|
||||
placeholder(R.drawable.bg_thumbnail_placeholder).
|
||||
into(vh.thumbnailView);
|
||||
vh.thumbnailView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
vh.thumbnailView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
BigDecimal cost = item.getActualCost(Lbryio.LBCUSDRate);
|
||||
vh.feeContainer.setVisibility(cost.doubleValue() > 0 && !hideFee ? View.VISIBLE : View.GONE);
|
||||
vh.feeView.setText(cost.doubleValue() > 0 ? Helper.shortCurrencyFormat(cost.doubleValue()) : "Paid");
|
||||
vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
|
||||
vh.publisherView.setText(signingChannel != null ? signingChannel.getName() : context.getString(R.string.anonymous));
|
||||
vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
|
||||
publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
|
||||
vh.durationView.setVisibility(duration > 0 ? View.VISIBLE : View.GONE);
|
||||
vh.durationView.setText(Helper.formatDuration(duration));
|
||||
|
||||
LbryFile claimFile = item.getFile();
|
||||
boolean isDownloading = false;
|
||||
int progress = 0;
|
||||
String fileSizeString = claimFile == null ? null : Helper.formatBytes(claimFile.getTotalBytes(), false);
|
||||
if (claimFile != null &&
|
||||
!Helper.isNullOrEmpty(claimFile.getDownloadPath()) &&
|
||||
!claimFile.isCompleted() &&
|
||||
claimFile.getWrittenBytes() < claimFile.getTotalBytes()) {
|
||||
isDownloading = true;
|
||||
progress = claimFile.getTotalBytes() > 0 ?
|
||||
Double.valueOf(((double) claimFile.getWrittenBytes() / (double) claimFile.getTotalBytes()) * 100.0).intValue() : 0;
|
||||
fileSizeString = String.format("%s / %s",
|
||||
Helper.formatBytes(claimFile.getWrittenBytes(), false),
|
||||
Helper.formatBytes(claimFile.getTotalBytes(), false));
|
||||
}
|
||||
|
||||
Helper.setViewText(vh.fileSizeView, claimFile != null && !Helper.isNullOrEmpty(claimFile.getDownloadPath()) ? fileSizeString : null);
|
||||
Helper.setViewVisibility(vh.downloadProgressView, isDownloading ? View.VISIBLE : View.INVISIBLE);
|
||||
Helper.setViewProgress(vh.downloadProgressView, progress);
|
||||
Helper.setViewText(vh.deviceView, item.getDevice());
|
||||
} else if (Claim.TYPE_CHANNEL.equalsIgnoreCase(item.getValueType())) {
|
||||
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
|
||||
Glide.with(context.getApplicationContext()).
|
||||
load(thumbnailUrl).
|
||||
centerCrop().
|
||||
placeholder(R.drawable.bg_thumbnail_placeholder).
|
||||
apply(RequestOptions.circleCropTransform()).
|
||||
into(vh.thumbnailView);
|
||||
}
|
||||
vh.alphaView.setText(item.getName().substring(1, 2).toUpperCase());
|
||||
vh.publisherView.setText(item.getName());
|
||||
vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
|
||||
publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleSelectedClaim(Claim claim) {
|
||||
if (selectedItems.contains(claim)) {
|
||||
selectedItems.remove(claim);
|
||||
} else {
|
||||
selectedItems.add(claim);
|
||||
}
|
||||
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onItemSelectionToggled();
|
||||
}
|
||||
|
||||
if (selectedItems.size() == 0) {
|
||||
inSelectionMode = false;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onExitSelectionMode();
|
||||
}
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public interface ClaimListItemListener {
|
||||
void onClaimClicked(Claim claim);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.model.ClaimCacheKey;
|
||||
import io.lbry.browser.model.Comment;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.Lbry;
|
||||
import io.lbry.browser.utils.LbryUri;
|
||||
import lombok.Setter;
|
||||
|
||||
public class CommentListAdapter extends RecyclerView.Adapter<CommentListAdapter.ViewHolder> {
|
||||
private final List<Comment> items;
|
||||
private final Context context;
|
||||
private final boolean nested;
|
||||
private float scale;
|
||||
@Setter
|
||||
private ClaimListAdapter.ClaimListItemListener listener;
|
||||
@Setter
|
||||
private ReplyClickListener replyListener;
|
||||
|
||||
public CommentListAdapter(List<Comment> items, Context context) {
|
||||
this(items, context, false);
|
||||
}
|
||||
|
||||
public CommentListAdapter(List<Comment> items, Context context, boolean nested) {
|
||||
this.items = new ArrayList<>(items);
|
||||
this.context = context;
|
||||
this.nested = nested;
|
||||
if (context != null) {
|
||||
scale = context.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
for (Comment item : this.items) {
|
||||
ClaimCacheKey key = new ClaimCacheKey();
|
||||
key.setClaimId(item.getChannelId());
|
||||
if (Lbry.claimCache.containsKey(key)) {
|
||||
item.setPoster(Lbry.claimCache.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearItems() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
public int getPositionForComment(String commentHash) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (commentHash.equalsIgnoreCase(items.get(i).getId())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public List<String> getClaimUrlsToResolve() {
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Comment item = items.get(i);
|
||||
if (item.getPoster() == null) {
|
||||
LbryUri url = LbryUri.tryParse(String.format("%s#%s", item.getChannelName(), item.getChannelId()));
|
||||
if (url != null && !urls.contains(url.toString())) {
|
||||
urls.add(url.toString());
|
||||
}
|
||||
}
|
||||
if (item.getReplies().size() > 0) {
|
||||
for (int j = 0; j < item.getReplies().size(); j++) {
|
||||
Comment reply = item.getReplies().get(j);
|
||||
if (reply.getPoster() == null) {
|
||||
LbryUri url = LbryUri.tryParse(String.format("%s#%s", reply.getChannelName(), reply.getChannelId()));
|
||||
if (url != null && !urls.contains(url.toString())) {
|
||||
urls.add(url.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final TextView channelName;
|
||||
protected final TextView commentText;
|
||||
protected final ImageView thumbnailView;
|
||||
protected final View noThumbnailView;
|
||||
protected final TextView alphaView;
|
||||
protected final TextView commentTimeView;
|
||||
protected final View replyLink;
|
||||
protected final RecyclerView repliesList;
|
||||
|
||||
public ViewHolder (View v) {
|
||||
super(v);
|
||||
channelName = v.findViewById(R.id.comment_channel_name);
|
||||
commentTimeView = v.findViewById(R.id.comment_time);
|
||||
commentText = v.findViewById(R.id.comment_text);
|
||||
replyLink = v.findViewById(R.id.comment_reply_link);
|
||||
thumbnailView = v.findViewById(R.id.comment_thumbnail);
|
||||
noThumbnailView = v.findViewById(R.id.comment_no_thumbnail);
|
||||
alphaView = v.findViewById(R.id.comment_thumbnail_alpha);
|
||||
repliesList = v.findViewById(R.id.comment_replies);
|
||||
}
|
||||
}
|
||||
|
||||
public void insert(int index, Comment comment) {
|
||||
if (!items.contains(comment)) {
|
||||
items.add(index, comment);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void addReply(Comment comment) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Comment parent = items.get(i);
|
||||
if (parent.getId().equalsIgnoreCase(comment.getParentId())) {
|
||||
parent.addReply(comment);
|
||||
notifyDataSetChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePosterForComment(String channelId, Claim channel) {
|
||||
for (int i = 0 ; i < items.size(); i++) {
|
||||
Comment item = items.get(i);
|
||||
List<Comment> replies = item.getReplies();
|
||||
if (replies != null && replies.size() > 0) {
|
||||
for (int j = 0; j < replies.size(); j++) {
|
||||
Comment reply = item.getReplies().get(j);
|
||||
if (channelId.equalsIgnoreCase(reply.getChannelId())) {
|
||||
reply.setPoster(channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId.equalsIgnoreCase(item.getChannelId())) {
|
||||
item.setPoster(channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_comment, parent, false);
|
||||
return new CommentListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
Comment comment = items.get(position);
|
||||
holder.itemView.setPadding(
|
||||
nested ? Helper.getScaledValue(56, scale) : holder.itemView.getPaddingStart(),
|
||||
holder.itemView.getPaddingTop(),
|
||||
nested ? 0 : holder.itemView.getPaddingEnd(),
|
||||
holder.itemView.getPaddingBottom());
|
||||
|
||||
holder.channelName.setText(comment.getChannelName());
|
||||
holder.commentTimeView.setText(DateUtils.getRelativeTimeSpanString(
|
||||
(comment.getTimestamp() * 1000), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
|
||||
holder.commentText.setText(comment.getText());
|
||||
holder.replyLink.setVisibility(!nested ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean hasThumbnail = comment.getPoster() != null && !Helper.isNullOrEmpty(comment.getPoster().getThumbnailUrl());
|
||||
holder.thumbnailView.setVisibility(hasThumbnail ? View.VISIBLE : View.INVISIBLE);
|
||||
holder.noThumbnailView.setVisibility(!hasThumbnail ? View.VISIBLE : View.INVISIBLE);
|
||||
int bgColor = Helper.generateRandomColorForValue(comment.getChannelId());
|
||||
Helper.setIconViewBackgroundColor(holder.noThumbnailView, bgColor, false, context);
|
||||
if (hasThumbnail) {
|
||||
Glide.with(context.getApplicationContext()).asBitmap().load(comment.getPoster().getThumbnailUrl(holder.thumbnailView.getLayoutParams().width, holder.thumbnailView.getLayoutParams().height, 85)).
|
||||
apply(RequestOptions.circleCropTransform()).into(holder.thumbnailView);
|
||||
}
|
||||
holder.alphaView.setText(comment.getChannelName() != null ? comment.getChannelName().substring(1, 2).toUpperCase() : null);
|
||||
List<Comment> replies = comment.getReplies();
|
||||
boolean hasReplies = replies != null && replies.size() > 0;
|
||||
if (hasReplies) {
|
||||
holder.repliesList.setLayoutManager(new LinearLayoutManager(context));
|
||||
holder.repliesList.setAdapter(new CommentListAdapter(replies, context, true));
|
||||
} else {
|
||||
holder.repliesList.setAdapter(null);
|
||||
}
|
||||
|
||||
holder.channelName.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null && comment.getPoster() != null) {
|
||||
listener.onClaimClicked(comment.getPoster());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
holder.replyLink.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (replyListener != null) {
|
||||
replyListener.onReplyClicked(comment);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface ReplyClickListener {
|
||||
void onReplyClicked(Comment comment);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.EditorsChoiceItem;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Setter;
|
||||
|
||||
public class EditorsChoiceItemAdapter extends RecyclerView.Adapter<EditorsChoiceItemAdapter.ViewHolder> {
|
||||
private static final int VIEW_TYPE_HEADER = 1;
|
||||
private static final int VIEW_TYPE_CONTENT = 2;
|
||||
|
||||
private final Context context;
|
||||
private final List<EditorsChoiceItem> items;
|
||||
@Setter
|
||||
private EditorsChoiceItemListener listener;
|
||||
|
||||
public EditorsChoiceItemAdapter(List<EditorsChoiceItem> items, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(items);
|
||||
}
|
||||
|
||||
public void addFeaturedItem(EditorsChoiceItem item) {
|
||||
items.add(0, item);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<EditorsChoiceItem> items) {
|
||||
for (EditorsChoiceItem item : items) {
|
||||
if (!this.items.contains(item)) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final ImageView thumbnailView;
|
||||
protected final TextView descriptionView;
|
||||
protected final TextView headerView;
|
||||
protected final TextView titleView;
|
||||
protected final View cardView;
|
||||
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
cardView = v.findViewById(R.id.editors_choice_content_card);
|
||||
descriptionView = v.findViewById(R.id.editors_choice_content_description);
|
||||
titleView = v.findViewById(R.id.editors_choice_content_title);
|
||||
|
||||
thumbnailView = v.findViewById(R.id.editors_choice_content_thumbnail);
|
||||
headerView = v.findViewById(R.id.editors_choice_header_title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return items.get(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_CONTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditorsChoiceItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_editors_choice, parent, false);
|
||||
return new EditorsChoiceItemAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(EditorsChoiceItemAdapter.ViewHolder vh, int position) {
|
||||
int type = getItemViewType(position);
|
||||
EditorsChoiceItem item = items.get(position);
|
||||
|
||||
vh.headerView.setVisibility(type == VIEW_TYPE_HEADER ? View.VISIBLE : View.GONE);
|
||||
vh.cardView.setVisibility(type == VIEW_TYPE_CONTENT ? View.VISIBLE : View.GONE);
|
||||
|
||||
vh.headerView.setText(item.getTitle());
|
||||
vh.titleView.setText(item.getTitle());
|
||||
vh.descriptionView.setText(item.getDescription());
|
||||
if (!Helper.isNullOrEmpty(item.getThumbnailUrl())) {
|
||||
Glide.with(context.getApplicationContext()).
|
||||
asBitmap().
|
||||
load(item.getThumbnailUrl()).
|
||||
centerCrop().
|
||||
placeholder(R.drawable.bg_thumbnail_placeholder).
|
||||
into(vh.thumbnailView);
|
||||
}
|
||||
|
||||
vh.cardView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
listener.onEditorsChoiceItemClicked(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface EditorsChoiceItemListener {
|
||||
void onEditorsChoiceItemClicked(EditorsChoiceItem item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.GalleryItem;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Setter;
|
||||
|
||||
public class GalleryGridAdapter extends RecyclerView.Adapter<GalleryGridAdapter.ViewHolder> {
|
||||
private final Context context;
|
||||
private final List<GalleryItem> items;
|
||||
@Setter
|
||||
private GalleryItemClickListener listener;
|
||||
|
||||
public GalleryGridAdapter(List<GalleryItem> items, Context context) {
|
||||
this.items = new ArrayList<>(items);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final ImageView thumbnailView;
|
||||
protected final TextView durationView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
thumbnailView = v.findViewById(R.id.gallery_item_thumbnail);
|
||||
durationView = v.findViewById(R.id.gallery_item_duration);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public void addItem(GalleryItem item) {
|
||||
if (!items.contains(item)) {
|
||||
items.add(item);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void addItems(List<GalleryItem> items) {
|
||||
for (GalleryItem item : items) {
|
||||
if (!this.items.contains(item)) {
|
||||
this.items.add(item);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearItems() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GalleryGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_gallery, root, false);
|
||||
return new GalleryGridAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(GalleryGridAdapter.ViewHolder vh, int position) {
|
||||
GalleryItem item = items.get(position);
|
||||
String thumbnailUrl = item.getThumbnailPath();
|
||||
Glide.with(context.getApplicationContext()).load(thumbnailUrl).centerCrop().into(vh.thumbnailView);
|
||||
vh.durationView.setVisibility(item.getDuration() > 0 ? View.VISIBLE : View.INVISIBLE);
|
||||
vh.durationView.setText(item.getDuration() > 0 ? Helper.formatDuration(Double.valueOf(item.getDuration() / 1000.0).longValue()) : null);
|
||||
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
listener.onGalleryItemClicked(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface GalleryItemClickListener {
|
||||
void onGalleryItemClicked(GalleryItem item);
|
||||
}
|
||||
|
||||
public static class GalleryGridItemDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
private final int spanCount;
|
||||
private final int spacing;
|
||||
|
||||
public GalleryGridItemDecoration(int spanCount, int spacing) {
|
||||
this.spanCount = spanCount;
|
||||
this.spacing = spacing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
int position = parent.getChildAdapterPosition(view); // item position
|
||||
int column = position % spanCount; // item column
|
||||
|
||||
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
|
||||
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
|
||||
if (position >= spanCount) {
|
||||
outRect.top = spacing; // item top
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Claim;
|
||||
|
||||
public class InlineChannelSpinnerAdapter extends ArrayAdapter<Claim> {
|
||||
|
||||
private final List<Claim> channels;
|
||||
private final int layoutResourceId;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public InlineChannelSpinnerAdapter(Context context, int resource, List<Claim> channels) {
|
||||
super(context, resource, 0, channels);
|
||||
inflater = LayoutInflater.from(context);
|
||||
layoutResourceId = resource;
|
||||
this.channels = new ArrayList<>(channels);
|
||||
}
|
||||
public void addPlaceholder(boolean includeAnonymous) {
|
||||
Claim placeholder = new Claim();
|
||||
placeholder.setPlaceholder(true);
|
||||
insert(placeholder, 0);
|
||||
channels.add(0, placeholder);
|
||||
|
||||
if (includeAnonymous) {
|
||||
Claim anonymous = new Claim();
|
||||
anonymous.setPlaceholderAnonymous(true);
|
||||
insert(anonymous, 1);
|
||||
channels.add(1, anonymous);
|
||||
}
|
||||
}
|
||||
public void addAnonymousPlaceholder() {
|
||||
Claim anonymous = new Claim();
|
||||
anonymous.setPlaceholderAnonymous(true);
|
||||
insert(anonymous, 0);
|
||||
channels.add(0, anonymous);
|
||||
}
|
||||
|
||||
public void addAll(Collection<? extends Claim> collection) {
|
||||
for (Claim claim : collection) {
|
||||
if (!channels.contains(claim)) {
|
||||
channels.add(claim);
|
||||
}
|
||||
}
|
||||
super.addAll(collection);
|
||||
}
|
||||
public void clear() {
|
||||
channels.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
public int getItemPosition(Claim item) {
|
||||
for (int i = 0; i < channels.size(); i++) {
|
||||
Claim channel = channels.get(i);
|
||||
if (item.getClaimId() != null && item.getClaimId().equalsIgnoreCase(channel.getClaimId())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View view, ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
private View createView(int position, View convertView, ViewGroup parent){
|
||||
View view = inflater.inflate(layoutResourceId, parent, false);
|
||||
|
||||
Context context = getContext();
|
||||
Claim channel = getItem(position);
|
||||
String name = channel.getName();
|
||||
if (channel.isPlaceholder()) {
|
||||
name = context.getString(R.string.create_a_channel);
|
||||
} else if (channel.isPlaceholderAnonymous()) {
|
||||
name = context.getString(R.string.anonymous);
|
||||
}
|
||||
|
||||
TextView label = view.findViewById(R.id.channel_item_name);
|
||||
label.setText(name);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.lbryinc.Invitee;
|
||||
|
||||
public class InviteeListAdapter extends RecyclerView.Adapter<InviteeListAdapter.ViewHolder> {
|
||||
|
||||
private final Context context;
|
||||
private final List<Invitee> items;
|
||||
|
||||
public InviteeListAdapter(List<Invitee> invitees, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(invitees);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<Invitee> getItems() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
public void addHeader() {
|
||||
Invitee header = new Invitee();
|
||||
header.setHeader(true);
|
||||
items.add(0, header);
|
||||
}
|
||||
|
||||
public void addInvitees(List<Invitee> Invitees) {
|
||||
for (Invitee tx : Invitees) {
|
||||
if (!items.contains(tx)) {
|
||||
items.add(tx);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_invitee, root, false);
|
||||
return new InviteeListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(InviteeListAdapter.ViewHolder vh, int position) {
|
||||
Invitee item = items.get(position);
|
||||
vh.emailView.setText(item.isHeader() ? context.getString(R.string.email) : item.getEmail());
|
||||
vh.emailView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
|
||||
|
||||
String rewardText = context.getString(
|
||||
item.isInviteRewardClaimed() ? R.string.claimed :
|
||||
(item.isInviteRewardClaimable() ? R.string.claimable : R.string.unclaimable));
|
||||
vh.rewardView.setText(item.isHeader() ? context.getString(R.string.reward) : rewardText);
|
||||
vh.rewardView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final TextView emailView;
|
||||
protected final TextView rewardView;
|
||||
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
emailView = v.findViewById(R.id.invitee_email);
|
||||
rewardView = v.findViewById(R.id.invitee_reward);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Language;
|
||||
import io.lbry.browser.utils.Predefined;
|
||||
|
||||
public class LanguageSpinnerAdapter extends ArrayAdapter<Language> {
|
||||
private final int layoutResourceId;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public LanguageSpinnerAdapter(Context context, int resource) {
|
||||
super(context, resource, 0, Predefined.PUBLISH_LANGUAGES);
|
||||
inflater = LayoutInflater.from(context);
|
||||
layoutResourceId = resource;
|
||||
}
|
||||
|
||||
public int getItemPosition(String languageCode) {
|
||||
for (int i = 0; i < Predefined.PUBLISH_LANGUAGES.size(); i++) {
|
||||
Language lang = Predefined.PUBLISH_LANGUAGES.get(i);
|
||||
if (lang.getCode().equalsIgnoreCase(languageCode)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
@Override
|
||||
public View getView(int position, View view, @NonNull ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
private View createView(int position, View convertView, ViewGroup parent) {
|
||||
Language item = getItem(position);
|
||||
View view = inflater.inflate(layoutResourceId, parent, false);
|
||||
TextView label = view.findViewById(R.id.item_display_name);
|
||||
label.setText(item != null ? item.getStringResourceId() : 0);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.License;
|
||||
import io.lbry.browser.utils.Predefined;
|
||||
|
||||
public class LicenseSpinnerAdapter extends ArrayAdapter<License> {
|
||||
private final int layoutResourceId;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public LicenseSpinnerAdapter(Context context, int resource) {
|
||||
super(context, resource, 0, Predefined.LICENSES);
|
||||
inflater = LayoutInflater.from(context);
|
||||
layoutResourceId = resource;
|
||||
}
|
||||
public int getItemPosition(String name) {
|
||||
for (int i = 0; i < Predefined.LICENSES.size(); i++) {
|
||||
License lic = Predefined.LICENSES.get(i);
|
||||
if (lic.getName().equalsIgnoreCase(name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
@Override
|
||||
public View getView(int position, View view, @NonNull ViewGroup parent) {
|
||||
return createView(position, view, parent);
|
||||
}
|
||||
private View createView(int position, View convertView, ViewGroup parent) {
|
||||
License item = getItem(position);
|
||||
View view = inflater.inflate(layoutResourceId, parent, false);
|
||||
TextView label = view.findViewById(R.id.item_display_name);
|
||||
label.setText(item != null ? item.getStringResourceId() : 0);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.NavMenuItem;
|
||||
import io.lbry.browser.ui.controls.SolidIconView;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NavigationMenuAdapter extends RecyclerView.Adapter<NavigationMenuAdapter.ViewHolder> {
|
||||
private static final int TYPE_GROUP = 1;
|
||||
private static final int TYPE_ITEM = 2;
|
||||
|
||||
private final Context context;
|
||||
private final List<NavMenuItem> menuItems;
|
||||
private NavMenuItem currentItem;
|
||||
@Setter
|
||||
private NavigationMenuItemClickListener listener;
|
||||
|
||||
public NavigationMenuAdapter(List<NavMenuItem> menuItems, Context context) {
|
||||
this.menuItems = new ArrayList<>(menuItems);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setCurrentItem(int id) {
|
||||
for (NavMenuItem item : menuItems) {
|
||||
if (item.getId() == id) {
|
||||
this.currentItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setExtraLabelForItem(int id, String extraLabel) {
|
||||
for (NavMenuItem item : menuItems) {
|
||||
if (item.getId() == id) {
|
||||
item.setExtraLabel(extraLabel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setCurrentItem(NavMenuItem currentItem) {
|
||||
this.currentItem = currentItem;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getCurrentItemId() {
|
||||
return currentItem != null ? currentItem.getId() : -1;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final SolidIconView iconView;
|
||||
protected final TextView titleView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
titleView = v.findViewById(R.id.nav_menu_title);
|
||||
iconView = v.findViewById(R.id.nav_menu_item_icon);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return menuItems != null ? menuItems.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return menuItems.get(position).isGroup() ? TYPE_GROUP : TYPE_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigationMenuAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(viewType == TYPE_GROUP ?
|
||||
R.layout.list_item_nav_menu_group : R.layout.list_item_nav_menu_item, parent, false);
|
||||
return new NavigationMenuAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder vh, int position) {
|
||||
int type = getItemViewType(position);
|
||||
NavMenuItem item = menuItems.get(position);
|
||||
String displayTitle = !Helper.isNullOrEmpty(item.getExtraLabel()) ? String.format("%s (%s)", item.getTitle(), item.getExtraLabel()) : item.getTitle();
|
||||
vh.titleView.setText(displayTitle);
|
||||
if (type == TYPE_ITEM && vh.iconView != null) {
|
||||
vh.iconView.setText(item.getIcon());
|
||||
}
|
||||
vh.itemView.setSelected(item.equals(currentItem));
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
listener.onNavigationMenuItemClicked(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface NavigationMenuItemClickListener {
|
||||
void onNavigationMenuItemClicked(NavMenuItem menuItem);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.listener.SelectionModeListener;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.model.lbryinc.LbryNotification;
|
||||
import io.lbry.browser.ui.controls.SolidIconView;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class NotificationListAdapter extends RecyclerView.Adapter<NotificationListAdapter.ViewHolder> {
|
||||
|
||||
private static final String RULE_CREATOR_SUBSCRIBER = "creator_subscriber";
|
||||
private static final String RULE_COMMENT = "comment";
|
||||
|
||||
private final Context context;
|
||||
private final List<LbryNotification> items;
|
||||
private final List<LbryNotification> selectedItems;
|
||||
@Setter
|
||||
private NotificationClickListener clickListener;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean inSelectionMode;
|
||||
@Setter
|
||||
private SelectionModeListener selectionModeListener;
|
||||
|
||||
public NotificationListAdapter(List<LbryNotification> notifications, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(notifications);
|
||||
this.selectedItems = new ArrayList<>();
|
||||
Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final View layoutView;
|
||||
protected final TextView titleView;
|
||||
protected final TextView bodyView;
|
||||
protected final TextView timeView;
|
||||
protected final SolidIconView iconView;
|
||||
protected final ImageView thumbnailView;
|
||||
protected final View selectedOverlayView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
layoutView = v.findViewById(R.id.notification_layout);
|
||||
titleView = v.findViewById(R.id.notification_title);
|
||||
bodyView = v.findViewById(R.id.notification_body);
|
||||
timeView = v.findViewById(R.id.notification_time);
|
||||
iconView = v.findViewById(R.id.notification_icon);
|
||||
thumbnailView = v.findViewById(R.id.notification_author_thumbnail);
|
||||
selectedOverlayView = v.findViewById(R.id.notification_selected_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
public List<LbryNotification> getSelectedItems() {
|
||||
return this.selectedItems;
|
||||
}
|
||||
public int getSelectedCount() {
|
||||
return selectedItems != null ? selectedItems.size() : 0;
|
||||
}
|
||||
public void clearSelectedItems() {
|
||||
this.selectedItems.clear();
|
||||
}
|
||||
public boolean isNotificationSelected(LbryNotification notification) {
|
||||
return selectedItems.contains(notification);
|
||||
}
|
||||
|
||||
public void insertNotification(LbryNotification notification, int index) {
|
||||
if (!items.contains(notification)) {
|
||||
items.add(index, notification);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addNotification(LbryNotification notification) {
|
||||
if (!items.contains(notification)) {
|
||||
items.add(notification);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public void removeNotifications(List<LbryNotification> notifications) {
|
||||
for (LbryNotification notification : notifications) {
|
||||
items.remove(notification);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<String> getAuthorUrls() {
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (LbryNotification item : items) {
|
||||
if (!Helper.isNullOrEmpty(item.getAuthorUrl())) {
|
||||
urls.add(item.getAuthorUrl());
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void updateAuthorClaims(List<Claim> claims) {
|
||||
for (Claim claim : claims) {
|
||||
if (claim != null && claim.getThumbnailUrl() != null) {
|
||||
updateClaimForAuthorUrl(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void updateClaimForAuthorUrl(Claim claim) {
|
||||
for (LbryNotification item : items) {
|
||||
if (claim.getPermanentUrl().equalsIgnoreCase(item.getAuthorUrl())) {
|
||||
item.setCommentAuthor(claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addNotifications(List<LbryNotification> notifications) {
|
||||
for (LbryNotification notification : notifications) {
|
||||
if (!items.contains(notification)) {
|
||||
items.add(notification);
|
||||
}
|
||||
}
|
||||
Collections.sort(items, Collections.reverseOrder(new LbryNotification()));
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_notification, root, false);
|
||||
return new NotificationListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
private int getStringIdForRule(String rule) {
|
||||
if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) {
|
||||
return R.string.fa_heart;
|
||||
}
|
||||
if (RULE_COMMENT.equalsIgnoreCase(rule)) {
|
||||
return R.string.fa_comment_alt;
|
||||
}
|
||||
return R.string.fa_asterisk;
|
||||
}
|
||||
|
||||
private int getColorForRule(String rule) {
|
||||
if (RULE_CREATOR_SUBSCRIBER.equalsIgnoreCase(rule)) {
|
||||
return Color.RED;
|
||||
}
|
||||
if (RULE_COMMENT.equalsIgnoreCase(rule)) {
|
||||
return ContextCompat.getColor(context, R.color.nextLbryGreen);
|
||||
}
|
||||
|
||||
return ContextCompat.getColor(context, R.color.lbryGreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(NotificationListAdapter.ViewHolder vh, int position) {
|
||||
LbryNotification notification = items.get(position);
|
||||
vh.layoutView.setBackgroundColor(ContextCompat.getColor(context, notification.isSeen() ? android.R.color.transparent : R.color.nextLbryGreenSemiTransparent));
|
||||
vh.selectedOverlayView.setVisibility(isNotificationSelected(notification) ? View.VISIBLE : View.GONE);
|
||||
|
||||
vh.titleView.setVisibility(!Helper.isNullOrEmpty(notification.getTitle()) ? View.VISIBLE : View.GONE);
|
||||
vh.titleView.setText(notification.getTitle());
|
||||
vh.bodyView.setText(notification.getDescription());
|
||||
vh.timeView.setText(DateUtils.getRelativeTimeSpanString(
|
||||
getLocalNotificationTime(notification), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
|
||||
|
||||
vh.thumbnailView.setVisibility(notification.getCommentAuthor() == null ? View.INVISIBLE : View.VISIBLE);
|
||||
if (notification.getCommentAuthor() != null) {
|
||||
Glide.with(context.getApplicationContext()).load(
|
||||
notification.getCommentAuthor().getThumbnailUrl(vh.thumbnailView.getLayoutParams().width, vh.thumbnailView.getLayoutParams().height, 85)).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
|
||||
}
|
||||
|
||||
vh.iconView.setVisibility(notification.getCommentAuthor() != null ? View.INVISIBLE : View.VISIBLE);
|
||||
vh.iconView.setText(getStringIdForRule(notification.getRule()));
|
||||
vh.iconView.setTextColor(getColorForRule(notification.getRule()));
|
||||
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (inSelectionMode) {
|
||||
toggleSelectedNotification(notification);
|
||||
} else {
|
||||
if (clickListener != null) {
|
||||
clickListener.onNotificationClicked(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (!inSelectionMode) {
|
||||
inSelectionMode = true;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onEnterSelectionMode();
|
||||
}
|
||||
}
|
||||
toggleSelectedNotification(notification);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleSelectedNotification(LbryNotification notification) {
|
||||
if (selectedItems.contains(notification)) {
|
||||
selectedItems.remove(notification);
|
||||
} else {
|
||||
selectedItems.add(notification);
|
||||
}
|
||||
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onItemSelectionToggled();
|
||||
}
|
||||
|
||||
if (selectedItems.size() == 0) {
|
||||
inSelectionMode = false;
|
||||
if (selectionModeListener != null) {
|
||||
selectionModeListener.onExitSelectionMode();
|
||||
}
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private long getLocalNotificationTime(LbryNotification notification) {
|
||||
TimeZone utcTZ = TimeZone.getTimeZone("UTC");
|
||||
TimeZone targetTZ = TimeZone.getDefault();
|
||||
Calendar cal = new GregorianCalendar(utcTZ);
|
||||
cal.setTimeInMillis(notification.getTimestamp().getTime());
|
||||
|
||||
cal.add(Calendar.MILLISECOND, utcTZ.getRawOffset() * -1);
|
||||
cal.add(Calendar.MILLISECOND, targetTZ.getRawOffset());
|
||||
return cal.getTimeInMillis();
|
||||
}
|
||||
|
||||
public interface NotificationClickListener {
|
||||
void onNotificationClicked(LbryNotification notification);
|
||||
}
|
||||
}
|
220
app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
Normal file
220
app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
Normal file
|
@ -0,0 +1,220 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.lbryinc.Reward;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.Lbryio;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class RewardListAdapter extends RecyclerView.Adapter<RewardListAdapter.ViewHolder> {
|
||||
|
||||
public static final int DISPLAY_MODE_ALL = 1;
|
||||
public static final int DISPLAY_MODE_UNCLAIMED = 2;
|
||||
|
||||
private final Context context;
|
||||
@Setter
|
||||
private List<Reward> all;
|
||||
private List<Reward> items;
|
||||
@Setter
|
||||
private RewardClickListener clickListener;
|
||||
@Getter
|
||||
private int displayMode;
|
||||
|
||||
public RewardListAdapter(List<Reward> all, Context context) {
|
||||
this.all = new ArrayList<>(all);
|
||||
this.items = new ArrayList<>(all);
|
||||
this.context = context;
|
||||
this.displayMode = DISPLAY_MODE_ALL;
|
||||
|
||||
addCustomReward();
|
||||
}
|
||||
|
||||
public void setRewards(List<Reward> rewards) {
|
||||
this.all = new ArrayList<>(rewards);
|
||||
updateItemsForDisplayMode();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setDisplayMode(int displayMode) {
|
||||
this.displayMode = displayMode;
|
||||
updateItemsForDisplayMode();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void updateItemsForDisplayMode() {
|
||||
if (displayMode == DISPLAY_MODE_ALL) {
|
||||
items = new ArrayList<>(all);
|
||||
} else if (displayMode == DISPLAY_MODE_UNCLAIMED) {
|
||||
items = new ArrayList<>();
|
||||
for (Reward reward : all) {
|
||||
if (!reward.isClaimed()) {
|
||||
items.add(reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
addCustomReward();
|
||||
}
|
||||
|
||||
private void addCustomReward() {
|
||||
Reward custom = new Reward();
|
||||
custom.setCustom(true);
|
||||
custom.setRewardTitle(context.getString(R.string.custom_reward_title));
|
||||
custom.setRewardDescription(context.getString(R.string.custom_reward_description));
|
||||
items.add(custom);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final View iconClaimed;
|
||||
protected final View loading;
|
||||
protected final View upTo;
|
||||
protected final TextView textTitle;
|
||||
protected final TextView textDescription;
|
||||
protected final TextView textLbcValue;
|
||||
protected final TextView textUsdValue;
|
||||
protected final TextView textLinkTransaction;
|
||||
protected final EditText inputCustomCode;
|
||||
protected final MaterialButton buttonClaimCustom;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
iconClaimed = v.findViewById(R.id.reward_item_claimed_icon);
|
||||
upTo = v.findViewById(R.id.reward_item_up_to);
|
||||
loading = v.findViewById(R.id.reward_item_loading);
|
||||
textTitle = v.findViewById(R.id.reward_item_title);
|
||||
textDescription = v.findViewById(R.id.reward_item_description);
|
||||
textLbcValue = v.findViewById(R.id.reward_item_lbc_value);
|
||||
textLinkTransaction = v.findViewById(R.id.reward_item_tx_link);
|
||||
textUsdValue = v.findViewById(R.id.reward_item_usd_value);
|
||||
inputCustomCode = v.findViewById(R.id.reward_item_custom_code_input);
|
||||
buttonClaimCustom = v.findViewById(R.id.reward_item_custom_claim_button);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public void addReward(Reward reward) {
|
||||
if (!items.contains(reward)) {
|
||||
items.add(reward);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public List<Reward> getRewards() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RewardListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_reward, root, false);
|
||||
return new RewardListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RewardListAdapter.ViewHolder vh, int position) {
|
||||
Reward reward = items.get(position);
|
||||
String displayAmount = reward.getDisplayAmount();
|
||||
double rewardAmount = 0;
|
||||
if (!"?".equals(displayAmount)) {
|
||||
rewardAmount = Double.valueOf(displayAmount);
|
||||
}
|
||||
boolean hasTransaction = !Helper.isNullOrEmpty(reward.getTransactionId()) && reward.getTransactionId().length() > 7;
|
||||
vh.iconClaimed.setVisibility(reward.isClaimed() ? View.VISIBLE : View.INVISIBLE);
|
||||
vh.inputCustomCode.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
|
||||
vh.buttonClaimCustom.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
|
||||
vh.textTitle.setText(reward.getRewardTitle());
|
||||
vh.textDescription.setText(reward.getRewardDescription());
|
||||
vh.upTo.setVisibility(reward.shouldDisplayRange() ? View.VISIBLE : View.GONE);
|
||||
vh.textLbcValue.setText(reward.isCustom() ? "?" : Helper.LBC_CURRENCY_FORMAT.format(Helper.parseDouble(reward.getDisplayAmount(), 0)));
|
||||
vh.textLinkTransaction.setVisibility(hasTransaction ? View.VISIBLE : View.GONE);
|
||||
vh.textLinkTransaction.setText(hasTransaction ? reward.getTransactionId().substring(0, 7) : null);
|
||||
vh.textLinkTransaction.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (context != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, reward.getTransactionId())));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.textUsdValue.setText(reward.isCustom() || Lbryio.LBCUSDRate == 0 ? null :
|
||||
String.format("≈$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(rewardAmount * Lbryio.LBCUSDRate)));
|
||||
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (reward.isClaimed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vh.inputCustomCode != null && !vh.inputCustomCode.hasFocus()) {
|
||||
vh.inputCustomCode.requestFocus();
|
||||
}
|
||||
|
||||
if (clickListener != null) {
|
||||
clickListener.onRewardClicked(reward, vh.loading);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.inputCustomCode.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
String value = charSequence.toString().trim();
|
||||
vh.buttonClaimCustom.setEnabled(value.length() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
vh.buttonClaimCustom.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String claimCode = Helper.getValue(vh.inputCustomCode.getText());
|
||||
if (Helper.isNullOrEmpty(claimCode)) {
|
||||
Snackbar.make(view, R.string.please_enter_claim_code, Snackbar.LENGTH_LONG).
|
||||
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickListener != null) {
|
||||
clickListener.onCustomClaimButtonClicked(claimCode, vh.inputCustomCode, vh.buttonClaimCustom, vh.loading);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface RewardClickListener {
|
||||
void onRewardClicked(Reward reward, View loadingView);
|
||||
void onCustomClaimButtonClicked(String code, EditText inputCustomCode, MaterialButton buttonClaim, View loadingView);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.StartupStage;
|
||||
|
||||
public class StartupStageAdapter extends BaseAdapter {
|
||||
private final List<StartupStage> list;
|
||||
private final LayoutInflater inflater;
|
||||
private final String[] stagesString;
|
||||
|
||||
public StartupStageAdapter(Context ctx, List<StartupStage> rows) {
|
||||
this.list = rows;
|
||||
this.inflater = LayoutInflater.from(ctx);
|
||||
|
||||
stagesString = new String[9];
|
||||
|
||||
stagesString[0] = ctx.getResources().getString(R.string.installation_id_loaded);
|
||||
stagesString[1] = ctx.getResources().getString(R.string.known_tags_loaded);
|
||||
stagesString[2] = ctx.getResources().getString(R.string.exchange_rate_loaded);
|
||||
stagesString[3] = ctx.getResources().getString(R.string.user_authenticated);
|
||||
stagesString[4] = ctx.getResources().getString(R.string.installation_registered);
|
||||
stagesString[5] = ctx.getResources().getString(R.string.subscriptions_loaded);
|
||||
stagesString[6] = ctx.getResources().getString(R.string.subscriptions_resolved);
|
||||
stagesString[7] = ctx.getResources().getString(R.string.block_list_loaded);
|
||||
stagesString[8] = ctx.getResources().getString(R.string.filter_list_loaded);
|
||||
}
|
||||
@Override
|
||||
public int getCount() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int i) {
|
||||
return list.get(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int i, View view, ViewGroup viewGroup) {
|
||||
if (view == null) {
|
||||
view = inflater.inflate(R.layout.list_item_startupstage, viewGroup, false);
|
||||
|
||||
ImageView iconView = view.findViewById(R.id.startup_stage_icon);
|
||||
TextView textView = view.findViewById(R.id.startup_stage_text);
|
||||
|
||||
StartupStage item = (StartupStage) getItem(i);
|
||||
|
||||
iconView.setImageResource(item.stageDone ? R.drawable.ic_check : R.drawable.ic_close);
|
||||
iconView.setColorFilter(item.stageDone ? Color.WHITE : Color.RED);
|
||||
|
||||
textView.setText(stagesString[item.stage - 1]);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.listener.ChannelItemSelectionListener;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import lombok.Setter;
|
||||
|
||||
public class SuggestedChannelGridAdapter extends RecyclerView.Adapter<SuggestedChannelGridAdapter.ViewHolder> {
|
||||
private final Context context;
|
||||
private final List<Claim> items;
|
||||
private final List<Claim> selectedItems;
|
||||
@Setter
|
||||
private ChannelItemSelectionListener listener;
|
||||
|
||||
public SuggestedChannelGridAdapter(List<Claim> items, Context context) {
|
||||
this.items = new ArrayList<>(items);
|
||||
this.selectedItems = new ArrayList<>();
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final View noThumbnailView;
|
||||
protected final ImageView thumbnailView;
|
||||
protected final TextView alphaView;
|
||||
protected final TextView titleView;
|
||||
protected final TextView tagView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
noThumbnailView = v.findViewById(R.id.suggested_channel_no_thumbnail);
|
||||
alphaView = v.findViewById(R.id.suggested_channel_alpha_view);
|
||||
thumbnailView = v.findViewById(R.id.suggested_channel_thumbnail);
|
||||
titleView = v.findViewById(R.id.suggested_channel_title);
|
||||
tagView = v.findViewById(R.id.suggested_channel_tag);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public int getSelectedCount() { return selectedItems.size(); }
|
||||
|
||||
public void clearItems() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<Claim> getSelectedItems() {
|
||||
return this.selectedItems;
|
||||
}
|
||||
public void clearSelectedItems() {
|
||||
this.selectedItems.clear();
|
||||
}
|
||||
public boolean isClaimSelected(Claim claim) {
|
||||
return selectedItems.contains(claim);
|
||||
}
|
||||
|
||||
public void addClaims(List<Claim> claims) {
|
||||
for (Claim claim : claims) {
|
||||
if (!items.contains(claim)) {
|
||||
items.add(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestedChannelGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_suggested_channel, root, false);
|
||||
return new SuggestedChannelGridAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestedChannelGridAdapter.ViewHolder vh, int position) {
|
||||
Claim claim = items.get(position);
|
||||
ViewGroup.LayoutParams lp = vh.thumbnailView.getLayoutParams();
|
||||
String thumbnailUrl = claim.getThumbnailUrl(lp.width, lp.height, 85);
|
||||
|
||||
int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
|
||||
Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
|
||||
vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.INVISIBLE : View.VISIBLE);
|
||||
vh.alphaView.setText(claim.getName().substring(1, 2));
|
||||
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
|
||||
vh.thumbnailView.setVisibility(View.VISIBLE);
|
||||
Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
|
||||
} else {
|
||||
vh.thumbnailView.setVisibility(View.GONE);
|
||||
}
|
||||
vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
|
||||
|
||||
String firstTag = claim.getFirstTag();
|
||||
vh.tagView.setVisibility(Helper.isNullOrEmpty(firstTag) ? View.INVISIBLE : View.VISIBLE);
|
||||
vh.tagView.setBackgroundResource(R.drawable.bg_tag);
|
||||
vh.tagView.setText(firstTag);
|
||||
vh.itemView.setSelected(isClaimSelected(claim));
|
||||
|
||||
vh.itemView.setOnClickListener(view -> {
|
||||
if (selectedItems.contains(claim)) {
|
||||
selectedItems.remove(claim);
|
||||
if (listener != null) {
|
||||
listener.onChannelItemDeselected(claim);
|
||||
}
|
||||
} else {
|
||||
selectedItems.add(claim);
|
||||
if (listener != null) {
|
||||
listener.onChannelItemSelected(claim);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
}
|
||||
}
|
109
app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
Normal file
109
app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
Normal file
|
@ -0,0 +1,109 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Tag;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHolder> {
|
||||
|
||||
public static final int CUSTOMIZE_MODE_NONE = 0;
|
||||
public static final int CUSTOMIZE_MODE_ADD = 1;
|
||||
public static final int CUSTOMIZE_MODE_REMOVE = 2;
|
||||
|
||||
private final Context context;
|
||||
private List<Tag> items;
|
||||
@Setter
|
||||
private TagClickListener clickListener;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private int customizeMode;
|
||||
|
||||
public TagListAdapter(List<Tag> tags, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(tags);
|
||||
this.customizeMode = CUSTOMIZE_MODE_NONE;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final ImageView iconView;
|
||||
protected final TextView nameView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
iconView = v.findViewById(R.id.tag_action);
|
||||
nameView = v.findViewById(R.id.tag_name);
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
public void addTag(Tag tag) {
|
||||
if (!items.contains(tag)) {
|
||||
items.add(tag);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public List<Tag> getTags() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
public void setTags(List<Tag> tags) {
|
||||
items = new ArrayList<>(tags);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addTags(List<Tag> tags) {
|
||||
for (Tag tag : tags) {
|
||||
if (!items.contains(tag)) {
|
||||
items.add(tag);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
public void removeTag(Tag tag) {
|
||||
items.remove(tag);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TagListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_tag, root, false);
|
||||
return new TagListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TagListAdapter.ViewHolder vh, int position) {
|
||||
Tag tag = items.get(position);
|
||||
vh.nameView.setText(tag.getName().toLowerCase());
|
||||
vh.iconView.setVisibility(customizeMode == CUSTOMIZE_MODE_NONE ? View.GONE : View.VISIBLE);
|
||||
vh.iconView.setImageResource(customizeMode == CUSTOMIZE_MODE_REMOVE ? R.drawable.ic_close : R.drawable.ic_add);
|
||||
vh.itemView.setBackgroundResource(tag.isMature() ? R.drawable.bg_tag_mature : R.drawable.bg_tag);
|
||||
vh.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (clickListener != null) {
|
||||
clickListener.onTagClicked(tag, customizeMode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface TagClickListener {
|
||||
void onTagClicked(Tag tag, int customizeMode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.Transaction;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.LbryUri;
|
||||
import lombok.Setter;
|
||||
|
||||
public class TransactionListAdapter extends RecyclerView.Adapter<TransactionListAdapter.ViewHolder> {
|
||||
|
||||
private static final DecimalFormat TX_LIST_AMOUNT_FORMAT = new DecimalFormat("#,##0.0000");
|
||||
private static final SimpleDateFormat TX_LIST_DATE_FORMAT = new SimpleDateFormat("MMM d");
|
||||
|
||||
private final Context context;
|
||||
private final List<Transaction> items;
|
||||
@Setter
|
||||
private TransactionClickListener listener;
|
||||
|
||||
public TransactionListAdapter(List<Transaction> transactions, Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>(transactions);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<Transaction> getItems() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
public void addTransactions(List<Transaction> transactions) {
|
||||
for (Transaction tx : transactions) {
|
||||
if (!items.contains(tx)) {
|
||||
items.add(tx);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_transaction, root, false);
|
||||
return new TransactionListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TransactionListAdapter.ViewHolder vh, int position) {
|
||||
Transaction item = items.get(position);
|
||||
vh.descView.setText(item.getDescriptionStringId());
|
||||
vh.amountView.setText(TX_LIST_AMOUNT_FORMAT.format(item.getValue().doubleValue()));
|
||||
vh.claimView.setText(item.getClaim());
|
||||
vh.feeView.setText(context.getString(R.string.tx_list_fee, TX_LIST_AMOUNT_FORMAT.format(item.getFee().doubleValue())));
|
||||
vh.txidLinkView.setText(item.getTxid().substring(0, 7));
|
||||
vh.dateView.setVisibility(item.getConfirmations() > 0 ? View.VISIBLE : View.GONE);
|
||||
vh.dateView.setText(item.getConfirmations() > 0 ? TX_LIST_DATE_FORMAT.format(item.getTxDate()) : null);
|
||||
vh.pendingView.setVisibility(item.getConfirmations() == 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
vh.infoFeeContainer.setVisibility(!Helper.isNullOrEmpty(item.getClaim()) || Math.abs(item.getFee().doubleValue()) > 0 ?
|
||||
View.VISIBLE : View.GONE);
|
||||
|
||||
vh.claimView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
LbryUri claimUrl = item.getClaimUrl();
|
||||
if (claimUrl != null && listener != null) {
|
||||
listener.onClaimUrlClicked(claimUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.txidLinkView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (context != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, item.getTxid())));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vh.itemView.setOnClickListener(view -> {
|
||||
if (listener != null) {
|
||||
listener.onTransactionClicked(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final TextView descView;
|
||||
protected final TextView amountView;
|
||||
protected final TextView claimView;
|
||||
protected final TextView feeView;
|
||||
protected final TextView txidLinkView;
|
||||
protected final TextView dateView;
|
||||
protected final TextView pendingView;
|
||||
protected final View infoFeeContainer;
|
||||
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
descView = v.findViewById(R.id.transaction_desc);
|
||||
amountView = v.findViewById(R.id.transaction_amount);
|
||||
claimView = v.findViewById(R.id.transaction_claim);
|
||||
feeView = v.findViewById(R.id.transaction_fee);
|
||||
txidLinkView = v.findViewById(R.id.transaction_id_link);
|
||||
dateView = v.findViewById(R.id.transaction_date);
|
||||
pendingView = v.findViewById(R.id.transaction_pending_text);
|
||||
infoFeeContainer = v.findViewById(R.id.transaction_info_fee_container);
|
||||
}
|
||||
}
|
||||
|
||||
public interface TransactionClickListener {
|
||||
void onTransactionClicked(Transaction transaction);
|
||||
void onClaimUrlClicked(LbryUri uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.exceptions.LbryUriException;
|
||||
import io.lbry.browser.model.Claim;
|
||||
import io.lbry.browser.model.UrlSuggestion;
|
||||
import io.lbry.browser.ui.controls.SolidIconView;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.utils.LbryUri;
|
||||
import lombok.Setter;
|
||||
|
||||
public class UrlSuggestionListAdapter extends RecyclerView.Adapter<UrlSuggestionListAdapter.ViewHolder> {
|
||||
private final Context context;
|
||||
private final List<UrlSuggestion> items;
|
||||
@Setter
|
||||
private UrlSuggestionClickListener listener;
|
||||
|
||||
public UrlSuggestionListAdapter(Context context) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<UrlSuggestion> getItems() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
public List<String> getItemUrls() {
|
||||
List<String> uris = new ArrayList<>();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
LbryUri uri = items.get(i).getUri();
|
||||
if (uri != null) {
|
||||
uris.add(uri.toString());
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
}
|
||||
|
||||
public void setClaimForUrl(LbryUri url, Claim claim) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
LbryUri thisUrl = items.get(i).getUri();
|
||||
try {
|
||||
if (thisUrl != null) {
|
||||
LbryUri vanity = LbryUri.parse(thisUrl.toVanityString());
|
||||
if (thisUrl.equals(url) || vanity.equals(url)) {
|
||||
items.get(i).setClaim(claim);
|
||||
}
|
||||
}
|
||||
} catch (LbryUriException ex) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addUrlSuggestions(List<UrlSuggestion> urlSuggestions) {
|
||||
for (UrlSuggestion urlSuggestion : urlSuggestions) {
|
||||
if (!items.contains(urlSuggestion)) {
|
||||
items.add(urlSuggestion);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
return items != null ? items.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UrlSuggestionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
|
||||
View v = LayoutInflater.from(context).inflate(R.layout.list_item_url_suggestion, root, false);
|
||||
return new UrlSuggestionListAdapter.ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(UrlSuggestionListAdapter.ViewHolder vh, int position) {
|
||||
UrlSuggestion item = items.get(position);
|
||||
|
||||
String fullTitle, desc;
|
||||
int iconStringId;
|
||||
switch (item.getType()) {
|
||||
case UrlSuggestion.TYPE_CHANNEL:
|
||||
iconStringId = R.string.fa_at;
|
||||
fullTitle = item.getTitle();
|
||||
desc = item.getClaim() != null ? item.getClaim().getTitle() :
|
||||
((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_channel_url_desc), item.getText()));
|
||||
break;
|
||||
case UrlSuggestion.TYPE_TAG:
|
||||
iconStringId = R.string.fa_hashtag;
|
||||
fullTitle = String.format(context.getString(R.string.tag_url_title), item.getText());
|
||||
desc = String.format(context.getString(R.string.explore_tag_url_desc), item.getText());
|
||||
break;
|
||||
case UrlSuggestion.TYPE_SEARCH:
|
||||
iconStringId = R.string.fa_search;
|
||||
fullTitle = String.format(context.getString(R.string.search_url_title), item.getText());
|
||||
desc = String.format(context.getString(R.string.search_url_desc), item.getText());
|
||||
break;
|
||||
case UrlSuggestion.TYPE_FILE:
|
||||
default:
|
||||
iconStringId = R.string.fa_file;
|
||||
fullTitle = item.getTitle();
|
||||
desc = item.getClaim() != null ? item.getClaim().getTitle() :
|
||||
((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_file_url_desc), item.getText()));
|
||||
break;
|
||||
}
|
||||
|
||||
vh.iconView.setText(iconStringId);
|
||||
vh.titleView.setText(fullTitle);
|
||||
vh.descView.setText(desc);
|
||||
|
||||
vh.itemView.setOnClickListener(view -> {
|
||||
if (listener != null) {
|
||||
listener.onUrlSuggestionClicked(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected final SolidIconView iconView;
|
||||
protected final TextView titleView;
|
||||
protected final TextView descView;
|
||||
public ViewHolder(View v) {
|
||||
super(v);
|
||||
iconView = v.findViewById(R.id.url_suggestion_icon);
|
||||
titleView = v.findViewById(R.id.url_suggestion_title);
|
||||
descView = v.findViewById(R.id.url_suggestion_description);
|
||||
}
|
||||
}
|
||||
|
||||
public interface UrlSuggestionClickListener {
|
||||
void onUrlSuggestionClicked(UrlSuggestion urlSuggestion);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import io.lbry.browser.listener.SignInListener;
|
||||
import io.lbry.browser.listener.WalletSyncListener;
|
||||
import io.lbry.browser.ui.verification.EmailVerificationFragment;
|
||||
import io.lbry.browser.ui.verification.ManualVerificationFragment;
|
||||
import io.lbry.browser.ui.verification.PhoneVerificationFragment;
|
||||
import io.lbry.browser.ui.verification.WalletVerificationFragment;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
/**
|
||||
* 4 fragments
|
||||
* - Email collect / verify (sign in)
|
||||
* - Phone number collect / verify (rewards)
|
||||
* - Wallet password
|
||||
* - Manual verification page
|
||||
*/
|
||||
public class VerificationPagerAdapter extends FragmentStateAdapter {
|
||||
public static final int PAGE_VERIFICATION_EMAIL = 0;
|
||||
public static final int PAGE_VERIFICATION_PHONE = 1;
|
||||
public static final int PAGE_VERIFICATION_WALLET = 2;
|
||||
public static final int PAGE_VERIFICATION_MANUAL = 3;
|
||||
|
||||
private final FragmentActivity activity;
|
||||
|
||||
public VerificationPagerAdapter(FragmentActivity activity) {
|
||||
super(activity);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
default:
|
||||
EmailVerificationFragment evFragment = EmailVerificationFragment.class.newInstance();
|
||||
if (activity instanceof SignInListener) {
|
||||
evFragment.setListener((SignInListener) activity);
|
||||
}
|
||||
return evFragment;
|
||||
case 1:
|
||||
PhoneVerificationFragment pvFragment = PhoneVerificationFragment.class.newInstance();
|
||||
if (activity instanceof SignInListener) {
|
||||
pvFragment.setListener((SignInListener) activity);
|
||||
}
|
||||
return pvFragment;
|
||||
case 2:
|
||||
WalletVerificationFragment wvFragment = WalletVerificationFragment.class.newInstance();
|
||||
if (activity instanceof WalletSyncListener) {
|
||||
wvFragment.setListener((WalletSyncListener) activity);
|
||||
}
|
||||
return wvFragment;
|
||||
case 3:
|
||||
ManualVerificationFragment mvFragment = ManualVerificationFragment.class.newInstance();
|
||||
if (activity instanceof SignInListener) {
|
||||
mvFragment.setListener((SignInListener) activity);
|
||||
}
|
||||
return mvFragment;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 4;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package io.lbry.browser.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.lbry.browser.MainActivity;
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.model.WalletDetailItem;
|
||||
import io.lbry.browser.utils.Helper;
|
||||
import io.lbry.browser.views.CreditsBalanceView;
|
||||
|
||||
public class WalletDetailAdapter extends BaseAdapter {
|
||||
private final List<WalletDetailItem> list;
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public WalletDetailAdapter(Context ctx, List<WalletDetailItem> rows) {
|
||||
this.list = rows;
|
||||
this.inflater = LayoutInflater.from(ctx);
|
||||
}
|
||||
@Override
|
||||
public int getCount() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int i) {
|
||||
return list.get(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int i, View view, ViewGroup viewGroup) {
|
||||
if (view == null) {
|
||||
view = inflater.inflate(R.layout.list_item_boosting_balance, viewGroup, false);
|
||||
|
||||
CreditsBalanceView balanceView = view.findViewById(R.id.wallet_supporting_balance);
|
||||
TextView detailTextView = view.findViewById(R.id.detail);
|
||||
TextView detailExplanationTextView = view.findViewById(R.id.detail_explanation);
|
||||
|
||||
WalletDetailItem item = (WalletDetailItem) getItem(i);
|
||||
|
||||
detailTextView.setText(item.detail);
|
||||
detailExplanationTextView.setText(item.detailDesc);
|
||||
|
||||
Helper.setViewText(balanceView, item.detailAmount);
|
||||
|
||||
ProgressBar progressUnlockTips = view.findViewById(R.id.wallet_unlock_tips_progress);
|
||||
progressUnlockTips.setVisibility(item.isInProgress ? View.VISIBLE : View.GONE);
|
||||
|
||||
ImageButton buttonLock = view.findViewById(R.id.lock_button);
|
||||
buttonLock.setVisibility((item.isUnlockable && !item.isInProgress) ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (item.isUnlockable) {
|
||||
buttonLock.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (view.getContext() != null) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()).
|
||||
setTitle(R.string.unlock_tips).
|
||||
setMessage(R.string.confirm_unlock_tips)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
unlockTips(view);
|
||||
}
|
||||
}).setNegativeButton(R.string.no, null);
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
private void unlockTips(View v) {
|
||||
Context ctx = v.getContext();
|
||||
if (ctx instanceof MainActivity) {
|
||||
v.setVisibility(View.GONE);
|
||||
View progress = v.getRootView().findViewById(R.id.wallet_unlock_tips_progress);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
((MainActivity) ctx).unlockTips();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue