Compare commits
661 Commits
Author | SHA1 | Date | |
---|---|---|---|
54856a087f | |||
da0696cac0 | |||
288970483b | |||
5b540dbc38 | |||
def75b6431 | |||
4826ed0355 | |||
3b47a4a2f0 | |||
9693dec807 | |||
ca5186d20a | |||
78d5744139 | |||
b2609554e6 | |||
5db312bbb8 | |||
7592ab512b | |||
000b346529 | |||
1bb975c584 | |||
69aaa323e2 | |||
cedb207eca | |||
b9e91f30ef | |||
6a8c2d7fcd | |||
60a908a44a | |||
6b887ff74b | |||
721a15ec21 | |||
a2933ac763 | |||
e1efe9643c | |||
90242ae801 | |||
0c88f33981 | |||
5e13a8f20f | |||
ca4b7ada97 | |||
62a82b01b8 | |||
1994fa2f7d | |||
ae32cbfb6f | |||
2dff3d9191 | |||
c0ae0466c2 | |||
58b0574cf9 | |||
5472c607cd | |||
f95cb20408 | |||
5640b7e56c | |||
fa697f1313 | |||
a12623f8e4 | |||
abba04839a | |||
2e38639910 | |||
db78717eec | |||
58a498868d | |||
46e723a238 | |||
304b6c3761 | |||
33fb04956c | |||
6e3381fb61 | |||
56720659ee | |||
626c9e2797 | |||
05cd96afc0 | |||
c8faa8984f | |||
a025efbf3b | |||
e62e04e13b | |||
e6b5ea4e67 | |||
c3148c6744 | |||
193f538d29 | |||
7f45db0473 | |||
d89423b9ac | |||
25fd869c01 | |||
d1d956b77a | |||
41c14362a8 | |||
6fa8c901fc | |||
db124ab9de | |||
953940690d | |||
918661be2d | |||
7b8a5c9a56 | |||
2d5ab7bf0c | |||
9ba281befb | |||
00c8eed034 | |||
a1e4f89cd1 | |||
36a43b3861 | |||
aa6d470f40 | |||
0046a8a477 | |||
43ff9d186a | |||
73dae304be | |||
66103a451b | |||
600c62316d | |||
d370ddc4d1 | |||
4049f6a5c7 | |||
3e96ac207e | |||
84f1ab12cf | |||
f48f6ed788 | |||
e517803bd8 | |||
3eaf390790 | |||
6de54d63e6 | |||
dd7a2f476b | |||
1485cc05f4 | |||
d1dad3e61a | |||
e5024b0420 | |||
9b01692c55 | |||
33aa587d36 | |||
12e0766803 | |||
a8721ad7a4 | |||
bc5e882894 | |||
e3460322b1 | |||
7e3288a076 | |||
ddc754ec25 | |||
134a0766d6 | |||
69da932ab5 | |||
592fb6328a | |||
a0aead6491 | |||
722b6cc06d | |||
6d7c4b40f6 | |||
f538ed39fc | |||
65821492ad | |||
6ede718a9f | |||
f1757937a4 | |||
2bd2e0a953 | |||
b5aef28af0 | |||
45747a1506 | |||
c6e2e08bcb | |||
25bf18661e | |||
6b088dcd24 | |||
d2b18e1880 | |||
eec7c94e98 | |||
d1f8fcacc0 | |||
07e4a33cbd | |||
f6317f566e | |||
9f51e4e6a5 | |||
750604a31f | |||
392eee0ad4 | |||
37e7b987ee | |||
9eac51e729 | |||
fa9cce6783 | |||
f0d4b63a97 | |||
83eeb11388 | |||
01f746f33d | |||
200851894b | |||
862e5cf4ab | |||
0b07f2a407 | |||
9ba6feef0b | |||
63a0638522 | |||
f9a4e6e363 | |||
6b40fd4bdc | |||
04c7776466 | |||
92c335b4e1 | |||
17251e576b | |||
62ea782429 | |||
f99474e3c1 | |||
57ac8f428f | |||
9cc1adbf15 | |||
1d9a440ae7 | |||
511553806c | |||
87e7d7c4fe | |||
ec87089310 | |||
d8478ebb01 | |||
600adc81b5 | |||
ddac2870af | |||
8d9c8c1394 | |||
b59c3bcb23 | |||
7f554adba5 | |||
21ce061282 | |||
bdb71e9b14 | |||
df22e7de15 | |||
6b3550396b | |||
c70f1e31a6 | |||
695670e944 | |||
1028826788 | |||
82a8977c96 | |||
07d9ce1054 | |||
7da7d49277 | |||
9b45365441 | |||
91a7464bce | |||
51add226eb | |||
332e9f5108 | |||
0b91087c07 | |||
ebbb1ba0f8 | |||
e9143ae852 | |||
42e8ecee78 | |||
4efd76fcbc | |||
fb1614070e | |||
c473dd7227 | |||
76bddb195d | |||
1e02ad2041 | |||
f6ab909f8b | |||
7e520e9bed | |||
32e2d05014 | |||
40d9c97f73 | |||
1aa68d3449 | |||
aeeac8cccd | |||
7292edf997 | |||
f49256c72f | |||
d02b28b81f | |||
08117043dd | |||
63496c993e | |||
00ef542e49 | |||
a78c6e6b33 | |||
363eaf9bf9 | |||
fec6683701 | |||
1549edb647 | |||
3de48ba162 | |||
a2a3d6f1a7 | |||
ccab2c7648 | |||
880dd1db5c | |||
ed18fea356 | |||
9816b20bf6 | |||
0bb2195bff | |||
ab2d0c4036 | |||
99fc417109 | |||
dc304ef8c1 | |||
c5511880bc | |||
5fe76d735e | |||
3064b3b835 | |||
70dc8af3ce | |||
53c8c241da | |||
bdc4f5680b | |||
ed290573b2 | |||
1616a97a8a | |||
d090183007 | |||
de337fd260 | |||
12dc206323 | |||
d47c508dee | |||
ed75f55437 | |||
5ad3ad4a57 | |||
aeac1bd1d4 | |||
4d18085072 | |||
0c9f8214ca | |||
a7ce7ce02e | |||
820986c7f0 | |||
8079cae745 | |||
6f067bd258 | |||
b6ade0f212 | |||
27dadc1be3 | |||
95e4162b4c | |||
f75557585e | |||
1b4c26919b | |||
ad085bf129 | |||
8fcd551105 | |||
a0954700e2 | |||
9705560442 | |||
1f47a13ce5 | |||
6f0ff2c975 | |||
76e5477986 | |||
7f308d5be3 | |||
54a43c83e8 | |||
8fe7266c84 | |||
d7a46b27b7 | |||
2257d09fdd | |||
047c5481c4 | |||
8a6719f934 | |||
51a692f3be | |||
b333f93171 | |||
89d34a1a71 | |||
8788e920ce | |||
d306fb53d3 | |||
374537b5c7 | |||
598149d4cd | |||
50bcf18096 | |||
a089ced03f | |||
1f18dddf8b | |||
f5934e240e | |||
6b8da2eacf | |||
f4757a67b7 | |||
6edeb9d840 | |||
43ce0fd7bc | |||
5599f5a8fc | |||
6fd45ceb4f | |||
05ad8aac29 | |||
fa4f2476b7 | |||
00818a94e9 | |||
5d5250e44a | |||
3052b33132 | |||
50de6f8b5b | |||
f88a2f415f | |||
96f9813e01 | |||
fee739cb17 | |||
b1814c63b9 | |||
c1d45678f8 | |||
3d34e59a94 | |||
f1133bea8b | |||
ec64c88ff1 | |||
be66dbba6c | |||
8926cdbbf5 | |||
a956870dec | |||
8ed7951c9b | |||
5569a47674 | |||
0dc6981913 | |||
4984f2f7ad | |||
3b6891c84a | |||
4901e7174c | |||
8d70e68fe2 | |||
d3e1527b70 | |||
0c201301f2 | |||
6090590f24 | |||
06b88c783d | |||
bb75ebf635 | |||
7d7d0014be | |||
b3f8d44794 | |||
29d1e38340 | |||
2be872e61e | |||
377c5518f7 | |||
21be7357b5 | |||
d47ba2c820 | |||
a64b14614a | |||
6a88192e77 | |||
aa7c630818 | |||
7fb54f14c7 | |||
3d709c02b7 | |||
339d384561 | |||
50338d51af | |||
92dbabf899 | |||
0043021390 | |||
70ba9b20da | |||
7fda0a04a1 | |||
3db3157dc9 | |||
2089fe60ca | |||
9606d36670 | |||
869cf64c54 | |||
f57ec1f6c0 | |||
361eea9a06 | |||
838b4056ac | |||
0c0a98510b | |||
be642ed06f | |||
fd77f38e95 | |||
c9baab7267 | |||
86985cfd5b | |||
1327a4e069 | |||
c46acbc579 | |||
4c6a403fae | |||
78920022bd | |||
7b16c41e82 | |||
3389f8bd09 | |||
8dc25c527d | |||
46d6bd57c1 | |||
db014fe13d | |||
6c293f4cac | |||
91e5d3736f | |||
e11dee220f | |||
fcebf916d2 | |||
73cc1a7297 | |||
798f112498 | |||
38b5e7dc65 | |||
2799a48f2b | |||
ad5edae6cd | |||
9cb02f0272 | |||
6d24fd9336 | |||
a3a7b78c96 | |||
e995286068 | |||
65fb6d9b7e | |||
eb02d1efad | |||
f8d3e1eefb | |||
218b8fa843 | |||
9f94af6239 | |||
d3584ac40e | |||
90bdb289d0 | |||
78a08750a2 | |||
baba851e97 | |||
2a03783623 | |||
9f2a4438b1 | |||
5ee5287ffa | |||
29547c2c94 | |||
4846c870fa | |||
c17980a032 | |||
a929e419d9 | |||
487d484bae | |||
0ca4c04c61 | |||
c857cf2d67 | |||
acb502028b | |||
533636f3a1 | |||
eb5672901b | |||
53a8716b51 | |||
3aaff612af | |||
fdcd8c6c6a | |||
bafd478604 | |||
987513a88b | |||
a450ab2a3b | |||
db89fe5aad | |||
67a30b92f6 | |||
c397de8c3e | |||
b4db532c45 | |||
ebecc9c80a | |||
4f8556fca8 | |||
68892fb41b | |||
6d6f6c72ac | |||
df5556b945 | |||
d6c74049c3 | |||
18946464a2 | |||
edb5eabee7 | |||
99a305f3e2 | |||
68dc5a6acf | |||
6816461502 | |||
15b93bbd9e | |||
cd61e140f6 | |||
4d861a84e6 | |||
f24de68618 | |||
3bcffff444 | |||
75e9031fa5 | |||
3b77e24399 | |||
0a738e895f | |||
242e5ba035 | |||
c94612106c | |||
320924b4ed | |||
403ecc4521 | |||
6a50b37364 | |||
d9d341ac5d | |||
e9805b731e | |||
c6d4337cd1 | |||
173f4b2ff7 | |||
3b9436264c | |||
35fe87d79d | |||
f1bb7ba9ad | |||
279f229166 | |||
be1794e27b | |||
4d4a2039c8 | |||
3013ae4f35 | |||
bb3f7d3786 | |||
f7cc305e44 | |||
da17f89148 | |||
ec71ab3c6f | |||
0d007f1492 | |||
96f8663b8f | |||
1a4bc1b301 | |||
b51ae58a97 | |||
b126fc32da | |||
b8d234c415 | |||
2c8902d404 | |||
80ad65b196 | |||
744d9ba72b | |||
0c1d708588 | |||
95e79e7c5d | |||
3ce3260d20 | |||
641f4f34d3 | |||
99620cb1c5 | |||
8f5f33f5d2 | |||
78e9230b82 | |||
78aa44c007 | |||
53fd944f00 | |||
9e6cb4ee3d | |||
87ad6f2826 | |||
9050f5a56f | |||
3437004082 | |||
dcf620af87 | |||
128085a02e | |||
302040ec25 | |||
e177c22032 | |||
a11007113a | |||
5e7897bcf4 | |||
9559af3637 | |||
4c499abcdb | |||
0055a503b3 | |||
3a189ee4b6 | |||
e25dc49271 | |||
4208a80db8 | |||
ddb75e0d93 | |||
8b37e992a2 | |||
bac59036cd | |||
6c89a3b77c | |||
dc2ef39fc6 | |||
a4806da2c5 | |||
ee30edb214 | |||
e4ed663fb3 | |||
01629309b0 | |||
059c2991fb | |||
686ec5dd90 | |||
eab9df8ed9 | |||
0107c3d7e2 | |||
2def2f2e2c | |||
44c79892a0 | |||
bc96b314c2 | |||
8dcf749b4e | |||
6a56ec6442 | |||
30e46d7eae | |||
9458b1834b | |||
297f797b97 | |||
c70e80758c | |||
3bf1d7c4f9 | |||
173247041a | |||
3a28772096 | |||
bd08b8aba3 | |||
2ceb0f988b | |||
4ef3b155b8 | |||
350e24cded | |||
1bf8a578bc | |||
4818a101cc | |||
baebf938ef | |||
fea57c7b1e | |||
113dfa68be | |||
60c6514fa1 | |||
114485afc3 | |||
d6a51381b9 | |||
620f13fd7c | |||
6577b2c3d7 | |||
caef522c8b | |||
40ea07de2e | |||
7905e4aa12 | |||
64f4fd708a | |||
b46e4a018f | |||
3e999a9be2 | |||
f656d621e6 | |||
951cc1e6bd | |||
d8d4264f1b | |||
014eeec2b9 | |||
83837bddc3 | |||
f97666db92 | |||
ee08ea41a1 | |||
4ca64610cb | |||
4980145e46 | |||
10cbc19a0c | |||
15fba2b29b | |||
096952f88c | |||
0ea70c1922 | |||
69ac2e2b44 | |||
68098f4d84 | |||
080d52893e | |||
b02334a8d4 | |||
27118add22 | |||
2a6f98a1e8 | |||
1f67f2fdee | |||
ebf4d294a8 | |||
4a4dbacc95 | |||
687839b5f8 | |||
8fb339034f | |||
8e9fd9c985 | |||
72400f71c0 | |||
1151587951 | |||
abcd500045 | |||
beda24e736 | |||
37b2c5c2df | |||
7b5246ebf1 | |||
d6d5e72f48 | |||
fa8e88d489 | |||
1ef2da9f76 | |||
1ebd894be7 | |||
a9c493d105 | |||
f833d73fab | |||
9e6602f114 | |||
3bdfef9f8b | |||
6f7f475a6b | |||
8fc5fab67b | |||
6927e92396 | |||
c7470396d7 | |||
f21570e2e4 | |||
51f406e20c | |||
9e3fde744e | |||
ccf406ae68 | |||
bc78d1e079 | |||
d151eb261e | |||
0856598cd9 | |||
f0563efc62 | |||
84dfa9a8a5 | |||
8e25489cca | |||
198f95e1ca | |||
7e02fe89ea | |||
819356412c | |||
deb789bc1b | |||
133ba74548 | |||
1461e32643 | |||
f400c3d9ac | |||
7e595a4f74 | |||
18c9c499b2 | |||
24ae115ed4 | |||
7f345558cd | |||
57177cc910 | |||
cea258bc21 | |||
ed9b1c8ba7 | |||
5a79fd89e9 | |||
42a130db08 | |||
320a8d19de | |||
5721506007 | |||
803e8cb2f4 | |||
98492fd0c0 | |||
0b07178577 | |||
07e545079c | |||
95d64dc5e8 | |||
abe546dcda | |||
e6f367acaf | |||
a9b61853b9 | |||
5afc04a630 | |||
1da4cc2782 | |||
c5ebc89e4f | |||
dfc1719cce | |||
0812259470 | |||
e1476c5840 | |||
e30ea28e3f | |||
4a6d3aab7f | |||
8157146498 | |||
94d23888b1 | |||
737fe9bb4a | |||
0051ed2e73 | |||
e0595957e2 | |||
8d09ff7fdb | |||
04feb66b07 | |||
54b2ac7f24 | |||
12356a35fa | |||
12262304ac | |||
c58f97452e | |||
eb3872f7a6 | |||
9fa178d513 | |||
043b184065 | |||
10559bb894 | |||
d0000d66b2 | |||
b447ac738a | |||
faebfc238c | |||
c28fbd37cc | |||
4b8396959d | |||
b39d510e07 | |||
286dda7f80 | |||
7bda896e2d | |||
ba4feeea87 | |||
6f52eae3c6 | |||
40ea8d56e6 | |||
72e562e8a8 | |||
6fa01bfe19 | |||
0ef59c9b91 | |||
d768d2232b | |||
b44a200731 | |||
016815e0d1 | |||
590534e4a6 | |||
7ea9d4e519 | |||
e0ab09f533 | |||
fbe98f1b16 | |||
d0675b8443 | |||
3ea1ed02ae | |||
ba120b1e0b | |||
acf6995c2d | |||
8306860f90 | |||
65974166be | |||
ee8924f986 | |||
170e575465 | |||
b7d5317b10 | |||
f12e7748c5 | |||
69a2418afc | |||
4924ddd172 | |||
1889b43786 | |||
f2e38a4203 | |||
90a8fac8d4 | |||
04402c5ab9 | |||
f8f710df99 | |||
b8105bb6fb | |||
1d18c898b2 | |||
95e208000f | |||
ecdddef81d | |||
c9b1d329e6 | |||
e68c16c7a4 | |||
585c57fe3a | |||
d04cbac79c | |||
044585ee9b | |||
299478e840 | |||
b2d69be5f8 | |||
dc970bbf3c | |||
8717bd5d5d | |||
5b307a8407 | |||
daef66087d | |||
1ad1cf4460 | |||
c0b9718368 | |||
d684f323b8 | |||
24a1c56fe6 | |||
cdeba4f84e | |||
cafba196cf | |||
493b1b12b3 | |||
5320f88230 | |||
246ec2c3ac | |||
9c9b45aeab | |||
8c5dc43735 | |||
b1e812314f | |||
c14f47a74b | |||
58a5b4a5e5 | |||
1cfc2bf36f | |||
5a56d826d9 | |||
8ad8b55424 | |||
3da1d431db |
5
.gitignore
vendored
@ -214,7 +214,6 @@ gradle-app.setting
|
|||||||
|
|
||||||
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
|
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
|
||||||
|
|
||||||
secrets.xml
|
release/
|
||||||
|
|
||||||
mipmap-*
|
crowdin.properties
|
||||||
release/
|
|
162
CHANGELOG.md
@ -1,162 +0,0 @@
|
|||||||
**1.5.1**
|
|
||||||
|
|
||||||
- Added a drawer for filtering sources and tags.
|
|
||||||
|
|
||||||
- You can now search for items from the toolbar.
|
|
||||||
|
|
||||||
**1.5.0.2**
|
|
||||||
|
|
||||||
- If the content in the article viewer is empty, the article will open in a custom tab.
|
|
||||||
|
|
||||||
- Added a share button, and an "open in browser" button to the bottom of the article viewer.
|
|
||||||
|
|
||||||
- Updated custom tab code.
|
|
||||||
|
|
||||||
**1.5.0.1**
|
|
||||||
|
|
||||||
- The release APK wasn't working at all.
|
|
||||||
|
|
||||||
**1.5.0.0**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- The app is now open source ! And rewritten in Kotlin !
|
|
||||||
|
|
||||||
**1.4.0.9**
|
|
||||||
|
|
||||||
_Fixes_
|
|
||||||
|
|
||||||
- Fixes and missing translations.
|
|
||||||
|
|
||||||
**1.4.0.8**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added setting for full height and fixed height cards size.
|
|
||||||
|
|
||||||
_Fixed_
|
|
||||||
|
|
||||||
- Action Bar color now matches the primary color on the recent apps screen.
|
|
||||||
|
|
||||||
- Added a bottom margin to de article viewer content
|
|
||||||
|
|
||||||
- Multiple fixes for the new article viewer.
|
|
||||||
|
|
||||||
**1.4.0.7**
|
|
||||||
|
|
||||||
_Fixed_
|
|
||||||
|
|
||||||
- Disable swipe to hide from other "tabs" and avoid badges problems
|
|
||||||
|
|
||||||
- Fixed a bug with the new Article viewer with some displaying fixes
|
|
||||||
|
|
||||||
|
|
||||||
**1.4.0.6**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added the ability to use http authentication (Basic and Digest)
|
|
||||||
|
|
||||||
_Fixed_
|
|
||||||
|
|
||||||
- Fixed gitter link
|
|
||||||
|
|
||||||
- Change the article viewer because the other was causing crashes
|
|
||||||
|
|
||||||
**1.4.0.5**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added an intro to the app.
|
|
||||||
|
|
||||||
- Added the ability to test the app without a Selfoss instance.
|
|
||||||
|
|
||||||
**1.4.0.4**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added the ability to have a github build. If the apk is a Github build, check for update and ask the user to download it (directly from the github page).
|
|
||||||
|
|
||||||
_Changes_
|
|
||||||
|
|
||||||
- The apk stating that the app wasn't installed from the store is only displayed on start.
|
|
||||||
|
|
||||||
**1.4.0.3**
|
|
||||||
|
|
||||||
_Fixed_
|
|
||||||
|
|
||||||
- Fixed boolean problem.
|
|
||||||
|
|
||||||
**1.4.0.2**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- The app is available in Dutch !
|
|
||||||
|
|
||||||
_Fixed_
|
|
||||||
|
|
||||||
- Fixed a bug with the articles states.
|
|
||||||
|
|
||||||
**1.4.0.1**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- You can now help me translate the app ! There will be a dialog displayed the first time you open the app, and the link will still be available from the settings page.
|
|
||||||
|
|
||||||
_Changes_
|
|
||||||
|
|
||||||
- Changed the custom tabs color to dark orange to fix the wrong title color.
|
|
||||||
|
|
||||||
_Fixes_
|
|
||||||
|
|
||||||
- The badges now are shown even if the tab is selected.
|
|
||||||
|
|
||||||
- Fixed feeds not reloading on app resume (caused by 1.4.0.0 changes).
|
|
||||||
|
|
||||||
**1.4.0.0**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added a setting to enable/disable the article viewer when the internal browser is enabled.
|
|
||||||
|
|
||||||
- Added peek to the card view.
|
|
||||||
|
|
||||||
- Text drawable if no icon.
|
|
||||||
|
|
||||||
|
|
||||||
_Changes_
|
|
||||||
|
|
||||||
- Changed the external browser setting to internal browser and handled the change on first open.
|
|
||||||
|
|
||||||
- Some text changes.
|
|
||||||
|
|
||||||
- Better animations handling on slow networks.
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
**1.3.3.5**
|
|
||||||
|
|
||||||
_New_
|
|
||||||
|
|
||||||
- Added tab bar badges with settings to display them.
|
|
||||||
|
|
||||||
- Added invites.
|
|
||||||
|
|
||||||
_Fixes_
|
|
||||||
|
|
||||||
- Fixed a typo.
|
|
||||||
|
|
||||||
_Updates_
|
|
||||||
|
|
||||||
- Updated support library to 10.2.0.
|
|
||||||
|
|
||||||
- Updated firebase to 10.2.0.
|
|
||||||
|
|
||||||
- Updated article_viewer to 0.20.1.
|
|
||||||
|
|
||||||
- Updated bottom-bar to 2.1.1.
|
|
||||||
|
|
||||||
|
|
||||||
**1.3.3.4**
|
|
||||||
|
|
||||||
...
|
|
674
LICENSE.md
@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
{one line to give the program's name and a brief idea of what it does.}
|
|
||||||
Copyright (C) {year} {name of author}
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
{project} Copyright (C) {year} {fullname}
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
31
README.md
@ -1,30 +1,3 @@
|
|||||||
# ReaderForSelfoss
|
# Multiplatform version [here](https://gitea.amine-louveau.fr/Amine_L/ReaderForSelfoss-multiplatform)
|
||||||
|
|
||||||
[](https://circleci.com/gh/aminecmi/ReaderforSelfoss/tree/master)
|
# Original moved [here](https://gitea.amine-louveau.fr/Amine_L/ReaderforSelfoss)
|
||||||
|
|
||||||
This is the repo of [Reader For Selfoss](https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss&hl=en).
|
|
||||||
|
|
||||||
It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/)
|
|
||||||
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
You can directly import this project into IntellIJ/Android Studio.
|
|
||||||
|
|
||||||
You'll have to:
|
|
||||||
|
|
||||||
- [Create your own launcher icon](https://developer.android.com/studio/write/image-asset-studio.html#creating-launcher)
|
|
||||||
|
|
||||||
- Configure Fabric, or [remove it](https://docs.fabric.io/android/fabric/settings/removing.html#).
|
|
||||||
- Define the following in `res/values/strings.xml` or create `res/values/secrets.xml`
|
|
||||||
|
|
||||||
- mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser
|
|
||||||
- feedback_email: An email to receive users feedback.
|
|
||||||
- source_url: an url to the source code, used in the settings
|
|
||||||
- tracker_url: an url to the tracker, used in the settings
|
|
||||||
|
|
||||||
## Useful links
|
|
||||||
|
|
||||||
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
|
||||||
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
|
|
||||||
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
|
||||||
|
1
app/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/build
|
|
140
app/build.gradle
@ -1,140 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'io.fabric.tools:gradle:1.+'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
apply plugin: 'io.fabric'
|
|
||||||
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 25
|
|
||||||
buildToolsVersion "25.0.3"
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "apps.amine.bou.readerforselfoss"
|
|
||||||
minSdkVersion 16
|
|
||||||
targetSdkVersion 25
|
|
||||||
versionCode 1510
|
|
||||||
versionName "1.5.1"
|
|
||||||
|
|
||||||
// Enabling multidex support.
|
|
||||||
multiDexEnabled true
|
|
||||||
lintOptions {
|
|
||||||
abortOnError true
|
|
||||||
disable 'InvalidPackage'
|
|
||||||
}
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
|
||||||
'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flavorDimensions "build"
|
|
||||||
productFlavors {
|
|
||||||
githubConfig {
|
|
||||||
versionNameSuffix '-github'
|
|
||||||
dimension "build"
|
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "true"
|
|
||||||
}
|
|
||||||
storeConfig {
|
|
||||||
versionNameSuffix '-store'
|
|
||||||
dimension "build"
|
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "false"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
|
||||||
|
|
||||||
// Android Support
|
|
||||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
|
||||||
compile 'com.android.support:design:25.3.1'
|
|
||||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
|
||||||
compile 'com.android.support:support-v4:25.3.1'
|
|
||||||
compile 'com.android.support:support-vector-drawable:25.3.1'
|
|
||||||
compile 'com.android.support:customtabs:25.3.1'
|
|
||||||
compile 'com.android.support:cardview-v7:25.3.1'
|
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
|
||||||
|
|
||||||
// Firebase + crashlytics
|
|
||||||
compile 'com.google.firebase:firebase-core:10.2.6'
|
|
||||||
compile 'com.google.firebase:firebase-config:10.2.6'
|
|
||||||
compile 'com.google.firebase:firebase-invites:10.2.6'
|
|
||||||
compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//multidex
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
|
||||||
|
|
||||||
// Intro
|
|
||||||
compile 'agency.tango.android:material-intro-screen:0.0.5'
|
|
||||||
|
|
||||||
// About
|
|
||||||
compile('com.mikepenz:aboutlibraries:5.9.6@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrofit + http logging + okhttp
|
|
||||||
compile 'com.squareup.retrofit2:retrofit:2.3.0'
|
|
||||||
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
|
|
||||||
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
|
|
||||||
compile 'com.burgstaller:okhttp-digest:1.12'
|
|
||||||
|
|
||||||
// Material-ish things
|
|
||||||
compile 'com.roughike:bottom-bar:2.3.1'
|
|
||||||
compile 'com.melnykov:floatingactionbutton:1.3.0'
|
|
||||||
compile 'com.github.jd-alexander:LikeButton:0.2.1'
|
|
||||||
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
|
||||||
compile 'org.sufficientlysecure:html-textview:3.3'
|
|
||||||
|
|
||||||
// glide
|
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
|
||||||
|
|
||||||
// Asking politely users to rate the app
|
|
||||||
compile 'com.github.stkent:amplify:1.5.0'
|
|
||||||
|
|
||||||
// For the article reader
|
|
||||||
compile 'com.klinkerapps:drag-dismiss-activity:1.4.0'
|
|
||||||
|
|
||||||
// Drawer
|
|
||||||
compile('com.mikepenz:materialdrawer:5.9.2@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
compile 'com.anupcowkur:reservoir:3.1.0'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
initFabricPropertiesIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
def initFabricPropertiesIfNeeded() {
|
|
||||||
def propertiesFile = file('fabric.properties')
|
|
||||||
if (!propertiesFile.exists()) {
|
|
||||||
def commentMessage = "This is autogenerated fabric property from system environment to prevent key to be committed to source control."
|
|
||||||
ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
|
|
||||||
entry(key: "apiSecret", value: crashlyticsdemoApisecret)
|
|
||||||
entry(key: "apiKey", value: crashlyticsdemoApikey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
59
app/proguard-rules.pro
vendored
@ -1,59 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /home/amine/apps/android-sdk-linux/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
#About libraries
|
|
||||||
-keep class .R
|
|
||||||
-keep class **.R$* {
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
##Retrofit
|
|
||||||
#-keep class com.google.gson.** { *; }
|
|
||||||
#-keep class com.google.inject.** { *; }
|
|
||||||
#-keep class org.apache.http.** { *; }
|
|
||||||
#-keep class org.apache.james.mime4j.** { *; }
|
|
||||||
#-keep class javax.inject.** { *; }
|
|
||||||
#-keep class retrofit.** { *; }
|
|
||||||
#-keepclassmembernames interface * {
|
|
||||||
# @retrofit.http.* <methods>;
|
|
||||||
#}
|
|
||||||
#-keep class retrofit.** { *; }
|
|
||||||
#-keep class apps.amine.bou.readerforselfoss.api.selfoss.model.** { *; }
|
|
||||||
#-keepclassmembernames interface * {
|
|
||||||
# @retrofit.http.* <methods>;
|
|
||||||
#}
|
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn retrofit2.Platform$Java8
|
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes Exceptions
|
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn javax.annotation.Nullable
|
|
||||||
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
|
||||||
|
|
||||||
|
|
||||||
#Bottom bar lib
|
|
||||||
-dontwarn com.roughike.bottombar.**
|
|
@ -1,70 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="apps.amine.bou.readerforselfoss">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<!-- For firebase only -->
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name=".MyApp"
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:theme="@style/SplashTheme">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".IntroActivity"
|
|
||||||
android:theme="@style/Theme.Intro">
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".LoginActivity"
|
|
||||||
android:label="@string/title_activity_login">
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".HomeActivity"
|
|
||||||
android:theme="@style/NoBar">
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsActivity"
|
|
||||||
android:label="@string/title_activity_settings"
|
|
||||||
android:parentActivityName=".HomeActivity">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".SourcesActivity"
|
|
||||||
android:parentActivityName=".HomeActivity">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".HomeActivity" />
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".AddSourceActivity"
|
|
||||||
android:parentActivityName=".SourcesActivity">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".SourcesActivity" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".ReaderActivity"
|
|
||||||
android:theme="@style/DragDismissTheme">
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
@ -1,122 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.constraint.ConstraintLayout
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.*
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
|
|
||||||
class AddSourceActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private var mSpoutsValue: String? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_add_source)
|
|
||||||
|
|
||||||
val mProgress = findViewById(R.id.progress) as ProgressBar
|
|
||||||
val mForm = findViewById(R.id.formContainer) as ConstraintLayout
|
|
||||||
val mNameInput = findViewById(R.id.nameInput) as EditText
|
|
||||||
val mSourceUri = findViewById(R.id.sourceUri) as EditText
|
|
||||||
val mTags = findViewById(R.id.tags) as EditText
|
|
||||||
val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner
|
|
||||||
val mSaveBtn = findViewById(R.id.saveBtn) as Button
|
|
||||||
val api = SelfossApi(this)
|
|
||||||
|
|
||||||
|
|
||||||
val intent = intent
|
|
||||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
|
||||||
mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
|
||||||
mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
|
||||||
}
|
|
||||||
|
|
||||||
mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api) }
|
|
||||||
|
|
||||||
|
|
||||||
val spoutsKV = HashMap<String, String>()
|
|
||||||
mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
|
||||||
val spoutName = (view as TextView).text.toString()
|
|
||||||
mSpoutsValue = spoutsKV[spoutName]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
|
||||||
mSpoutsValue = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = Config(this)
|
|
||||||
|
|
||||||
if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) {
|
|
||||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
|
||||||
val i = Intent(this, LoginActivity::class.java)
|
|
||||||
startActivity(i)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
|
|
||||||
var items: Map<String, Spout>
|
|
||||||
api.spouts().enqueue(object : Callback<Map<String, Spout>> {
|
|
||||||
override fun onResponse(call: Call<Map<String, Spout>>, response: Response<Map<String, Spout>>) {
|
|
||||||
if (response.body() != null) {
|
|
||||||
items = response.body()!!
|
|
||||||
|
|
||||||
val itemsStrings = items.map { it.value.name }
|
|
||||||
for ((key, value) in items) {
|
|
||||||
spoutsKV.put(value.name, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgress.visibility = View.GONE
|
|
||||||
mForm.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val spinnerArrayAdapter = ArrayAdapter(this@AddSourceActivity, android.R.layout.simple_spinner_item, itemsStrings)
|
|
||||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
mSpoutsSpinner.adapter = spinnerArrayAdapter
|
|
||||||
|
|
||||||
} else {
|
|
||||||
handleProblemWithSpouts()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
|
||||||
handleProblemWithSpouts()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleProblemWithSpouts() {
|
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
|
|
||||||
mProgress.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
|
|
||||||
|
|
||||||
if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
api.createSource(title, url, mSpoutsValue!!, mTags.text.toString(), "").enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,702 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.support.design.widget.CoordinatorLayout
|
|
||||||
import android.support.v4.view.MenuItemCompat
|
|
||||||
import android.support.v4.widget.SwipeRefreshLayout
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.*
|
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import apps.amine.bou.readerforselfoss.adapters.ItemCardAdapter
|
|
||||||
import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.*
|
|
||||||
import apps.amine.bou.readerforselfoss.settings.SettingsActivity
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.checkApkVersion
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.drawer.CustomUrlPrimaryDrawerItem
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.longHash
|
|
||||||
import com.anupcowkur.reservoir.Reservoir
|
|
||||||
import com.anupcowkur.reservoir.ReservoirGetCallback
|
|
||||||
import com.anupcowkur.reservoir.ReservoirPutCallback
|
|
||||||
import com.crashlytics.android.answers.Answers
|
|
||||||
import com.crashlytics.android.answers.InviteEvent
|
|
||||||
import com.github.stkent.amplify.prompt.DefaultLayoutPromptView
|
|
||||||
import com.github.stkent.amplify.tracking.Amplify
|
|
||||||
import com.google.android.gms.appinvite.AppInviteInvitation
|
|
||||||
import com.google.android.gms.common.ConnectionResult
|
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.mikepenz.aboutlibraries.Libs
|
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
|
||||||
import com.mikepenz.materialdrawer.Drawer
|
|
||||||
import com.mikepenz.materialdrawer.DrawerBuilder
|
|
||||||
import com.mikepenz.materialdrawer.holder.BadgeStyle
|
|
||||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem
|
|
||||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
|
||||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
|
|
||||||
import com.roughike.bottombar.BottomBar
|
|
||||||
import com.roughike.bottombar.BottomBarTab
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
private val MENU_PREFERENCES = 12302
|
|
||||||
private val REQUEST_INVITE = 13231
|
|
||||||
private val REQUEST_INVITE_BYMAIL = 13232
|
|
||||||
private val DRAWER_ID_TAGS = 100101L
|
|
||||||
private val DRAWER_ID_SOURCES = 100110L
|
|
||||||
private val DRAWER_ID_FILTERS = 100111L
|
|
||||||
private var mRecyclerView: RecyclerView? = null
|
|
||||||
private var api: SelfossApi? = null
|
|
||||||
private var items: ArrayList<Item> = ArrayList()
|
|
||||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
|
||||||
|
|
||||||
private var clickBehavior = false
|
|
||||||
private var internalBrowser = false
|
|
||||||
private var articleViewer = false
|
|
||||||
private var shouldBeCardView = false
|
|
||||||
private var displayUnreadCount = false
|
|
||||||
private var displayAllCount = false
|
|
||||||
private var editor: SharedPreferences.Editor? = null
|
|
||||||
|
|
||||||
private val UNREAD_SHOWN = 1
|
|
||||||
private val READ_SHOWN = 2
|
|
||||||
private val FAV_SHOWN = 3
|
|
||||||
private var elementsShown: Int = 0
|
|
||||||
private var mBottomBar: BottomBar? = null
|
|
||||||
private var mCoordinatorLayout: CoordinatorLayout? = null
|
|
||||||
private var mSwipeRefreshLayout: SwipeRefreshLayout? = null
|
|
||||||
private var sharedPref: SharedPreferences? = null
|
|
||||||
private var tabNew: BottomBarTab? = null
|
|
||||||
private var tabArchive: BottomBarTab? = null
|
|
||||||
private var tabStarred: BottomBarTab? = null
|
|
||||||
private var mFirebaseRemoteConfig: FirebaseRemoteConfig? = null
|
|
||||||
private var fullHeightCards: Boolean = false
|
|
||||||
private var toolbar: Toolbar? = null
|
|
||||||
private var drawer: Drawer? = null
|
|
||||||
private var maybeTagFilter: Tag? = null
|
|
||||||
private var maybeSourceFilter: Sources? = null
|
|
||||||
private var maybeSearchFilter: String? = null
|
|
||||||
|
|
||||||
data class DrawerData(val tags: List<Tag>?, val sources: List<Sources>?)
|
|
||||||
|
|
||||||
private fun handleSharedPrefs() {
|
|
||||||
clickBehavior = this.sharedPref!!.getBoolean("tab_on_tap", false)
|
|
||||||
internalBrowser = this.sharedPref!!.getBoolean("prefer_internal_browser", true)
|
|
||||||
articleViewer = this.sharedPref!!.getBoolean("prefer_article_viewer", true)
|
|
||||||
shouldBeCardView = this.sharedPref!!.getBoolean("card_view_active", false)
|
|
||||||
displayUnreadCount = this.sharedPref!!.getBoolean("display_unread_count", true)
|
|
||||||
displayAllCount = this.sharedPref!!.getBoolean("display_other_count", false)
|
|
||||||
fullHeightCards = this.sharedPref!!.getBoolean("full_height_cards", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
handleDrawerItems()
|
|
||||||
|
|
||||||
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
|
||||||
editor = settings.edit()
|
|
||||||
|
|
||||||
if (BuildConfig.GITHUB_VERSION) {
|
|
||||||
checkApkVersion(settings, editor!!, this@HomeActivity, mFirebaseRemoteConfig!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSharedPrefs()
|
|
||||||
|
|
||||||
tabNew = mBottomBar!!.getTabWithId(R.id.tab_new)
|
|
||||||
tabArchive = mBottomBar!!.getTabWithId(R.id.tab_archive)
|
|
||||||
tabStarred = mBottomBar!!.getTabWithId(R.id.tab_fav)
|
|
||||||
|
|
||||||
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleDrawer() {
|
|
||||||
|
|
||||||
drawer = DrawerBuilder()
|
|
||||||
.withActivity(this)
|
|
||||||
.withRootView(R.id.drawer_layout)
|
|
||||||
.withToolbar(toolbar!!)
|
|
||||||
.withActionBarDrawerToggle(true)
|
|
||||||
.withActionBarDrawerToggleAnimated(true)
|
|
||||||
.withShowDrawerOnFirstLaunch(true)
|
|
||||||
.withOnDrawerListener(object: Drawer.OnDrawerListener {
|
|
||||||
override fun onDrawerSlide(p0: View?, p1: Float) {
|
|
||||||
mBottomBar!!.alpha = (1 - p1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerClosed(p0: View?) {
|
|
||||||
mBottomBar!!.shySettings.showBar()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawerOpened(p0: View?) {
|
|
||||||
mBottomBar!!.shySettings.hideBar()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
|
|
||||||
drawer!!.addStickyFooterItem(
|
|
||||||
PrimaryDrawerItem()
|
|
||||||
.withName(R.string.action_about)
|
|
||||||
.withSelectable(false)
|
|
||||||
.withIcon(R.drawable.ic_info_outline)
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
LibsBuilder()
|
|
||||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
|
||||||
.withAboutIconShown(true)
|
|
||||||
.withAboutVersionShown(true)
|
|
||||||
.start(this@HomeActivity)
|
|
||||||
false
|
|
||||||
})
|
|
||||||
drawer!!.addStickyFooterItem(
|
|
||||||
PrimaryDrawerItem()
|
|
||||||
.withName(R.string.title_activity_settings)
|
|
||||||
.withIcon(R.drawable.ic_settings)
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
startActivityForResult(Intent(this@HomeActivity, SettingsActivity::class.java), MENU_PREFERENCES)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleDrawerItems() {
|
|
||||||
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
|
|
||||||
fun handleTags(maybeTags: List<Tag>?) {
|
|
||||||
if (maybeTags == null) {
|
|
||||||
if (loadedFromCache)
|
|
||||||
drawer!!.addItem(SecondaryDrawerItem().withName(getString(R.string.drawer_error_loading_tags)).withSelectable(false))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (tag in maybeTags) {
|
|
||||||
val gd: GradientDrawable = GradientDrawable()
|
|
||||||
gd.setColor(Color.parseColor(tag.color))
|
|
||||||
gd.shape = GradientDrawable.RECTANGLE
|
|
||||||
gd.setSize(30, 30)
|
|
||||||
gd.cornerRadius = 30F
|
|
||||||
drawer!!.addItem(
|
|
||||||
PrimaryDrawerItem()
|
|
||||||
.withName(tag.tag)
|
|
||||||
.withIdentifier(longHash(tag.tag))
|
|
||||||
.withIcon(gd)
|
|
||||||
.withBadge("${tag.unread}")
|
|
||||||
.withBadgeStyle(
|
|
||||||
BadgeStyle().withTextColor(Color.WHITE)
|
|
||||||
.withColorRes(R.color.colorAccent)
|
|
||||||
)
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
maybeTagFilter = tag
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleSources(maybeSources: List<Sources>?) {
|
|
||||||
if (maybeSources == null) {
|
|
||||||
if (loadedFromCache)
|
|
||||||
drawer!!.addItem(SecondaryDrawerItem().withName(getString(R.string.drawer_error_loading_sources)).withSelectable(false))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
for (tag in maybeSources)
|
|
||||||
drawer!!.addItem(
|
|
||||||
CustomUrlPrimaryDrawerItem()
|
|
||||||
.withName(tag.title)
|
|
||||||
.withIdentifier(tag.id.toLong())
|
|
||||||
.withIcon(tag.getIcon(this@HomeActivity))
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
maybeSourceFilter = tag
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
drawer!!.removeAllItems()
|
|
||||||
if (maybeDrawerData != null) {
|
|
||||||
drawer!!.addItem(
|
|
||||||
SecondaryDrawerItem()
|
|
||||||
.withName(getString(R.string.drawer_item_filters))
|
|
||||||
.withSelectable(false)
|
|
||||||
.withIdentifier(DRAWER_ID_FILTERS)
|
|
||||||
.withBadge(getString(R.string.drawer_action_clear))
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
maybeSourceFilter = null
|
|
||||||
maybeTagFilter = null
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
drawer!!.addItem(DividerDrawerItem())
|
|
||||||
drawer!!.addItem(SecondaryDrawerItem().withName(getString(R.string.drawer_item_tags)).withIdentifier(DRAWER_ID_TAGS).withSelectable(false))
|
|
||||||
handleTags(maybeDrawerData.tags)
|
|
||||||
drawer!!.addItem(
|
|
||||||
SecondaryDrawerItem()
|
|
||||||
.withName(getString(R.string.drawer_item_sources))
|
|
||||||
.withIdentifier(DRAWER_ID_TAGS)
|
|
||||||
.withBadge(getString(R.string.drawer_action_edit))
|
|
||||||
.withSelectable(false)
|
|
||||||
.withOnDrawerItemClickListener { _, _, _ ->
|
|
||||||
startActivity(Intent(this, SourcesActivity::class.java))
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
handleSources(maybeDrawerData.sources)
|
|
||||||
|
|
||||||
|
|
||||||
if (!loadedFromCache)
|
|
||||||
Reservoir.putAsync("drawerData", maybeDrawerData, object : ReservoirPutCallback {
|
|
||||||
override fun onSuccess() {}
|
|
||||||
|
|
||||||
override fun onFailure(p0: Exception?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (!loadedFromCache) {
|
|
||||||
drawer!!.addItem(PrimaryDrawerItem().withName(getString(R.string.no_tags_loaded)).withIdentifier(DRAWER_ID_TAGS).withSelectable(false))
|
|
||||||
drawer!!.addItem(PrimaryDrawerItem().withName(getString(R.string.no_sources_loaded)).withIdentifier(DRAWER_ID_SOURCES).withSelectable(false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun drawerApiCalls(maybeDrawerData: DrawerData?) {
|
|
||||||
var tags: List<Tag>? = null
|
|
||||||
var sources: List<Sources>?
|
|
||||||
|
|
||||||
fun sourcesApiCall() {
|
|
||||||
api!!.sources.enqueue(object: Callback<List<Sources>> {
|
|
||||||
override fun onResponse(call: Call<List<Sources>>?, response: Response<List<Sources>>) {
|
|
||||||
sources = response.body()
|
|
||||||
val apiDrawerData = DrawerData(tags, sources)
|
|
||||||
if (maybeDrawerData == null || (maybeDrawerData != null && maybeDrawerData != apiDrawerData))
|
|
||||||
handleDrawerData(apiDrawerData)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Sources>>?, t: Throwable?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api!!.tags.enqueue(object: Callback<List<Tag>> {
|
|
||||||
override fun onResponse(call: Call<List<Tag>>, response: Response<List<Tag>>) {
|
|
||||||
tags = response.body()
|
|
||||||
sourcesApiCall()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Tag>>?, t: Throwable?) {
|
|
||||||
sourcesApiCall()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
drawer!!.addItem(PrimaryDrawerItem().withName(getString(R.string.drawer_loading)).withSelectable(false))
|
|
||||||
|
|
||||||
val resultType = object : TypeToken<DrawerData>() {}.type
|
|
||||||
Reservoir.getAsync("drawerData", resultType, object: ReservoirGetCallback<DrawerData> {
|
|
||||||
override fun onSuccess(maybeDrawerData: DrawerData?) {
|
|
||||||
handleDrawerData(maybeDrawerData, loadedFromCache = true)
|
|
||||||
drawerApiCalls(maybeDrawerData)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(p0: Exception?) {
|
|
||||||
drawerApiCalls(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_home)
|
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar) as Toolbar?
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
val promptView = findViewById(R.id.prompt_view) as DefaultLayoutPromptView
|
|
||||||
Amplify.getSharedInstance().promptIfReady(promptView)
|
|
||||||
}
|
|
||||||
|
|
||||||
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
|
|
||||||
mFirebaseRemoteConfig!!.setDefaults(R.xml.default_remote_config)
|
|
||||||
|
|
||||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
|
||||||
|
|
||||||
api = SelfossApi(this)
|
|
||||||
items = ArrayList()
|
|
||||||
|
|
||||||
mBottomBar = findViewById(R.id.bottomBar) as BottomBar
|
|
||||||
|
|
||||||
handleDrawer()
|
|
||||||
|
|
||||||
// TODO: clean this hack
|
|
||||||
val listenerAlreadySet = booleanArrayOf(false)
|
|
||||||
mBottomBar!!.setOnTabSelectListener { tabId ->
|
|
||||||
if (listenerAlreadySet[0]) {
|
|
||||||
if (tabId == R.id.tab_new) {
|
|
||||||
getUnRead()
|
|
||||||
} else if (tabId == R.id.tab_archive) {
|
|
||||||
getRead()
|
|
||||||
} else if (tabId == R.id.tab_fav) {
|
|
||||||
getStarred()
|
|
||||||
}
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
} else {
|
|
||||||
listenerAlreadySet[0] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mCoordinatorLayout = findViewById(R.id.coordLayout) as CoordinatorLayout
|
|
||||||
mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout
|
|
||||||
mRecyclerView = findViewById(R.id.my_recycler_view) as RecyclerView
|
|
||||||
|
|
||||||
reloadLayoutManager()
|
|
||||||
|
|
||||||
mSwipeRefreshLayout!!.setColorSchemeResources(
|
|
||||||
R.color.refresh_progress_1,
|
|
||||||
R.color.refresh_progress_2,
|
|
||||||
R.color.refresh_progress_3)
|
|
||||||
mSwipeRefreshLayout!!.setOnRefreshListener { getElementsAccordingToTab() }
|
|
||||||
|
|
||||||
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
|
||||||
|
|
||||||
override fun getSwipeDirs(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
|
|
||||||
if (elementsShown != UNREAD_SHOWN) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return super.getSwipeDirs(recyclerView, viewHolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
|
|
||||||
try {
|
|
||||||
val i = items[viewHolder.adapterPosition]
|
|
||||||
val position = items.indexOf(i)
|
|
||||||
|
|
||||||
if (shouldBeCardView) {
|
|
||||||
(mRecyclerView!!.adapter as ItemCardAdapter).removeItemAtIndex(position)
|
|
||||||
} else {
|
|
||||||
(mRecyclerView!!.adapter as ItemListAdapter).removeItemAtIndex(position)
|
|
||||||
}
|
|
||||||
tabNew!!.setBadgeCount(items.size - 1)
|
|
||||||
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
|
|
||||||
itemTouchHelper.attachToRecyclerView(mRecyclerView)
|
|
||||||
|
|
||||||
|
|
||||||
checkAndDisplayStoreApk(this@HomeActivity)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reloadLayoutManager() {
|
|
||||||
val mLayoutManager: RecyclerView.LayoutManager
|
|
||||||
if (shouldBeCardView) {
|
|
||||||
mLayoutManager = StaggeredGridLayoutManager(calculateNoOfColumns(), StaggeredGridLayoutManager.VERTICAL)
|
|
||||||
mLayoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
|
||||||
} else {
|
|
||||||
mLayoutManager = GridLayoutManager(this, calculateNoOfColumns())
|
|
||||||
}
|
|
||||||
|
|
||||||
mRecyclerView!!.layoutManager = mLayoutManager
|
|
||||||
mRecyclerView!!.setHasFixedSize(true)
|
|
||||||
|
|
||||||
mBottomBar!!.setOnTabReselectListener {
|
|
||||||
if (shouldBeCardView) {
|
|
||||||
if ((mLayoutManager as StaggeredGridLayoutManager).findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
} else {
|
|
||||||
mLayoutManager.scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((mLayoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) {
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
} else {
|
|
||||||
mLayoutManager.scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getElementsAccordingToTab() {
|
|
||||||
|
|
||||||
when (elementsShown) {
|
|
||||||
UNREAD_SHOWN -> getUnRead()
|
|
||||||
READ_SHOWN -> getRead()
|
|
||||||
FAV_SHOWN -> getStarred()
|
|
||||||
else -> getUnRead()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUnRead() {
|
|
||||||
elementsShown = UNREAD_SHOWN
|
|
||||||
api!!.unreadItems(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter).enqueue(object : Callback<List<Item>> {
|
|
||||||
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
|
|
||||||
handleItemsResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.cant_get_new_elements, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleItemsResponse(response: Response<List<Item>>) {
|
|
||||||
val didUpdate = (response.body() != items)
|
|
||||||
if (response.body() != null) {
|
|
||||||
if (response.body() != items) {
|
|
||||||
items = response.body() as ArrayList<Item>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items = ArrayList()
|
|
||||||
}
|
|
||||||
if (didUpdate)
|
|
||||||
handleListResult()
|
|
||||||
if (items.isEmpty()) Toast.makeText(this@HomeActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRead() {
|
|
||||||
elementsShown = READ_SHOWN
|
|
||||||
api!!.readItems(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter).enqueue(object : Callback<List<Item>> {
|
|
||||||
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
|
|
||||||
handleItemsResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.cant_get_read, Toast.LENGTH_SHORT).show()
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStarred() {
|
|
||||||
elementsShown = FAV_SHOWN
|
|
||||||
api!!.starredItems(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter).enqueue(object : Callback<List<Item>> {
|
|
||||||
override fun onResponse(call: Call<List<Item>>, response: Response<List<Item>>) {
|
|
||||||
handleItemsResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.cant_get_favs, Toast.LENGTH_SHORT).show()
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleListResult() {
|
|
||||||
reloadLayoutManager()
|
|
||||||
|
|
||||||
val mAdapter: RecyclerView.Adapter<*>
|
|
||||||
if (shouldBeCardView) {
|
|
||||||
mAdapter = ItemCardAdapter(this, items, api!!, mCustomTabActivityHelper!!, internalBrowser, articleViewer, fullHeightCards)
|
|
||||||
} else {
|
|
||||||
mAdapter = ItemListAdapter(this, items, api!!, mCustomTabActivityHelper!!, clickBehavior, internalBrowser, articleViewer)
|
|
||||||
}
|
|
||||||
mRecyclerView!!.adapter = mAdapter
|
|
||||||
mAdapter.notifyDataSetChanged()
|
|
||||||
|
|
||||||
reloadBadges()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
val inflater = menuInflater
|
|
||||||
inflater.inflate(R.menu.home_menu, menu)
|
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.action_search)
|
|
||||||
val searchView = MenuItemCompat.getActionView(searchItem) as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.refresh -> {
|
|
||||||
api!!.update().enqueue(object : Callback<String> {
|
|
||||||
override fun onResponse(call: Call<String>, response: Response<String>) {
|
|
||||||
Toast.makeText(this@HomeActivity,
|
|
||||||
R.string.refresh_success_response, Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<String>, t: Throwable) {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.refresh_failer_message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.readAll -> {
|
|
||||||
if (elementsShown == UNREAD_SHOWN) {
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
val ids = items.map { it.id }
|
|
||||||
|
|
||||||
api!!.readAll(ids).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.all_posts_read, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show()
|
|
||||||
mSwipeRefreshLayout!!.isRefreshing = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
items = ArrayList()
|
|
||||||
if (items.isEmpty()) Toast.makeText(this@HomeActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
|
|
||||||
handleListResult()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.action_disconnect -> {
|
|
||||||
editor!!.remove("url")
|
|
||||||
editor!!.remove("login")
|
|
||||||
editor!!.remove("password")
|
|
||||||
editor!!.apply()
|
|
||||||
val intent = Intent(this, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.action_share_the_app -> {
|
|
||||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
|
|
||||||
val share = AppInviteInvitation.IntentBuilder(getString(R.string.invitation_title))
|
|
||||||
.setMessage(getString(R.string.invitation_message))
|
|
||||||
.setDeepLink(Uri.parse("https://ymbh5.app.goo.gl/qbvQ"))
|
|
||||||
.setCallToActionText(getString(R.string.invitation_cta))
|
|
||||||
.build()
|
|
||||||
startActivityForResult(share, REQUEST_INVITE)
|
|
||||||
} else {
|
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.invitation_message) + " https://ymbh5.app.goo.gl/qbvQ")
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
startActivityForResult(sendIntent, REQUEST_INVITE_BYMAIL)
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadBadges() {
|
|
||||||
if (displayUnreadCount || displayAllCount) {
|
|
||||||
api!!.stats.enqueue(object : Callback<Stats> {
|
|
||||||
override fun onResponse(call: Call<Stats>, response: Response<Stats>) {
|
|
||||||
if (response.body() != null) {
|
|
||||||
tabNew!!.setBadgeCount(response.body()!!.unread)
|
|
||||||
if (displayAllCount) {
|
|
||||||
tabArchive!!.setBadgeCount(response.body()!!.total)
|
|
||||||
tabStarred!!.setBadgeCount(response.body()!!.starred)
|
|
||||||
} else {
|
|
||||||
tabArchive!!.removeBadge()
|
|
||||||
tabStarred!!.removeBadge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Stats>, t: Throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tabNew!!.removeBadge()
|
|
||||||
tabArchive!!.removeBadge()
|
|
||||||
tabStarred!!.removeBadge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(req: Int, result: Int, data: Intent?) {
|
|
||||||
when (req) {
|
|
||||||
MENU_PREFERENCES -> {
|
|
||||||
drawer!!.closeDrawer()
|
|
||||||
recreate()
|
|
||||||
}
|
|
||||||
REQUEST_INVITE -> if (result == Activity.RESULT_OK) {
|
|
||||||
Answers.getInstance().logInvite(InviteEvent())
|
|
||||||
}
|
|
||||||
REQUEST_INVITE_BYMAIL -> {
|
|
||||||
Answers.getInstance().logInvite(InviteEvent())
|
|
||||||
super.onActivityResult(req, result, data)
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(req, result, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun calculateNoOfColumns(): Int {
|
|
||||||
val displayMetrics = resources.displayMetrics
|
|
||||||
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
|
||||||
return (dpWidth / 300).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(p0: String?): Boolean {
|
|
||||||
if (p0.isNullOrBlank()) {
|
|
||||||
maybeSearchFilter = null
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(p0: String?): Boolean {
|
|
||||||
maybeSearchFilter = p0
|
|
||||||
getElementsAccordingToTab()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import agency.tango.materialintroscreen.MaterialIntroActivity
|
|
||||||
import agency.tango.materialintroscreen.MessageButtonBehaviour
|
|
||||||
import agency.tango.materialintroscreen.SlideFragmentBuilder
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
|
|
||||||
class IntroActivity : MaterialIntroActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
|
||||||
.backgroundColor(R.color.colorPrimary)
|
|
||||||
.buttonsColor(R.color.colorAccent)
|
|
||||||
.image(R.mipmap.ic_launcher)
|
|
||||||
.title(getString(R.string.intro_hello_title))
|
|
||||||
.description(getString(R.string.intro_hello_message))
|
|
||||||
.build())
|
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
|
||||||
.backgroundColor(R.color.colorAccent)
|
|
||||||
.buttonsColor(R.color.colorPrimary)
|
|
||||||
.image(R.drawable.ic_info_outline_white_48dp)
|
|
||||||
.title(getString(R.string.intro_needs_selfoss_title))
|
|
||||||
.description(getString(R.string.intro_needs_selfoss_message))
|
|
||||||
.build(),
|
|
||||||
MessageButtonBehaviour(View.OnClickListener {
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://selfoss.aditu.de"))
|
|
||||||
startActivity(browserIntent)
|
|
||||||
}, getString(R.string.intro_needs_selfoss_link)))
|
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
|
||||||
.backgroundColor(R.color.colorPrimaryDark)
|
|
||||||
.buttonsColor(R.color.colorAccentDark)
|
|
||||||
.image(R.drawable.ic_thumb_up_white_48dp)
|
|
||||||
.title(getString(R.string.intro_all_set_title))
|
|
||||||
.description(getString(R.string.intro_all_set_message))
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
super.onFinish()
|
|
||||||
val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext)
|
|
||||||
val e = getPrefs.edit()
|
|
||||||
e.putBoolean("firstStart", false)
|
|
||||||
e.apply()
|
|
||||||
val intent = Intent(this, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,263 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.design.widget.TextInputLayout
|
|
||||||
import android.support.v7.app.AlertDialog
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Switch
|
|
||||||
import android.widget.TextView
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.mikepenz.aboutlibraries.Libs
|
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private var settings: SharedPreferences? = null
|
|
||||||
private var mProgressView: View? = null
|
|
||||||
private var mUrlView: EditText? = null
|
|
||||||
private var mLoginView: TextView? = null
|
|
||||||
private var mHTTPLoginView: TextView? = null
|
|
||||||
private var mPasswordView: EditText? = null
|
|
||||||
private var mHTTPPasswordView: EditText? = null
|
|
||||||
private var inValidCount: Int = 0
|
|
||||||
private var isWithLogin = false
|
|
||||||
private var isWithHTTPLogin = false
|
|
||||||
private var mLoginFormView: View? = null
|
|
||||||
private var mFirebaseAnalytics: FirebaseAnalytics? = null
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_login)
|
|
||||||
|
|
||||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
|
||||||
if (settings!!.getString("url", "").isNotEmpty()) {
|
|
||||||
goToMain()
|
|
||||||
} else {
|
|
||||||
checkAndDisplayStoreApk(this@LoginActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
isWithLogin = false
|
|
||||||
isWithHTTPLogin = false
|
|
||||||
inValidCount = 0
|
|
||||||
|
|
||||||
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
|
|
||||||
mUrlView = findViewById(R.id.url) as EditText
|
|
||||||
mLoginView = findViewById(R.id.login) as TextView
|
|
||||||
mHTTPLoginView = findViewById(R.id.httpLogin) as TextView
|
|
||||||
mPasswordView = findViewById(R.id.password) as EditText
|
|
||||||
mHTTPPasswordView = findViewById(R.id.httpPassword) as EditText
|
|
||||||
mLoginFormView = findViewById(R.id.login_form)
|
|
||||||
mProgressView = findViewById(R.id.login_progress)
|
|
||||||
|
|
||||||
val mSwitch = findViewById(R.id.withLogin) as Switch
|
|
||||||
val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch
|
|
||||||
val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout
|
|
||||||
val mHTTPLoginLayout = findViewById(R.id.httpLoginInput) as TextInputLayout
|
|
||||||
val mPasswordLayout = findViewById(R.id.passwordLayout) as TextInputLayout
|
|
||||||
val mHTTPPasswordLayout = findViewById(R.id.httpPasswordInput) as TextInputLayout
|
|
||||||
val mEmailSignInButton = findViewById(R.id.email_sign_in_button) as Button
|
|
||||||
|
|
||||||
mPasswordView!!.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
|
|
||||||
if (id == R.id.login || id == EditorInfo.IME_NULL) {
|
|
||||||
attemptLogin()
|
|
||||||
return@OnEditorActionListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
|
|
||||||
mEmailSignInButton.setOnClickListener { attemptLogin() }
|
|
||||||
|
|
||||||
mSwitch.setOnCheckedChangeListener { _, b ->
|
|
||||||
isWithLogin = !isWithLogin
|
|
||||||
val visi: Int
|
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
|
||||||
visi = View.GONE
|
|
||||||
}
|
|
||||||
mLoginLayout.visibility = visi
|
|
||||||
mPasswordLayout.visibility = visi
|
|
||||||
}
|
|
||||||
|
|
||||||
mHTTPSwitch.setOnCheckedChangeListener { _, b ->
|
|
||||||
isWithHTTPLogin = !isWithHTTPLogin
|
|
||||||
val visi: Int
|
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
|
||||||
visi = View.GONE
|
|
||||||
}
|
|
||||||
mHTTPLoginLayout.visibility = visi
|
|
||||||
mHTTPPasswordLayout.visibility = visi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToMain() {
|
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun attemptLogin() {
|
|
||||||
|
|
||||||
// Reset errors.
|
|
||||||
mUrlView!!.error = null
|
|
||||||
mLoginView!!.error = null
|
|
||||||
mHTTPLoginView!!.error = null
|
|
||||||
mPasswordView!!.error = null
|
|
||||||
mHTTPPasswordView!!.error = null
|
|
||||||
|
|
||||||
// Store values at the time of the login attempt.
|
|
||||||
val url = mUrlView!!.text.toString()
|
|
||||||
val login = mLoginView!!.text.toString()
|
|
||||||
val httpLogin = mHTTPLoginView!!.text.toString()
|
|
||||||
val password = mPasswordView!!.text.toString()
|
|
||||||
val httpPassword = mHTTPPasswordView!!.text.toString()
|
|
||||||
|
|
||||||
var cancel = false
|
|
||||||
var focusView: View? = null
|
|
||||||
|
|
||||||
if (!isUrlValid(url)) {
|
|
||||||
mUrlView!!.error = getString(R.string.login_url_problem)
|
|
||||||
focusView = mUrlView
|
|
||||||
cancel = true
|
|
||||||
inValidCount++
|
|
||||||
if (inValidCount == 3) {
|
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
|
||||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
|
||||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
|
||||||
{ dialog, _ -> dialog.dismiss() })
|
|
||||||
alertDialog.show()
|
|
||||||
inValidCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWithLogin || isWithHTTPLogin) {
|
|
||||||
if (TextUtils.isEmpty(password)) {
|
|
||||||
mPasswordView!!.error = getString(R.string.error_invalid_password)
|
|
||||||
focusView = mPasswordView
|
|
||||||
cancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(login)) {
|
|
||||||
mLoginView!!.error = getString(R.string.error_field_required)
|
|
||||||
focusView = mLoginView
|
|
||||||
cancel = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancel) {
|
|
||||||
focusView!!.requestFocus()
|
|
||||||
} else {
|
|
||||||
showProgress(true)
|
|
||||||
|
|
||||||
val editor = settings!!.edit()
|
|
||||||
editor.putString("url", url)
|
|
||||||
editor.putString("login", login)
|
|
||||||
editor.putString("httpUserName", httpLogin)
|
|
||||||
editor.putString("password", password)
|
|
||||||
editor.putString("httpPassword", httpPassword)
|
|
||||||
editor.apply()
|
|
||||||
|
|
||||||
val api = SelfossApi(this@LoginActivity)
|
|
||||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
|
||||||
private fun preferenceError() {
|
|
||||||
editor.remove("url")
|
|
||||||
editor.remove("login")
|
|
||||||
editor.remove("httpUserName")
|
|
||||||
editor.remove("password")
|
|
||||||
editor.remove("httpPassword")
|
|
||||||
editor.apply()
|
|
||||||
mUrlView!!.error = getString(R.string.wrong_infos)
|
|
||||||
mLoginView!!.error = getString(R.string.wrong_infos)
|
|
||||||
mPasswordView!!.error = getString(R.string.wrong_infos)
|
|
||||||
mHTTPLoginView!!.error = getString(R.string.wrong_infos)
|
|
||||||
mHTTPPasswordView!!.error = getString(R.string.wrong_infos)
|
|
||||||
showProgress(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
|
||||||
mFirebaseAnalytics!!.logEvent(FirebaseAnalytics.Event.LOGIN, Bundle())
|
|
||||||
goToMain()
|
|
||||||
} else {
|
|
||||||
preferenceError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
preferenceError()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress UI and hides the login form.
|
|
||||||
*/
|
|
||||||
private fun showProgress(show: Boolean) {
|
|
||||||
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
|
||||||
|
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
|
||||||
mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
|
||||||
if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
|
||||||
mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
|
||||||
if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
val inflater = menuInflater
|
|
||||||
inflater.inflate(R.menu.login_menu, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.about -> {
|
|
||||||
LibsBuilder()
|
|
||||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
|
||||||
.withAboutIconShown(true)
|
|
||||||
.withAboutVersionShown(true)
|
|
||||||
.start(this)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
|
|
||||||
val i = Intent(this@MainActivity, IntroActivity::class.java)
|
|
||||||
startActivity(i)
|
|
||||||
} else {
|
|
||||||
val intent = Intent(this, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.multidex.MultiDexApplication
|
|
||||||
import android.widget.ImageView
|
|
||||||
import com.crashlytics.android.Crashlytics
|
|
||||||
import com.github.stkent.amplify.tracking.Amplify
|
|
||||||
import io.fabric.sdk.android.Fabric
|
|
||||||
import com.anupcowkur.reservoir.Reservoir
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
|
||||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
|
||||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
class MyApp : MultiDexApplication() {
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
if (!BuildConfig.DEBUG)
|
|
||||||
Fabric.with(this, Crashlytics())
|
|
||||||
|
|
||||||
Amplify.initSharedInstance(this)
|
|
||||||
.setFeedbackEmailAddress(getString(R.string.feedback_email))
|
|
||||||
.setAlwaysShow(BuildConfig.DEBUG)
|
|
||||||
.applyAllDefaultRules()
|
|
||||||
|
|
||||||
try {
|
|
||||||
Reservoir.init(this, 8192) //in bytes
|
|
||||||
} catch (e: IOException) {
|
|
||||||
//failure
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
|
||||||
override fun set(imageView: ImageView?, uri: Uri?, placeholder: Drawable?, tag: String?) {
|
|
||||||
Glide.with(imageView?.context).load(uri).placeholder(placeholder).into(imageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(imageView: ImageView?) {
|
|
||||||
Glide.clear(imageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun placeholder(ctx: Context?, tag: String?): Drawable {
|
|
||||||
return applicationContext.resources.getDrawable(R.mipmap.ic_launcher)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
|
|
||||||
import org.sufficientlysecure.htmltextview.HtmlTextView
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
|
|
||||||
|
|
||||||
|
|
||||||
class ReaderActivity : DragDismissActivity() {
|
|
||||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
|
|
||||||
val v = inflater.inflate(R.layout.activity_reader, parent, false)
|
|
||||||
showProgressBar()
|
|
||||||
|
|
||||||
val image = v.findViewById(R.id.imageView) as ImageView
|
|
||||||
val source = v.findViewById(R.id.source) as TextView
|
|
||||||
val title = v.findViewById(R.id.title) as TextView
|
|
||||||
val content = v.findViewById(R.id.content) as HtmlTextView
|
|
||||||
val url = intent.getStringExtra("url")
|
|
||||||
val parser = MercuryApi(getString(R.string.mercury))
|
|
||||||
val browserBtn: ImageButton = v.findViewById(R.id.browserBtn) as ImageButton
|
|
||||||
val shareBtn: ImageButton = v.findViewById(R.id.shareBtn) as ImageButton
|
|
||||||
|
|
||||||
|
|
||||||
val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity)
|
|
||||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
|
||||||
|
|
||||||
|
|
||||||
parser.parseUrl(url).enqueue(object : Callback<ParsedContent> {
|
|
||||||
override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
|
|
||||||
if (response.body() != null && response.body()!!.content != null && response.body()!!.content.isNotEmpty()) {
|
|
||||||
source.text = response.body()!!.domain
|
|
||||||
title.text = response.body()!!.title
|
|
||||||
if (response.body()!!.content != null && !response.body()!!.content.isEmpty())
|
|
||||||
content.setHtml(response.body()!!.content, HtmlHttpImageGetter(content, null, true))
|
|
||||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isEmpty())
|
|
||||||
Glide.with(applicationContext).load(response.body()!!.lead_image_url).asBitmap().fitCenter().into(image)
|
|
||||||
|
|
||||||
shareBtn.setOnClickListener {
|
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, response.body()!!.url)
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
|
||||||
|
|
||||||
browserBtn.setOnClickListener {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse(response.body()!!.url)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
hideProgressBar()
|
|
||||||
} else {
|
|
||||||
errorAfterMercuryCall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<ParsedContent>, t: Throwable) {
|
|
||||||
errorAfterMercuryCall()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun errorAfterMercuryCall() {
|
|
||||||
CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url)
|
|
||||||
) { _, uri ->
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.widget.Toast
|
|
||||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
|
||||||
import com.melnykov.fab.FloatingActionButton
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
|
|
||||||
class SourcesActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
val mFab = findViewById(R.id.fab) as FloatingActionButton
|
|
||||||
val mRecyclerView = findViewById(R.id.activity_sources) as RecyclerView
|
|
||||||
val mLayoutManager = LinearLayoutManager(this)
|
|
||||||
val api = SelfossApi(this)
|
|
||||||
var items: ArrayList<Sources> = ArrayList()
|
|
||||||
|
|
||||||
mFab.attachToRecyclerView(mRecyclerView)
|
|
||||||
mRecyclerView.setHasFixedSize(true)
|
|
||||||
mRecyclerView.layoutManager = mLayoutManager
|
|
||||||
|
|
||||||
api.sources.enqueue(object : Callback<List<Sources>> {
|
|
||||||
override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
|
|
||||||
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
|
||||||
items = response.body() as ArrayList<Sources>
|
|
||||||
}
|
|
||||||
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
|
|
||||||
mRecyclerView.adapter = mAdapter
|
|
||||||
mAdapter.notifyDataSetChanged()
|
|
||||||
if (items.isEmpty()) Toast.makeText(this@SourcesActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
|
|
||||||
Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mFab.setOnClickListener {
|
|
||||||
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,252 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.constraint.ConstraintLayout
|
|
||||||
import android.support.design.widget.Snackbar
|
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ImageView.ScaleType
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import com.like.LikeButton
|
|
||||||
import com.like.OnLikeListener
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
class ItemCardAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
|
||||||
private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean,
|
|
||||||
private val articleViewer: Boolean, private val fullHeightCards: Boolean) : RecyclerView.Adapter<ItemCardAdapter.ViewHolder>() {
|
|
||||||
private val c: Context = app.applicationContext
|
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as ConstraintLayout
|
|
||||||
return ViewHolder(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val itm = items[position]
|
|
||||||
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
|
||||||
val d: Long
|
|
||||||
try {
|
|
||||||
d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
|
|
||||||
sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
|
|
||||||
d,
|
|
||||||
Date().time,
|
|
||||||
DateUtils.MINUTE_IN_MILLIS,
|
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
|
||||||
)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.sourceTitleAndDate!!.text = sourceAndDate
|
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
|
||||||
Glide.clear(holder.itemImage)
|
|
||||||
holder.itemImage!!.setImageDrawable(null)
|
|
||||||
} else {
|
|
||||||
if (fullHeightCards) {
|
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage)
|
|
||||||
} else {
|
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
|
||||||
val color = generator.getColor(itm.sourcetitle)
|
|
||||||
val textDrawable = StringBuilder()
|
|
||||||
for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
|
||||||
textDrawable.append(s[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = TextDrawable.builder().round()
|
|
||||||
|
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doUnmark(i: Item, position: Int) {
|
|
||||||
val s = Snackbar
|
|
||||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.undo_string) {
|
|
||||||
items.add(position, i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
|
|
||||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val view = s.view
|
|
||||||
val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
|
||||||
tv.setTextColor(Color.WHITE)
|
|
||||||
s.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeItemAtIndex(position: Int) {
|
|
||||||
|
|
||||||
val i = items[position]
|
|
||||||
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
|
|
||||||
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
|
|
||||||
items.add(i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
|
||||||
var saveBtn: LikeButton? = null
|
|
||||||
var browserBtn: ImageButton? = null
|
|
||||||
var shareBtn: ImageButton? = null
|
|
||||||
var itemImage: ImageView? = null
|
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var title: TextView? = null
|
|
||||||
var sourceTitleAndDate: TextView? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
handleClickListeners()
|
|
||||||
handleCustomTabActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
|
||||||
sourceImage = mView.findViewById(R.id.sourceImage) as ImageView
|
|
||||||
itemImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
title = mView.findViewById(R.id.title) as TextView
|
|
||||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
|
|
||||||
saveBtn = mView.findViewById(R.id.favButton) as LikeButton
|
|
||||||
shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
|
|
||||||
browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
|
|
||||||
|
|
||||||
if (!fullHeightCards) {
|
|
||||||
itemImage!!.maxHeight = c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
|
||||||
itemImage!!.scaleType = ScaleType.CENTER_CROP
|
|
||||||
}
|
|
||||||
|
|
||||||
saveBtn!!.setOnLikeListener(object : OnLikeListener {
|
|
||||||
override fun liked(likeButton: LikeButton) {
|
|
||||||
val (id) = items[adapterPosition]
|
|
||||||
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
saveBtn!!.isLiked = false
|
|
||||||
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unLiked(likeButton: LikeButton) {
|
|
||||||
val (id) = items[adapterPosition]
|
|
||||||
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
saveBtn!!.isLiked = true
|
|
||||||
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
shareBtn!!.setOnClickListener {
|
|
||||||
val i = items[adapterPosition]
|
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
|
||||||
|
|
||||||
browserBtn!!.setOnClickListener {
|
|
||||||
val i = items[adapterPosition]
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
|
||||||
c.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCustomTabActions() {
|
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
|
||||||
helper.bindCustomTabsService(app)
|
|
||||||
|
|
||||||
mView.setOnClickListener {
|
|
||||||
openItemUrl(items[adapterPosition],
|
|
||||||
customTabsIntent,
|
|
||||||
internalBrowser,
|
|
||||||
articleViewer,
|
|
||||||
app,
|
|
||||||
c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,290 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.constraint.ConstraintLayout
|
|
||||||
import android.support.design.widget.Snackbar
|
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import com.like.LikeButton
|
|
||||||
import com.like.OnLikeListener
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
class ItemListAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
|
||||||
private val helper: CustomTabActivityHelper, private val clickBehavior: Boolean,
|
|
||||||
private val internalBrowser: Boolean, private val articleViewer: Boolean) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {
|
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
|
||||||
private val c: Context = app.applicationContext
|
|
||||||
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.list_item, parent, false) as ConstraintLayout
|
|
||||||
return ViewHolder(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val itm = items[position]
|
|
||||||
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
|
||||||
val d: Long
|
|
||||||
try {
|
|
||||||
d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
|
|
||||||
sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
|
|
||||||
d,
|
|
||||||
Date().time,
|
|
||||||
DateUtils.MINUTE_IN_MILLIS,
|
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
|
||||||
)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.sourceTitleAndDate!!.text = sourceAndDate
|
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
|
||||||
val sizeInInt = 46
|
|
||||||
val sizeInDp = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
|
|
||||||
.displayMetrics).toInt()
|
|
||||||
|
|
||||||
val marginInInt = 16
|
|
||||||
val marginInDp = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
|
||||||
.displayMetrics).toInt()
|
|
||||||
|
|
||||||
val params = holder.sourceImage!!.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
params.height = sizeInDp
|
|
||||||
params.width = sizeInDp
|
|
||||||
params.setMargins(marginInDp, 0, 0, 0)
|
|
||||||
holder.sourceImage!!.layoutParams = params
|
|
||||||
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
|
||||||
val color = generator.getColor(itm.sourcetitle)
|
|
||||||
val textDrawable = StringBuilder()
|
|
||||||
for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
|
||||||
textDrawable.append(s[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = TextDrawable.builder().round()
|
|
||||||
|
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.sourceImage)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bars[position]) {
|
|
||||||
holder.actionBar!!.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
holder.actionBar!!.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun doUnmark(i: Item, position: Int) {
|
|
||||||
val s = Snackbar
|
|
||||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.undo_string) {
|
|
||||||
items.add(position, i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
|
|
||||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val view = s.view
|
|
||||||
val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
|
||||||
tv.setTextColor(Color.WHITE)
|
|
||||||
s.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeItemAtIndex(position: Int) {
|
|
||||||
|
|
||||||
val i = items[position]
|
|
||||||
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
|
|
||||||
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
doUnmark(i, position)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
|
|
||||||
items.add(i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
|
||||||
var saveBtn: LikeButton? = null
|
|
||||||
var browserBtn: ImageButton? = null
|
|
||||||
var shareBtn: ImageButton? = null
|
|
||||||
var actionBar: RelativeLayout? = null
|
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var title: TextView? = null
|
|
||||||
var sourceTitleAndDate: TextView? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
handleClickListeners()
|
|
||||||
handleCustomTabActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
|
||||||
actionBar = mView.findViewById(R.id.actionBar) as RelativeLayout
|
|
||||||
sourceImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
title = mView.findViewById(R.id.title) as TextView
|
|
||||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
|
|
||||||
saveBtn = mView.findViewById(R.id.favButton) as LikeButton
|
|
||||||
shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
|
|
||||||
browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
|
|
||||||
|
|
||||||
|
|
||||||
saveBtn!!.setOnLikeListener(object : OnLikeListener {
|
|
||||||
override fun liked(likeButton: LikeButton) {
|
|
||||||
val (id) = items[adapterPosition]
|
|
||||||
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
saveBtn!!.isLiked = false
|
|
||||||
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unLiked(likeButton: LikeButton) {
|
|
||||||
val (id) = items[adapterPosition]
|
|
||||||
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
saveBtn!!.isLiked = true
|
|
||||||
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
shareBtn!!.setOnClickListener {
|
|
||||||
val i = items[adapterPosition]
|
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
|
||||||
|
|
||||||
browserBtn!!.setOnClickListener {
|
|
||||||
val i = items[adapterPosition]
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
|
||||||
c.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun handleCustomTabActions() {
|
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
|
||||||
helper.bindCustomTabsService(app)
|
|
||||||
|
|
||||||
|
|
||||||
if (!clickBehavior) {
|
|
||||||
mView.setOnClickListener {
|
|
||||||
openItemUrl(items[adapterPosition],
|
|
||||||
customTabsIntent,
|
|
||||||
internalBrowser,
|
|
||||||
articleViewer,
|
|
||||||
app,
|
|
||||||
c)
|
|
||||||
}
|
|
||||||
mView.setOnLongClickListener {
|
|
||||||
actionBarShowHide()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mView.setOnClickListener { actionBarShowHide() }
|
|
||||||
mView.setOnLongClickListener {
|
|
||||||
openItemUrl(items[adapterPosition],
|
|
||||||
customTabsIntent,
|
|
||||||
internalBrowser,
|
|
||||||
articleViewer,
|
|
||||||
app,
|
|
||||||
c)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun actionBarShowHide() {
|
|
||||||
bars[adapterPosition] = true
|
|
||||||
if (actionBar!!.visibility == View.GONE)
|
|
||||||
actionBar!!.visibility = View.VISIBLE
|
|
||||||
else
|
|
||||||
actionBar!!.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.support.constraint.ConstraintLayout
|
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
class SourcesListAdapter(private val app: Activity, private val items: ArrayList<Sources>, private val api: SelfossApi) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
|
||||||
private val c: Context = app.baseContext
|
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false) as ConstraintLayout
|
|
||||||
return ViewHolder(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val itm = items[position]
|
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
|
||||||
val color = generator.getColor(itm.title)
|
|
||||||
val textDrawable = StringBuilder()
|
|
||||||
for (s in itm.title.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
|
||||||
textDrawable.append(s[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = TextDrawable.builder().round()
|
|
||||||
|
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
|
||||||
} else {
|
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.sourceTitle!!.text = itm.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var sourceTitle: TextView? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
handleClickListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
|
||||||
sourceImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
sourceTitle = mView.findViewById(R.id.sourceTitle) as TextView
|
|
||||||
|
|
||||||
val deleteBtn = mView.findViewById(R.id.deleteBtn) as Button
|
|
||||||
|
|
||||||
deleteBtn.setOnClickListener {
|
|
||||||
val (id) = items[adapterPosition]
|
|
||||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
|
||||||
items.removeAt(adapterPosition)
|
|
||||||
notifyItemRemoved(adapterPosition)
|
|
||||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
|
||||||
|
|
||||||
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
|
|
||||||
|
|
||||||
class MercuryApi(private val key: String) {
|
|
||||||
private val service: MercuryService
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
|
||||||
|
|
||||||
val gson = GsonBuilder()
|
|
||||||
.setLenient()
|
|
||||||
.create()
|
|
||||||
val retrofit = Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
|
||||||
service = retrofit.create(MercuryService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseUrl(url: String): Call<ParsedContent> {
|
|
||||||
return service.parseUrl(url, this.key)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
|
|
||||||
|
|
||||||
class ParsedContent(val title: String,
|
|
||||||
val content: String,
|
|
||||||
val date_published: String,
|
|
||||||
val lead_image_url: String,
|
|
||||||
val dek: String,
|
|
||||||
val url: String,
|
|
||||||
val domain: String,
|
|
||||||
val excerpt: String,
|
|
||||||
val total_pages: Int,
|
|
||||||
val rendered_pages: Int,
|
|
||||||
val next_page_url: String) : Parcelable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
|
|
||||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
|
||||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: Parcel) : this(
|
|
||||||
title = source.readString(),
|
|
||||||
content = source.readString(),
|
|
||||||
date_published = source.readString(),
|
|
||||||
lead_image_url = source.readString(),
|
|
||||||
dek = source.readString(),
|
|
||||||
url = source.readString(),
|
|
||||||
domain = source.readString(),
|
|
||||||
excerpt = source.readString(),
|
|
||||||
total_pages = source.readInt(),
|
|
||||||
rendered_pages = source.readInt(),
|
|
||||||
next_page_url = source.readString()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun describeContents() = 0
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeString(title)
|
|
||||||
dest.writeString(content)
|
|
||||||
dest.writeString(date_published)
|
|
||||||
dest.writeString(lead_image_url)
|
|
||||||
dest.writeString(dek)
|
|
||||||
dest.writeString(url)
|
|
||||||
dest.writeString(domain)
|
|
||||||
dest.writeString(excerpt)
|
|
||||||
dest.writeInt(total_pages)
|
|
||||||
dest.writeInt(rendered_pages)
|
|
||||||
dest.writeString(next_page_url)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Header
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
|
|
||||||
interface MercuryService {
|
|
||||||
@GET("parser")
|
|
||||||
fun parseUrl(@Query("url") url: String, @Header("x-api-key") key: String): Call<ParsedContent>
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
|
||||||
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
|
|
||||||
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
|
||||||
|
|
||||||
@Throws(JsonParseException::class)
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
|
|
||||||
try {
|
|
||||||
json.asInt == 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
json.asBoolean
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
|
||||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
|
||||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
|
||||||
import com.burgstaller.okhttp.DispatchingAuthenticator
|
|
||||||
import com.burgstaller.okhttp.basic.BasicAuthenticator
|
|
||||||
import com.burgstaller.okhttp.digest.CachingAuthenticator
|
|
||||||
import com.burgstaller.okhttp.digest.Credentials
|
|
||||||
import com.burgstaller.okhttp.digest.DigestAuthenticator
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
|
|
||||||
class SelfossApi(c: Context) {
|
|
||||||
|
|
||||||
private val service: SelfossService
|
|
||||||
private val config: Config = Config(c)
|
|
||||||
private val userName: String
|
|
||||||
private val password: String
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
|
|
||||||
val httpBuilder = OkHttpClient.Builder()
|
|
||||||
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
|
||||||
|
|
||||||
val httpUserName = config.httpUserLogin
|
|
||||||
val httpPassword = config.httpUserPassword
|
|
||||||
|
|
||||||
val credentials = Credentials(httpUserName, httpPassword)
|
|
||||||
val basicAuthenticator = BasicAuthenticator(credentials)
|
|
||||||
val digestAuthenticator = DigestAuthenticator(credentials)
|
|
||||||
|
|
||||||
// note that all auth schemes should be registered as lowercase!
|
|
||||||
val authenticator = DispatchingAuthenticator.Builder()
|
|
||||||
.with("digest", digestAuthenticator)
|
|
||||||
.with("basic", basicAuthenticator)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val client = httpBuilder
|
|
||||||
.authenticator(CachingAuthenticatorDecorator(authenticator, authCache))
|
|
||||||
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
val builder = GsonBuilder()
|
|
||||||
builder.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
|
||||||
|
|
||||||
val gson = builder
|
|
||||||
.setLenient()
|
|
||||||
.create()
|
|
||||||
|
|
||||||
userName = config.userLogin
|
|
||||||
password = config.userPassword
|
|
||||||
val retrofit = Retrofit.Builder().baseUrl(config.baseUrl).client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
|
||||||
service = retrofit.create(SelfossService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun login(): Call<SuccessResponse> {
|
|
||||||
return service.loginToSelfoss(config.userLogin, config.userPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
|
||||||
getItems("read", tag, sourceId, search)
|
|
||||||
|
|
||||||
fun unreadItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
|
||||||
getItems("unread", tag, sourceId, search)
|
|
||||||
|
|
||||||
fun starredItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
|
||||||
getItems("starred", tag, sourceId, search)
|
|
||||||
|
|
||||||
private fun getItems(type: String, tag: String?, sourceId: Long?, search: String?): Call<List<Item>> {
|
|
||||||
return service.getItems(type, tag, sourceId, search, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markItem(itemId: String): Call<SuccessResponse> {
|
|
||||||
return service.markAsRead(itemId, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unmarkItem(itemId: String): Call<SuccessResponse> {
|
|
||||||
return service.unmarkAsRead(itemId, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readAll(ids: List<String>): Call<SuccessResponse> {
|
|
||||||
return service.markAllAsRead(ids, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun starrItem(itemId: String): Call<SuccessResponse> {
|
|
||||||
return service.starr(itemId, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun unstarrItem(itemId: String): Call<SuccessResponse> {
|
|
||||||
return service.unstarr(itemId, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val stats: Call<Stats>
|
|
||||||
get() = service.stats(userName, password)
|
|
||||||
|
|
||||||
val tags: Call<List<Tag>>
|
|
||||||
get() = service.tags(userName, password)
|
|
||||||
|
|
||||||
fun update(): Call<String> {
|
|
||||||
return service.update(userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources: Call<List<Sources>>
|
|
||||||
get() = service.sources(userName, password)
|
|
||||||
|
|
||||||
fun deleteSource(id: String): Call<SuccessResponse> {
|
|
||||||
return service.deleteSource(id, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun spouts(): Call<Map<String, Spout>> {
|
|
||||||
return service.spouts(userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createSource(title: String, url: String, spout: String, tags: String, filter: String): Call<SuccessResponse> {
|
|
||||||
return service.createSource(title, url, spout, tags, filter, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
|
||||||
|
|
||||||
|
|
||||||
private fun constructUrl(config: Config?, path: String, file: String): String {
|
|
||||||
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
|
||||||
baseUriBuilder.appendPath(path).appendPath(file)
|
|
||||||
|
|
||||||
return if (isEmptyOrNullOrNullString(file)) ""
|
|
||||||
else baseUriBuilder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class Tag(val tag: String, val color: String, val unread: Int)
|
|
||||||
|
|
||||||
class SuccessResponse(val success: Boolean) {
|
|
||||||
val isSuccess: Boolean
|
|
||||||
get() = success
|
|
||||||
}
|
|
||||||
|
|
||||||
class Stats(val total: Int, val unread: Int, val starred: Int)
|
|
||||||
|
|
||||||
data class Spout(val name: String, val description: String)
|
|
||||||
|
|
||||||
data class Sources(val id: String,
|
|
||||||
val title: String,
|
|
||||||
val tags: String,
|
|
||||||
val spout: String,
|
|
||||||
val error: String,
|
|
||||||
val icon: String) {
|
|
||||||
var config: Config? = null
|
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
|
||||||
if (config == null) {
|
|
||||||
config = Config(app)
|
|
||||||
}
|
|
||||||
return constructUrl(config,"favicons", icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Item(val id: String,
|
|
||||||
val datetime: String,
|
|
||||||
val title: String,
|
|
||||||
val unread: Boolean,
|
|
||||||
val starred: Boolean,
|
|
||||||
val thumbnail: String,
|
|
||||||
val icon: String,
|
|
||||||
val link: String,
|
|
||||||
val sourcetitle: String) : Parcelable {
|
|
||||||
|
|
||||||
var config: Config? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField val CREATOR: Parcelable.Creator<Item> = object : Parcelable.Creator<Item> {
|
|
||||||
override fun createFromParcel(source: Parcel): Item = Item(source)
|
|
||||||
override fun newArray(size: Int): Array<Item?> = arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: Parcel) : this(
|
|
||||||
id = source.readString(),
|
|
||||||
datetime = source.readString(),
|
|
||||||
title = source.readString(),
|
|
||||||
unread = 0.toByte() != source.readByte(),
|
|
||||||
starred = 0.toByte() != source.readByte(),
|
|
||||||
thumbnail = source.readString(),
|
|
||||||
icon = source.readString(),
|
|
||||||
link = source.readString(),
|
|
||||||
sourcetitle = source.readString()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun describeContents() = 0
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeString(id)
|
|
||||||
dest.writeString(datetime)
|
|
||||||
dest.writeString(title)
|
|
||||||
dest.writeByte((if (unread) 1 else 0))
|
|
||||||
dest.writeByte((if (starred) 1 else 0))
|
|
||||||
dest.writeString(thumbnail)
|
|
||||||
dest.writeString(icon)
|
|
||||||
dest.writeString(link)
|
|
||||||
dest.writeString(sourcetitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
|
||||||
if (config == null) {
|
|
||||||
config = Config(app)
|
|
||||||
}
|
|
||||||
return constructUrl(config, "favicons", icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getThumbnail(app: Context): String {
|
|
||||||
if (config == null) {
|
|
||||||
config = Config(app)
|
|
||||||
}
|
|
||||||
return constructUrl(config, "thumbnails", thumbnail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe find a better way to handle these kind of urls
|
|
||||||
fun getLinkDecoded(): String {
|
|
||||||
var stringUrl: String
|
|
||||||
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
|
||||||
if (link.contains("&url=")) {
|
|
||||||
stringUrl = link.substringAfter("&url=")
|
|
||||||
} else {
|
|
||||||
stringUrl = this.link.replace("&", "&")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stringUrl = this.link.replace("&", "&")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle :443 => https
|
|
||||||
if (stringUrl.contains(":443")) {
|
|
||||||
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
|
||||||
}
|
|
||||||
return stringUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.http.DELETE
|
|
||||||
import retrofit2.http.Field
|
|
||||||
import retrofit2.http.FormUrlEncoded
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
|
|
||||||
internal interface SelfossService {
|
|
||||||
@GET("login")
|
|
||||||
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
@GET("items")
|
|
||||||
fun getItems(@Query("type") type: String,
|
|
||||||
@Query("tag") tag: String?,
|
|
||||||
@Query("source") source: Long?,
|
|
||||||
@Query("search") search: String?,
|
|
||||||
@Query("username") username: String,
|
|
||||||
@Query("password") password: String): Call<List<Item>>
|
|
||||||
|
|
||||||
@POST("mark/{id}")
|
|
||||||
fun markAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
|
|
||||||
@POST("unmark/{id}")
|
|
||||||
fun unmarkAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST("mark")
|
|
||||||
fun markAllAsRead(@Field("ids[]") ids: List<String>, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
|
|
||||||
@POST("starr/{id}")
|
|
||||||
fun starr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
|
|
||||||
@POST("unstarr/{id}")
|
|
||||||
fun unstarr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
|
|
||||||
@GET("stats")
|
|
||||||
fun stats(@Query("username") username: String, @Query("password") password: String): Call<Stats>
|
|
||||||
|
|
||||||
|
|
||||||
@GET("tags")
|
|
||||||
fun tags(@Query("username") username: String, @Query("password") password: String): Call<List<Tag>>
|
|
||||||
|
|
||||||
|
|
||||||
@GET("update")
|
|
||||||
fun update(@Query("username") username: String, @Query("password") password: String): Call<String>
|
|
||||||
|
|
||||||
@GET("sources/spouts")
|
|
||||||
fun spouts(@Query("username") username: String, @Query("password") password: String): Call<Map<String, Spout>>
|
|
||||||
|
|
||||||
@GET("sources/list")
|
|
||||||
fun sources(@Query("username") username: String, @Query("password") password: String): Call<List<Sources>>
|
|
||||||
|
|
||||||
|
|
||||||
@DELETE("source/{id}")
|
|
||||||
fun deleteSource(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST("source")
|
|
||||||
fun createSource(@Field("title") title: String, @Field("url") url: String, @Field("spout") spout: String, @Field("tags") tags: String, @Field("filter") filter: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.settings;
|
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceActivity;
|
|
||||||
import android.support.annotation.LayoutRes;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v7.app.ActionBar;
|
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
|
||||||
import android.support.v7.widget.Toolbar;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link PreferenceActivity} which implements and proxies the necessary calls
|
|
||||||
* to be used with AppCompat.
|
|
||||||
*/
|
|
||||||
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
|
||||||
|
|
||||||
private AppCompatDelegate mDelegate;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
getDelegate().installViewFactory();
|
|
||||||
getDelegate().onCreate(savedInstanceState);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
getDelegate().onPostCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionBar getSupportActionBar() {
|
|
||||||
return getDelegate().getSupportActionBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSupportActionBar(@Nullable Toolbar toolbar) {
|
|
||||||
getDelegate().setSupportActionBar(toolbar);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public MenuInflater getMenuInflater() {
|
|
||||||
return getDelegate().getMenuInflater();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContentView(@LayoutRes int layoutResID) {
|
|
||||||
getDelegate().setContentView(layoutResID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContentView(View view) {
|
|
||||||
getDelegate().setContentView(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
|
||||||
getDelegate().setContentView(view, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addContentView(View view, ViewGroup.LayoutParams params) {
|
|
||||||
getDelegate().addContentView(view, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostResume() {
|
|
||||||
super.onPostResume();
|
|
||||||
getDelegate().onPostResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onTitleChanged(CharSequence title, int color) {
|
|
||||||
super.onTitleChanged(title, color);
|
|
||||||
getDelegate().setTitle(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
getDelegate().onConfigurationChanged(newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
getDelegate().onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
getDelegate().onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void invalidateOptionsMenu() {
|
|
||||||
getDelegate().invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppCompatDelegate getDelegate() {
|
|
||||||
if (mDelegate == null) {
|
|
||||||
mDelegate = AppCompatDelegate.create(this, null);
|
|
||||||
}
|
|
||||||
return mDelegate;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.settings;
|
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.Preference.OnPreferenceChangeListener;
|
|
||||||
import android.preference.PreferenceActivity;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.support.v7.app.ActionBar;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.R;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link PreferenceActivity} that presents a set of application settings. On
|
|
||||||
* handset devices, settings are presented as a single list. On tablets,
|
|
||||||
* settings are split by category, with category headers shown to the left of
|
|
||||||
* the list of settings.
|
|
||||||
* <p>
|
|
||||||
* See <a href="http://developer.android.com/design/patterns/settings.html">
|
|
||||||
* Android Design: Settings</a> for design guidelines and the <a
|
|
||||||
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
|
|
||||||
* API Guide</a> for more information on developing a Settings UI.
|
|
||||||
*/
|
|
||||||
public class SettingsActivity extends AppCompatPreferenceActivity {
|
|
||||||
/**
|
|
||||||
* A preference value change listener that updates the preference's summary
|
|
||||||
* to reflect its new value.
|
|
||||||
*/
|
|
||||||
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
|
||||||
String stringValue = value.toString();
|
|
||||||
preference.setSummary(stringValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to determine if the device has an extra-large screen. For
|
|
||||||
* example, 10" tablets are extra-large.
|
|
||||||
*/
|
|
||||||
private static boolean isXLargeTablet(Context context) {
|
|
||||||
return (context.getResources().getConfiguration().screenLayout
|
|
||||||
& Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a preference's summary to its value. More specifically, when the
|
|
||||||
* preference's value is changed, its summary (line of text below the
|
|
||||||
* preference title) is updated to reflect the value. The summary is also
|
|
||||||
* immediately updated upon calling this method. The exact display format is
|
|
||||||
* dependent on the type of preference.
|
|
||||||
*
|
|
||||||
* @see #sBindPreferenceSummaryToValueListener
|
|
||||||
*/
|
|
||||||
private static void bindPreferenceSummaryToValue(Preference preference) {
|
|
||||||
// Set the listener to watch for value changes.
|
|
||||||
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
|
|
||||||
|
|
||||||
// Trigger the listener immediately with the preference's
|
|
||||||
// current value.
|
|
||||||
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
|
|
||||||
PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(preference.getContext())
|
|
||||||
.getString(preference.getKey(), ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setupActionBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up the {@link android.app.ActionBar}, if the API is available.
|
|
||||||
*/
|
|
||||||
private void setupActionBar() {
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
// Show the Up button in the action bar.
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onIsMultiPane() {
|
|
||||||
return isXLargeTablet(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public void onBuildHeaders(List<Header> target) {
|
|
||||||
loadHeadersFromResource(R.xml.pref_headers, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method stops fragment injection in malicious applications.
|
|
||||||
* Make sure to deny any unknown fragments here.
|
|
||||||
*/
|
|
||||||
protected boolean isValidFragment(String fragmentName) {
|
|
||||||
return PreferenceFragment.class.getName().equals(fragmentName)
|
|
||||||
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|
|
||||||
|| LinksPreferenceFragment.class.getName().equals(fragmentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This fragment shows general preferences only. It is used when the
|
|
||||||
* activity is showing a two-pane settings UI.
|
|
||||||
*/
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public static class GeneralPreferenceFragment extends PreferenceFragment {
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
addPreferencesFromResource(R.xml.pref_general);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
|
|
||||||
SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active");
|
|
||||||
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
|
|
||||||
tabOnTap.setEnabled(!cardViewActive.isChecked());
|
|
||||||
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue){
|
|
||||||
boolean isEnabled = (Boolean) newValue;
|
|
||||||
tabOnTap.setEnabled(!isEnabled);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == android.R.id.home) {
|
|
||||||
getActivity().finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This fragment shows general preferences only. It is used when the
|
|
||||||
* activity is showing a two-pane settings UI.
|
|
||||||
*/
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public static class LinksPreferenceFragment extends PreferenceFragment {
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
addPreferencesFromResource(R.xml.pref_links);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
|
|
||||||
Preference tracker = findPreference( "trackerLink" );
|
|
||||||
tracker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.tracker_url)));
|
|
||||||
startActivity(browserIntent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_url)));
|
|
||||||
startActivity(browserIntent);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == android.R.id.home) {
|
|
||||||
getActivity().finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == android.R.id.home) {
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v7.app.AlertDialog
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Patterns
|
|
||||||
import apps.amine.bou.readerforselfoss.BuildConfig
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
private fun isStoreVersion(context: Context): Boolean {
|
|
||||||
var result = false
|
|
||||||
try {
|
|
||||||
val installer = context.packageManager
|
|
||||||
.getInstallerPackageName(context.packageName)
|
|
||||||
result = !TextUtils.isEmpty(installer)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkAndDisplayStoreApk(context: Context) =
|
|
||||||
if (!isStoreVersion(context) && !BuildConfig.GITHUB_VERSION) {
|
|
||||||
val alertDialog = AlertDialog.Builder(context).create()
|
|
||||||
alertDialog.setTitle(context.getString(R.string.warning_version))
|
|
||||||
alertDialog.setMessage(context.getString(R.string.text_version))
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
|
||||||
{ dialog, _ -> dialog.dismiss() })
|
|
||||||
alertDialog.show()
|
|
||||||
} else Unit
|
|
||||||
|
|
||||||
|
|
||||||
fun isUrlValid(url: String): Boolean {
|
|
||||||
val baseUrl = HttpUrl.parse(url)
|
|
||||||
var existsAndEndsWithSlash = false
|
|
||||||
if (baseUrl != null) {
|
|
||||||
val pathSegments = baseUrl.pathSegments()
|
|
||||||
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return Patterns.WEB_URL.matcher(url).matches() && existsAndEndsWithSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmptyOrNullOrNullString(str: String?): Boolean =
|
|
||||||
str == null || str == "null" || str.isEmpty()
|
|
||||||
|
|
||||||
fun checkApkVersion(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
|
|
||||||
mFirebaseRemoteConfig.fetch(43200)
|
|
||||||
.addOnCompleteListener { task ->
|
|
||||||
if (task.isSuccessful) {
|
|
||||||
mFirebaseRemoteConfig.activateFetched()
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
|
|
||||||
isThereAnUpdate(settings, editor, context, mFirebaseRemoteConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isThereAnUpdate(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
|
|
||||||
val APK_LINK = "github_apk"
|
|
||||||
|
|
||||||
val apkLink = mFirebaseRemoteConfig.getString(APK_LINK)
|
|
||||||
val storedLink = settings.getString(APK_LINK, "")
|
|
||||||
if (apkLink != storedLink && !apkLink.isEmpty()) {
|
|
||||||
val alertDialog = AlertDialog.Builder(context).create()
|
|
||||||
alertDialog.setTitle(context.getString(R.string.new_apk_available_title))
|
|
||||||
alertDialog.setMessage(context.getString(R.string.new_apk_available_message))
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.new_apk_available_get)) { _, _ ->
|
|
||||||
editor.putString(APK_LINK, apkLink)
|
|
||||||
editor.apply()
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink))
|
|
||||||
context.startActivity(browserIntent)
|
|
||||||
}
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.new_apk_available_no),
|
|
||||||
{ dialog, _ ->
|
|
||||||
editor.putString(APK_LINK, apkLink)
|
|
||||||
editor.apply()
|
|
||||||
dialog.dismiss()
|
|
||||||
})
|
|
||||||
alertDialog.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun longHash(string: String): Long {
|
|
||||||
var h = 98764321261L
|
|
||||||
val l = string.length
|
|
||||||
val chars = string.toCharArray()
|
|
||||||
|
|
||||||
for (i in 0..l - 1) {
|
|
||||||
h = 31 * h + chars[i].toLong()
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
|
|
||||||
|
|
||||||
class Config(c: Context) {
|
|
||||||
|
|
||||||
private val settings: SharedPreferences
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val baseUrl: String
|
|
||||||
get() = settings.getString("url", "")
|
|
||||||
|
|
||||||
val userLogin: String
|
|
||||||
get() = settings.getString("login", "")
|
|
||||||
|
|
||||||
val userPassword: String
|
|
||||||
get() = settings.getString("password", "")
|
|
||||||
|
|
||||||
val httpUserLogin: String
|
|
||||||
get() = settings.getString("httpUserName", "")
|
|
||||||
|
|
||||||
val httpUserPassword: String
|
|
||||||
get() = settings.getString("httpPassword", "")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val settingsName = "paramsselfoss"
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.customtabs.CustomTabsIntent
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import apps.amine.bou.readerforselfoss.ReaderActivity
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
|
||||||
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
|
|
||||||
|
|
||||||
fun buildCustomTabsIntent(c: Context): CustomTabsIntent {
|
|
||||||
|
|
||||||
fun createPendingShareIntent(c: Context): PendingIntent {
|
|
||||||
val actionIntent = Intent(Intent.ACTION_SEND)
|
|
||||||
actionIntent.type = "text/plain"
|
|
||||||
return PendingIntent.getActivity(
|
|
||||||
c, 0, actionIntent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intentBuilder = CustomTabsIntent.Builder()
|
|
||||||
|
|
||||||
// TODO: change to primary when it's possible to customize custom tabs title color
|
|
||||||
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
|
|
||||||
intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark))
|
|
||||||
intentBuilder.setShowTitle(true)
|
|
||||||
|
|
||||||
|
|
||||||
intentBuilder.setStartAnimations(c,
|
|
||||||
R.anim.slide_in_right,
|
|
||||||
R.anim.slide_out_left)
|
|
||||||
intentBuilder.setExitAnimations(c,
|
|
||||||
android.R.anim.slide_in_left,
|
|
||||||
android.R.anim.slide_out_right)
|
|
||||||
|
|
||||||
val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp)
|
|
||||||
intentBuilder.setCloseButtonIcon(closeicon)
|
|
||||||
|
|
||||||
val shareLabel = c.getString(R.string.label_share)
|
|
||||||
val icon = BitmapFactory.decodeResource(c.resources,
|
|
||||||
R.drawable.ic_share_white_24dp)
|
|
||||||
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c))
|
|
||||||
|
|
||||||
return intentBuilder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openItemUrl(i: Item,
|
|
||||||
customTabsIntent: CustomTabsIntent,
|
|
||||||
internalBrowser: Boolean,
|
|
||||||
articleViewer: Boolean,
|
|
||||||
app: Activity,
|
|
||||||
c: Context) {
|
|
||||||
if (!internalBrowser) {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
|
||||||
app.startActivity(intent)
|
|
||||||
} else {
|
|
||||||
if (articleViewer) {
|
|
||||||
val intent = Intent(c, ReaderActivity::class.java)
|
|
||||||
|
|
||||||
DragDismissIntentBuilder(c)
|
|
||||||
.setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side
|
|
||||||
.setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL) // Larger elasticities will make it easier to dismiss.
|
|
||||||
.build(intent)
|
|
||||||
|
|
||||||
intent.putExtra("url", i.getLinkDecoded())
|
|
||||||
app.startActivity(intent)
|
|
||||||
} else {
|
|
||||||
CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(i.getLinkDecoded())
|
|
||||||
) { _, uri ->
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
c.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.customtabs.CustomTabsClient;
|
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
|
||||||
import android.support.customtabs.CustomTabsServiceConnection;
|
|
||||||
import android.support.customtabs.CustomTabsSession;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a helper class to manage the connection to the Custom Tabs Service.
|
|
||||||
*/
|
|
||||||
public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|
||||||
private CustomTabsSession mCustomTabsSession;
|
|
||||||
private CustomTabsClient mClient;
|
|
||||||
private CustomTabsServiceConnection mConnection;
|
|
||||||
private ConnectionCallback mConnectionCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
|
|
||||||
*
|
|
||||||
* @param activity The host activity.
|
|
||||||
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
|
||||||
* @param uri the Uri to be opened.
|
|
||||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
|
||||||
*/
|
|
||||||
public static void openCustomTab(Activity activity,
|
|
||||||
CustomTabsIntent customTabsIntent,
|
|
||||||
Uri uri,
|
|
||||||
CustomTabFallback fallback) {
|
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
|
||||||
|
|
||||||
//If we cant find a package name, it means theres no browser that supports
|
|
||||||
//Chrome Custom Tabs installed. So, we fallback to the webview
|
|
||||||
if (packageName == null) {
|
|
||||||
if (fallback != null) {
|
|
||||||
fallback.openUri(activity, uri);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
customTabsIntent.intent.setPackage(packageName);
|
|
||||||
customTabsIntent.launchUrl(activity, uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unbinds the Activity from the Custom Tabs Service.
|
|
||||||
* @param activity the activity that is connected to the service.
|
|
||||||
*/
|
|
||||||
public void unbindCustomTabsService(Activity activity) {
|
|
||||||
if (mConnection == null) return;
|
|
||||||
activity.unbindService(mConnection);
|
|
||||||
mClient = null;
|
|
||||||
mCustomTabsSession = null;
|
|
||||||
mConnection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates or retrieves an exiting CustomTabsSession.
|
|
||||||
*
|
|
||||||
* @return a CustomTabsSession.
|
|
||||||
*/
|
|
||||||
public CustomTabsSession getSession() {
|
|
||||||
if (mClient == null) {
|
|
||||||
mCustomTabsSession = null;
|
|
||||||
} else if (mCustomTabsSession == null) {
|
|
||||||
mCustomTabsSession = mClient.newSession(null);
|
|
||||||
}
|
|
||||||
return mCustomTabsSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
|
||||||
* @param connectionCallback
|
|
||||||
*/
|
|
||||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
|
||||||
this.mConnectionCallback = connectionCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the Activity to the Custom Tabs Service.
|
|
||||||
* @param activity the activity to be binded to the service.
|
|
||||||
*/
|
|
||||||
public void bindCustomTabsService(Activity activity) {
|
|
||||||
if (mClient != null) return;
|
|
||||||
|
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
|
||||||
if (packageName == null) return;
|
|
||||||
|
|
||||||
mConnection = new ServiceConnection(this);
|
|
||||||
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
|
||||||
* @return true if call to mayLaunchUrl was accepted.
|
|
||||||
*/
|
|
||||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
|
||||||
if (mClient == null) return false;
|
|
||||||
|
|
||||||
CustomTabsSession session = getSession();
|
|
||||||
if (session == null) return false;
|
|
||||||
|
|
||||||
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(CustomTabsClient client) {
|
|
||||||
mClient = client;
|
|
||||||
mClient.warmup(0L);
|
|
||||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected() {
|
|
||||||
mClient = null;
|
|
||||||
mCustomTabsSession = null;
|
|
||||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Callback for when the service is connected or disconnected. Use those callbacks to
|
|
||||||
* handle UI changes when the service is connected or disconnected.
|
|
||||||
*/
|
|
||||||
public interface ConnectionCallback {
|
|
||||||
/**
|
|
||||||
* Called when the service is connected.
|
|
||||||
*/
|
|
||||||
void onCustomTabsConnected();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the service is disconnected.
|
|
||||||
*/
|
|
||||||
void onCustomTabsDisconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be used as a fallback to open the Uri when Custom Tabs is not available.
|
|
||||||
*/
|
|
||||||
public interface CustomTabFallback {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param activity The Activity that wants to open the Uri.
|
|
||||||
* @param uri The uri to be opened by the fallback.
|
|
||||||
*/
|
|
||||||
void openUri(Activity activity, Uri uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.support.customtabs.CustomTabsService;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
|
||||||
class CustomTabsHelper {
|
|
||||||
private static final String TAG = "CustomTabsHelper";
|
|
||||||
private static final String STABLE_PACKAGE = "com.android.chrome";
|
|
||||||
private static final String BETA_PACKAGE = "com.chrome.beta";
|
|
||||||
private static final String DEV_PACKAGE = "com.chrome.dev";
|
|
||||||
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
|
|
||||||
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
|
|
||||||
"android.support.customtabs.extra.KEEP_ALIVE";
|
|
||||||
|
|
||||||
private static String sPackageNameToUse;
|
|
||||||
|
|
||||||
private CustomTabsHelper() {}
|
|
||||||
|
|
||||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
|
||||||
Intent keepAliveIntent = new Intent().setClassName(
|
|
||||||
context.getPackageName(), KeepAliveService.class.getCanonicalName());
|
|
||||||
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
|
||||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
|
||||||
* valid package name.
|
|
||||||
*
|
|
||||||
* This is <strong>not</strong> threadsafe.
|
|
||||||
*
|
|
||||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
|
||||||
* @return The package name recommended to use for connecting to custom tabs related components.
|
|
||||||
*/
|
|
||||||
public static String getPackageNameToUse(Context context) {
|
|
||||||
if (sPackageNameToUse != null) return sPackageNameToUse;
|
|
||||||
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
// Get default VIEW intent handler.
|
|
||||||
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
|
|
||||||
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
|
|
||||||
String defaultViewHandlerPackageName = null;
|
|
||||||
if (defaultViewHandlerInfo != null) {
|
|
||||||
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all apps that can handle VIEW intents.
|
|
||||||
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
|
|
||||||
List<String> packagesSupportingCustomTabs = new ArrayList<>();
|
|
||||||
for (ResolveInfo info : resolvedActivityList) {
|
|
||||||
Intent serviceIntent = new Intent();
|
|
||||||
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
|
|
||||||
serviceIntent.setPackage(info.activityInfo.packageName);
|
|
||||||
if (pm.resolveService(serviceIntent, 0) != null) {
|
|
||||||
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
|
|
||||||
// and service calls.
|
|
||||||
if (packagesSupportingCustomTabs.isEmpty()) {
|
|
||||||
sPackageNameToUse = null;
|
|
||||||
} else if (packagesSupportingCustomTabs.size() == 1) {
|
|
||||||
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
|
|
||||||
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
|
|
||||||
&& !hasSpecializedHandlerIntents(context, activityIntent)
|
|
||||||
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
|
|
||||||
sPackageNameToUse = defaultViewHandlerPackageName;
|
|
||||||
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
|
|
||||||
sPackageNameToUse = STABLE_PACKAGE;
|
|
||||||
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
|
|
||||||
sPackageNameToUse = BETA_PACKAGE;
|
|
||||||
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
|
|
||||||
sPackageNameToUse = DEV_PACKAGE;
|
|
||||||
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
|
|
||||||
sPackageNameToUse = LOCAL_PACKAGE;
|
|
||||||
}
|
|
||||||
return sPackageNameToUse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to check whether there is a specialized handler for a given intent.
|
|
||||||
* @param intent The intent to check with.
|
|
||||||
* @return Whether there is a specialized handler for the given intent.
|
|
||||||
*/
|
|
||||||
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
|
|
||||||
try {
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
|
||||||
intent,
|
|
||||||
PackageManager.GET_RESOLVED_FILTER);
|
|
||||||
if (handlers == null || handlers.size() == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (ResolveInfo resolveInfo : handlers) {
|
|
||||||
IntentFilter filter = resolveInfo.filter;
|
|
||||||
if (filter == null) continue;
|
|
||||||
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
|
|
||||||
if (resolveInfo.activityInfo == null) continue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
Log.e(TAG, "Runtime exception while getting specialized handlers");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return All possible chrome package names that provide custom tabs feature.
|
|
||||||
*/
|
|
||||||
public static String[] getPackages() {
|
|
||||||
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.support.customtabs.CustomTabsClient;
|
|
||||||
import android.support.customtabs.CustomTabsServiceConnection;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation for the CustomTabsServiceConnection that avoids leaking the
|
|
||||||
* ServiceConnectionCallback
|
|
||||||
*/
|
|
||||||
public class ServiceConnection extends CustomTabsServiceConnection {
|
|
||||||
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
|
|
||||||
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
|
|
||||||
|
|
||||||
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
|
|
||||||
mConnectionCallback = new WeakReference<>(connectionCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
|
|
||||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
|
||||||
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
|
||||||
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
|
||||||
|
|
||||||
|
|
||||||
import android.support.customtabs.CustomTabsClient;
|
|
||||||
|
|
||||||
|
|
||||||
public interface ServiceConnectionCallback {
|
|
||||||
/**
|
|
||||||
* Called when the service is connected.
|
|
||||||
* @param client a CustomTabsClient
|
|
||||||
*/
|
|
||||||
void onServiceConnected(CustomTabsClient client);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the service is disconnected.
|
|
||||||
*/
|
|
||||||
void onServiceDisconnected();
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs.helpers;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
public class KeepAliveService extends Service {
|
|
||||||
private static final Binder sBinder = new Binder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return sBinder;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
|
|
||||||
package apps.amine.bou.readerforselfoss.utils.drawer
|
|
||||||
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
|
|
||||||
|
|
||||||
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
var icon: ImageView = view.findViewById(R.id.material_drawer_icon) as ImageView
|
|
||||||
var name: TextView = view.findViewById(R.id.material_drawer_name) as TextView
|
|
||||||
var description: TextView = view.findViewById(R.id.material_drawer_description) as TextView
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlBasePrimaryDrawerItem.java */
|
|
||||||
package apps.amine.bou.readerforselfoss.utils.drawer
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.annotation.ColorInt
|
|
||||||
import android.support.annotation.ColorRes
|
|
||||||
import android.support.annotation.StringRes
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
|
|
||||||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
|
||||||
import com.mikepenz.materialdrawer.holder.ImageHolder
|
|
||||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
|
||||||
import com.mikepenz.materialdrawer.model.BaseDrawerItem
|
|
||||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
|
||||||
import com.mikepenz.materialdrawer.util.DrawerUIUtils
|
|
||||||
import com.mikepenz.materialize.util.UIUtils
|
|
||||||
|
|
||||||
|
|
||||||
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> : BaseDrawerItem<T, VH>() {
|
|
||||||
fun withIcon(url: String): T {
|
|
||||||
this.icon = ImageHolder(url)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withIcon(uri: Uri): T {
|
|
||||||
this.icon = ImageHolder(uri)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: StringHolder? = null
|
|
||||||
private set
|
|
||||||
var descriptionTextColor: ColorHolder? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
fun withDescription(description: String): T {
|
|
||||||
this.description = StringHolder(description)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withDescription(@StringRes descriptionRes: Int): T {
|
|
||||||
this.description = StringHolder(descriptionRes)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withDescriptionTextColor(@ColorInt color: Int): T {
|
|
||||||
this.descriptionTextColor = ColorHolder.fromColor(color)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withDescriptionTextColorRes(@ColorRes colorRes: Int): T {
|
|
||||||
this.descriptionTextColor = ColorHolder.fromColorRes(colorRes)
|
|
||||||
return this as T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* a helper method to have the logic for all secondaryDrawerItems only once
|
|
||||||
|
|
||||||
* @param viewHolder
|
|
||||||
*/
|
|
||||||
protected fun bindViewHelper(viewHolder: CustomBaseViewHolder) {
|
|
||||||
val ctx = viewHolder.itemView.context
|
|
||||||
|
|
||||||
//set the identifier from the drawerItem here. It can be used to run tests
|
|
||||||
viewHolder.itemView.id = hashCode()
|
|
||||||
|
|
||||||
//set the item selected if it is
|
|
||||||
viewHolder.itemView.isSelected = isSelected
|
|
||||||
|
|
||||||
//get the correct color for the background
|
|
||||||
val selectedColor = getSelectedColor(ctx)
|
|
||||||
//get the correct color for the text
|
|
||||||
val color = getColor(ctx)
|
|
||||||
val selectedTextColor = getSelectedTextColor(ctx)
|
|
||||||
//get the correct color for the icon
|
|
||||||
val iconColor = getIconColor(ctx)
|
|
||||||
val selectedIconColor = getSelectedIconColor(ctx)
|
|
||||||
|
|
||||||
//set the background for the item
|
|
||||||
UIUtils.setBackground(viewHolder.view, UIUtils.getSelectableBackground(ctx, selectedColor, true))
|
|
||||||
//set the text for the name
|
|
||||||
StringHolder.applyTo(this.getName(), viewHolder.name)
|
|
||||||
//set the text for the description or hide
|
|
||||||
StringHolder.applyToOrHide(this.description, viewHolder.description)
|
|
||||||
|
|
||||||
//set the colors for textViews
|
|
||||||
viewHolder.name.setTextColor(getTextColorStateList(color, selectedTextColor))
|
|
||||||
//set the description text color
|
|
||||||
ColorHolder.applyToOr(descriptionTextColor,
|
|
||||||
viewHolder.description, getTextColorStateList(color, selectedTextColor))
|
|
||||||
|
|
||||||
//define the typeface for our textViews
|
|
||||||
if (getTypeface() != null) {
|
|
||||||
viewHolder.name.typeface = getTypeface()
|
|
||||||
viewHolder.description.typeface = getTypeface()
|
|
||||||
}
|
|
||||||
|
|
||||||
//we make sure we reset the image first before setting the new one in case there is an empty one
|
|
||||||
DrawerImageLoader.getInstance().cancelImage(viewHolder.icon)
|
|
||||||
viewHolder.icon.setImageBitmap(null)
|
|
||||||
//get the drawables for our icon and set it
|
|
||||||
ImageHolder.applyTo(icon, viewHolder.icon, "customUrlItem")
|
|
||||||
|
|
||||||
//for android API 17 --> Padding not applied via xml
|
|
||||||
DrawerUIUtils.setDrawerVerticalPadding(viewHolder.view)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlPrimaryDrawerItem.java */
|
|
||||||
package apps.amine.bou.readerforselfoss.utils.drawer
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.annotation.LayoutRes
|
|
||||||
import android.support.annotation.StringRes
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import com.mikepenz.materialdrawer.holder.BadgeStyle
|
|
||||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(), ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
|
|
||||||
protected var mBadge: StringHolder = StringHolder("")
|
|
||||||
protected var mBadgeStyle = BadgeStyle()
|
|
||||||
|
|
||||||
override fun withBadge(badge: StringHolder): CustomUrlPrimaryDrawerItem {
|
|
||||||
this.mBadge = badge
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun withBadge(badge: String): CustomUrlPrimaryDrawerItem {
|
|
||||||
this.mBadge = StringHolder(badge)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun withBadge(@StringRes badgeRes: Int): CustomUrlPrimaryDrawerItem {
|
|
||||||
this.mBadge = StringHolder(badgeRes)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun withBadgeStyle(badgeStyle: BadgeStyle): CustomUrlPrimaryDrawerItem {
|
|
||||||
this.mBadgeStyle = badgeStyle
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getBadge(): StringHolder {
|
|
||||||
return mBadge
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getBadgeStyle(): BadgeStyle {
|
|
||||||
return mBadgeStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getType(): Int {
|
|
||||||
return R.id.material_drawer_item_custom_url_item
|
|
||||||
}
|
|
||||||
|
|
||||||
@LayoutRes
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return R.layout.material_drawer_item_primary
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindView(viewHolder: ViewHolder, payloads: List<*>?) {
|
|
||||||
super.bindView(viewHolder, payloads)
|
|
||||||
|
|
||||||
val ctx = viewHolder.itemView.context
|
|
||||||
|
|
||||||
//bind the basic view parts
|
|
||||||
bindViewHelper(viewHolder)
|
|
||||||
|
|
||||||
//set the text for the badge or hide
|
|
||||||
val badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge)
|
|
||||||
//style the badge if it is visible
|
|
||||||
if (badgeVisible) {
|
|
||||||
mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx)))
|
|
||||||
viewHolder.badgeContainer.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
viewHolder.badgeContainer.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
//define the typeface for our textViews
|
|
||||||
if (getTypeface() != null) {
|
|
||||||
viewHolder.badge.typeface = getTypeface()
|
|
||||||
}
|
|
||||||
|
|
||||||
//call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
|
|
||||||
onPostBindView(this, viewHolder.itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewHolder(v: View): ViewHolder {
|
|
||||||
return ViewHolder(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder(view: View) : CustomBaseViewHolder(view) {
|
|
||||||
val badgeContainer: View = view.findViewById(R.id.material_drawer_badge_container)
|
|
||||||
val badge: TextView = view.findViewById(R.id.material_drawer_badge) as TextView
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Copyright 2015 Google Inc. All Rights Reserved.
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
|
||||||
android:duration="@android:integer/config_mediumAnimTime"/>
|
|
||||||
</set>
|
|
@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Copyright 2015 Google Inc. All Rights Reserved.
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
|
||||||
android:duration="@android:integer/config_mediumAnimTime"/>
|
|
||||||
</set>
|
|
@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
|
|
||||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
|
||||||
</vector>
|
|
Before Width: | Height: | Size: 680 B |
Before Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 239 B |
Before Width: | Height: | Size: 221 B |
Before Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 301 B |
Before Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 953 B |
Before Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 473 B |
Before Width: | Height: | Size: 453 B |
Before Width: | Height: | Size: 398 B |
Before Width: | Height: | Size: 397 B |
Before Width: | Height: | Size: 434 B |
Before Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 86 B |
Before Width: | Height: | Size: 174 B |
Before Width: | Height: | Size: 175 B |
Before Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 208 B |
Before Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 144 B |
Before Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 309 B |
Before Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 634 B |
Before Width: | Height: | Size: 108 B |
Before Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 257 B |
Before Width: | Height: | Size: 300 B |
Before Width: | Height: | Size: 437 B |
Before Width: | Height: | Size: 308 B |
Before Width: | Height: | Size: 464 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 208 B |
Before Width: | Height: | Size: 557 B |
Before Width: | Height: | Size: 625 B |
Before Width: | Height: | Size: 557 B |
Before Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 496 B |
Before Width: | Height: | Size: 542 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 114 B |
Before Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 398 B |
Before Width: | Height: | Size: 636 B |
Before Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 649 B |
Before Width: | Height: | Size: 1.9 KiB |