Compare commits
641 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca4b424174 | |||
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 |
73
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
### Hey you !
|
||||||
|
|
||||||
|
Thank you for wanting to help. Even the smallest things can help this project become better.
|
||||||
|
|
||||||
|
Please read the guidelines before contributing, and follow them (or try to) when contributing.
|
||||||
|
|
||||||
|
### What you can do to help.
|
||||||
|
|
||||||
|
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
|
||||||
|
|
||||||
|
You can fork the repository, and [help me solve some issues](https://github.com/aminecmi/ReaderforSelfoss/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||||
|
|
||||||
|
### What I can't help you with.
|
||||||
|
|
||||||
|
Please, don't use the issue tracker for anything related to [Selfoss itself](https://github.com/SSilence/selfoss). The app calls the api provided by Selfoss, and can't help with solving issues with your Selfoss instance.
|
||||||
|
|
||||||
|
Always check if the web version of your instance is working.
|
||||||
|
|
||||||
|
# Some rules
|
||||||
|
### Bug reports/Feature request
|
||||||
|
|
||||||
|
* Always search before reporting an issue or asking for a feature to avoid duplicates.
|
||||||
|
* Include your unique user id. It's displayed on the debug settings page. (You can tap it, it'll be copied to your clipboard)
|
||||||
|
* Include every other useful details (app version, phone model, Android version and screenshots when possible).
|
||||||
|
* Avoid bumping non-fatal issues, or feature requests. I'll try to fix them as soon as possible, and try to prioritize the requests. (You may wan to use the [reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) for that)
|
||||||
|
|
||||||
|
### Pull requests
|
||||||
|
|
||||||
|
* Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why.
|
||||||
|
* Please ask before starting to work on an issue. I may be working on it, or someone else could be doing so.
|
||||||
|
* Each pull request should implement **ONE** feature or bugfix. Keep in mind that you can submit as many PR as you want.
|
||||||
|
* Your code must be simple and clear enough to avoid using comments to explain what it does.
|
||||||
|
* Follow the used coding style [the android koding style](https://android.github.io/kotlin-guides/style.html) ([some idoms for reference](http://kotlinlang.org/docs/reference/idioms.html)) with more to come.
|
||||||
|
* Try as much as possible to write a test for your feature, and if you do so, run it, and make it work.
|
||||||
|
* Always check your changes and discard the ones that are irrelevant to your feature or bugfix.
|
||||||
|
* Have meaningful commit messages.
|
||||||
|
* Always reference the issue you are working on in your PR description.
|
||||||
|
* Be willing to accept criticism on your PRs (as I am on mine).
|
||||||
|
* Remember that PR review can take time.
|
||||||
|
|
||||||
|
|
||||||
|
# Install Selfoss (if you don't have an instance)
|
||||||
|
|
||||||
|
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
|
||||||
|
|
||||||
|
All the details to need are [here](https://selfoss.aditu.de/).
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
|
||||||
|
You can directly import this project into IntellIJ/Android Studio.
|
||||||
|
|
||||||
|
You'll have to:
|
||||||
|
|
||||||
|
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
|
||||||
|
|
||||||
|
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
#### Inside ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
```
|
||||||
|
appLoginUrl="URL" # It can be empty.
|
||||||
|
appLoginUsername="LOGIN" # It can be empty.
|
||||||
|
appLoginPassword="PASS" # It can be empty.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### As gradle parameters
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
|
||||||
|
```
|
32
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* [ ] Are you running the latest version?
|
||||||
|
* [ ] Did you check for an existing issue ?
|
||||||
|
* [ ] Are you reporting to the correct repository?
|
||||||
|
* [ ] Did you perform a cursory search?
|
||||||
|
* [ ] Did you read the `CONTRIBUTING` guide ?
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
[Description of the bug or feature]
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. [First Step]
|
||||||
|
2. [Second Step]
|
||||||
|
3. [and so on...]
|
||||||
|
|
||||||
|
**Expected behavior:** [What you expected to happen]
|
||||||
|
|
||||||
|
**Actual behavior:** [What actually happened]
|
||||||
|
|
||||||
|
|
||||||
|
### Screenshots (optional)
|
||||||
|
|
||||||
|
`...`
|
||||||
|
|
||||||
|
### Device
|
||||||
|
|
||||||
|
- Device (manufacturer, model ...)
|
||||||
|
- OS (Android Version, ROM/Stock, Rooted/not, mods...)
|
||||||
|
- App version _(See Prerequisites)_
|
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
## Types of changes
|
||||||
|
|
||||||
|
- [ ] I have read the **CONTRIBUTING** document.
|
||||||
|
- [ ] My code follows the code style of this project.
|
||||||
|
- [ ] I have updated the documentation accordingly.
|
||||||
|
- [ ] I have added tests to cover my changes.
|
||||||
|
- [ ] All new and existing tests passed.
|
||||||
|
- [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654))
|
||||||
|
|
||||||
|
This closes issue #XXX
|
||||||
|
|
||||||
|
This is implements feature #YYY
|
||||||
|
|
||||||
|
This finishes chore #ZZZ
|
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/
|
|
398
CHANGELOG.md
@ -1,3 +1,399 @@
|
|||||||
|
**1.7.x**
|
||||||
|
|
||||||
|
- Hiding tags with 0 articles
|
||||||
|
|
||||||
|
- Fixed issue with basic auth and images loading
|
||||||
|
|
||||||
|
- Added the ability to justify or left align the reader text
|
||||||
|
|
||||||
|
- Fixed #251
|
||||||
|
|
||||||
|
- Added experimental issue to set a default timeout. Should work for #238.
|
||||||
|
|
||||||
|
- Closing #220.
|
||||||
|
|
||||||
|
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
|
||||||
|
|
||||||
|
- Closes #216. Issue with selfoss version 2.19.
|
||||||
|
|
||||||
|
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
|
||||||
|
|
||||||
|
- Closes #33. Background sync with settings.
|
||||||
|
|
||||||
|
- Closing #1. Initial article caching.
|
||||||
|
|
||||||
|
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
|
||||||
|
|
||||||
|
- Closing #38. Only doing api calls on network available.
|
||||||
|
|
||||||
|
- Closing #298 and #287. Issues with Listview rendering
|
||||||
|
|
||||||
|
- Closing #290. Fixing back button issue in Settings
|
||||||
|
|
||||||
|
- Closing #300. Fixing issues when displaying some special characters.
|
||||||
|
|
||||||
|
- Closing #310. Some feeds don't have icons nor thumbnails.
|
||||||
|
|
||||||
|
- Closing #178. Expending images on tap.
|
||||||
|
|
||||||
|
- Closing #323. Old issue with textview not having the right color.
|
||||||
|
|
||||||
|
- Closing #324. Svg images loading crashes the app.
|
||||||
|
|
||||||
|
- Closing #322. App crashed because of svg images.
|
||||||
|
|
||||||
|
- Closing #236. New sources can be added in Selfoss 2.19.
|
||||||
|
|
||||||
|
- Dropped support for android 4, the last version supporting it is v1721030811
|
||||||
|
|
||||||
|
**1.6.x**
|
||||||
|
|
||||||
|
- Handling hidden tags.
|
||||||
|
|
||||||
|
- Fixed pre-lolipop issue with automatic theme changes.
|
||||||
|
|
||||||
|
- Removed all Build config things.
|
||||||
|
|
||||||
|
- Removed firebase and fabric.
|
||||||
|
|
||||||
|
- Added Acra for optional crash reporting and error logging.
|
||||||
|
|
||||||
|
- Dynamic themes !
|
||||||
|
|
||||||
|
- Strings cleaning.
|
||||||
|
|
||||||
|
- Versions updates.
|
||||||
|
|
||||||
|
- Fixes #215, #208.
|
||||||
|
|
||||||
|
- Fixes #328.
|
||||||
|
|
||||||
|
**1.5.7.x**
|
||||||
|
|
||||||
|
- Added confirmation to the mark as read and update menues.
|
||||||
|
|
||||||
|
- Add to favorites from article viewer.
|
||||||
|
|
||||||
|
- Added an option to use a webview in the article viewer (see #149)
|
||||||
|
|
||||||
|
- Fixes (#151 #152 #155 #157 #160 #174) and more.
|
||||||
|
|
||||||
|
- New year fixes !!!
|
||||||
|
|
||||||
|
- Changed page indicator position as it was overlaping content.
|
||||||
|
|
||||||
|
- Now using slack instead of gitter.
|
||||||
|
|
||||||
|
- Moved completely to a webview to fix #161.
|
||||||
|
|
||||||
|
- Fixed typos in French ( Thanks @aancel )
|
||||||
|
|
||||||
|
- Updated the Contribution guide about translations.
|
||||||
|
|
||||||
|
- Better handling for articles update. (See #169)
|
||||||
|
|
||||||
|
- Ability to change the article viewer content font size (see #153)
|
||||||
|
|
||||||
|
- Versions updates * 2.
|
||||||
|
|
||||||
|
- Added padding to the recyclerview.
|
||||||
|
|
||||||
|
**1.5.5.x (didn't last long) AND 1.5.6.x**
|
||||||
|
|
||||||
|
- Toolbar in reader activity.
|
||||||
|
|
||||||
|
- Marking items as read on scroll (with settings to enable/disable).
|
||||||
|
|
||||||
|
- Swapped the title and subtitle in the article viewer.
|
||||||
|
|
||||||
|
- Added an animation to the viewpager.
|
||||||
|
|
||||||
|
- Completed Dutch, Indonesian and Portuguese translations !
|
||||||
|
|
||||||
|
- Fixed #142, #144, #147.
|
||||||
|
|
||||||
|
- Changed versions handling.
|
||||||
|
|
||||||
|
- Removed indonesian english as it was causing issues with the english version of the app.
|
||||||
|
|
||||||
|
**1.5.4.22**
|
||||||
|
|
||||||
|
- You can now scroll through the loaded articles !
|
||||||
|
|
||||||
|
**1.5.4.21**
|
||||||
|
|
||||||
|
- Spanish translation and some Indonesian !
|
||||||
|
|
||||||
|
**1.5.4.20**
|
||||||
|
|
||||||
|
- Turkish translation !
|
||||||
|
|
||||||
|
**1.5.4.19**
|
||||||
|
|
||||||
|
- Fixed an issue with crowdin configuration (and its translations)
|
||||||
|
|
||||||
|
**1.5.4.18**
|
||||||
|
|
||||||
|
- Typo fix.
|
||||||
|
|
||||||
|
- The real last infinite scroll bug fix.
|
||||||
|
|
||||||
|
- Simplified Chinese translation !
|
||||||
|
|
||||||
|
**1.5.4.17**
|
||||||
|
|
||||||
|
- Fixed the last bug with infinite scroll.
|
||||||
|
|
||||||
|
**1.5.4.16**
|
||||||
|
|
||||||
|
- Fixing list view displaying issues.
|
||||||
|
|
||||||
|
- Endless scroll is not in beta anymore.
|
||||||
|
|
||||||
|
**1.5.4.15**
|
||||||
|
|
||||||
|
- Fixed an issue with the sources list.
|
||||||
|
|
||||||
|
**1.5.4.14**
|
||||||
|
|
||||||
|
- Fixing infinite scroll trying to load more items when there are no more.
|
||||||
|
|
||||||
|
**1.5.4.13**
|
||||||
|
|
||||||
|
- Displaying the right number of items.
|
||||||
|
|
||||||
|
- Fixing infinite scroll remaining issues. Should be stable enough.
|
||||||
|
|
||||||
|
**1.5.4.12**
|
||||||
|
|
||||||
|
- Fixed fab and toolbar issue (#113)
|
||||||
|
|
||||||
|
- Fixed links clickable (#114)
|
||||||
|
|
||||||
|
- Changed the link colors in the article viewer
|
||||||
|
|
||||||
|
**1.5.4.11**
|
||||||
|
|
||||||
|
- Hiding FABs on scroll.
|
||||||
|
|
||||||
|
- Closing #109 (code cleaning)
|
||||||
|
|
||||||
|
- Hiding fabs on scroll (#101)
|
||||||
|
|
||||||
|
**1.5.4.10**
|
||||||
|
|
||||||
|
- Displaying a loader when "reading more" in the article viewer.
|
||||||
|
|
||||||
|
- Displaying the thumbnail instead of icon on the article viewer.
|
||||||
|
|
||||||
|
- Scrolling to top when loading content with the "read more" button.
|
||||||
|
|
||||||
|
**1.5.4.09**
|
||||||
|
|
||||||
|
- Using the kotlin wrapper for the material drawer (see #98 for more details).
|
||||||
|
|
||||||
|
- Updated support libraries
|
||||||
|
|
||||||
|
- Changed the Floating Action Button to the support library version.
|
||||||
|
|
||||||
|
- New reader activity action bar #103.
|
||||||
|
|
||||||
|
**1.5.4.08**
|
||||||
|
|
||||||
|
- Thanks @jrafaelsantana for translating the whole app in Brazilian Portuguese.
|
||||||
|
|
||||||
|
**1.5.4.07**
|
||||||
|
|
||||||
|
- Loading more items on swipe too.
|
||||||
|
|
||||||
|
- Fixed popup menu style. User may need to reselect the theme.
|
||||||
|
|
||||||
|
- Disabled reporting marking items as read if there isn't an issue.
|
||||||
|
|
||||||
|
**1.5.4.05/06**
|
||||||
|
|
||||||
|
- Translation fix.
|
||||||
|
|
||||||
|
**1.5.4.04**
|
||||||
|
|
||||||
|
- Fixing an issue with marking items as read (something related to an old version of selfoss).
|
||||||
|
|
||||||
|
**1.5.4.03**
|
||||||
|
|
||||||
|
- Trying to fix some issue with pre-launch reports. Reverted because it seems to be related to the dev console side.
|
||||||
|
|
||||||
|
**1.5.4.02**
|
||||||
|
|
||||||
|
- Fixing full height cards issue.
|
||||||
|
|
||||||
|
**1.5.4.01**
|
||||||
|
|
||||||
|
- Removed the "apk downloaded from outside of playstore" message.
|
||||||
|
|
||||||
|
- Versions update.
|
||||||
|
|
||||||
|
- HTML viewer version update. It should fix an issue with images.
|
||||||
|
|
||||||
|
- Some code cleaning.
|
||||||
|
|
||||||
|
**1.5.4.00**
|
||||||
|
|
||||||
|
- Added issue reporting from within the app.
|
||||||
|
|
||||||
|
**1.5.3.06**
|
||||||
|
|
||||||
|
- Fixed infinite scroll not working.
|
||||||
|
|
||||||
|
- Fixed logs not working.
|
||||||
|
|
||||||
|
- Temporary workaround handling opening invalid urls. Waiting to solve #83.
|
||||||
|
|
||||||
|
**1.5.3.05**
|
||||||
|
|
||||||
|
- Fixed an issue on older versions of Android.
|
||||||
|
|
||||||
|
- Libs update.
|
||||||
|
|
||||||
|
**1.5.3.04**
|
||||||
|
|
||||||
|
- Crowdin translations
|
||||||
|
|
||||||
|
**1.5.3.03**
|
||||||
|
|
||||||
|
- Libs updates.
|
||||||
|
|
||||||
|
- Translation fix.
|
||||||
|
|
||||||
|
**1.5.3.01/02**
|
||||||
|
|
||||||
|
- Added translation link to the settings page.
|
||||||
|
|
||||||
|
- Added the translation link to the README.
|
||||||
|
|
||||||
|
**1.5.3.00**
|
||||||
|
|
||||||
|
- (BETA) Added pull from bottom to load more pages of results. May be buggy.
|
||||||
|
|
||||||
|
**1.5.2.18/19**
|
||||||
|
|
||||||
|
- APK minification finally working. That means less space taken !
|
||||||
|
- Added an option to log every API call.
|
||||||
|
|
||||||
|
**1.5.2.17**
|
||||||
|
|
||||||
|
- Source code and tracker links weren't being set, and updated the contributing doc.
|
||||||
|
|
||||||
|
**1.5.2.15/16**
|
||||||
|
|
||||||
|
- Adding an account header on the lateral drawer.
|
||||||
|
|
||||||
|
- The account header is only displayed when the setting is enabled.
|
||||||
|
|
||||||
|
**1.5.2.13/14**
|
||||||
|
|
||||||
|
- Updated glide.
|
||||||
|
|
||||||
|
- Loading images from self signed certificate now working.
|
||||||
|
|
||||||
|
**1.5.2.12**
|
||||||
|
|
||||||
|
- Self signed certificates are now working for loading data. Image are not loading yet.
|
||||||
|
|
||||||
|
**1.5.2.11**
|
||||||
|
|
||||||
|
- Added a random unique identifier to be used in the logs.
|
||||||
|
|
||||||
|
**1.5.2.08/09/10**
|
||||||
|
|
||||||
|
- Added settable logs for reading articles problems.
|
||||||
|
|
||||||
|
**1.5.2.07**
|
||||||
|
|
||||||
|
- Added the ability to choose the number of items loaded (the maximum value is 200 and is imposed by the selfoss api)
|
||||||
|
|
||||||
|
**1.5.2.06**
|
||||||
|
|
||||||
|
- Fix problem introduced in 1.5.2.04. SVG file not working on older versions of android.
|
||||||
|
|
||||||
|
**1.5.2.05**
|
||||||
|
|
||||||
|
- Versions updates
|
||||||
|
|
||||||
|
**1.5.2.04**
|
||||||
|
|
||||||
|
- Reverted to the old icon.
|
||||||
|
|
||||||
|
- Better icon for the intro activity.
|
||||||
|
|
||||||
|
- Updated gradle version.
|
||||||
|
|
||||||
|
**1.5.2.03**
|
||||||
|
|
||||||
|
- Added the ability to accept self signed certificates. (Needs more testing)
|
||||||
|
|
||||||
|
**1.5.2.02**
|
||||||
|
|
||||||
|
- Added optional login option.
|
||||||
|
|
||||||
|
**1.5.2.01**
|
||||||
|
|
||||||
|
- New (Better) Icon !
|
||||||
|
|
||||||
|
**1.5.2.0**
|
||||||
|
|
||||||
|
- New Icon !
|
||||||
|
|
||||||
|
**1.5.1.9/10/11**
|
||||||
|
|
||||||
|
- Hiding the unread badge when marking all items as read.
|
||||||
|
|
||||||
|
**1.5.1.8**
|
||||||
|
|
||||||
|
- Fixes and libs updates.
|
||||||
|
|
||||||
|
**1.5.1.7**
|
||||||
|
|
||||||
|
- Bug fixes.
|
||||||
|
|
||||||
|
- Code cleaning
|
||||||
|
|
||||||
|
**1.5.1.6**
|
||||||
|
|
||||||
|
- Added back the badges after it was fixed on the library side.
|
||||||
|
|
||||||
|
**1.5.1.5**
|
||||||
|
|
||||||
|
- THEMES !!!! For now, the app has predefined themes. You can ask for new ones until I make them dynamic.
|
||||||
|
|
||||||
|
**1.5.1.3/4**
|
||||||
|
|
||||||
|
- Fixes introduces by the previous alpha (1.5.1.2)
|
||||||
|
|
||||||
|
**1.5.1.2**
|
||||||
|
|
||||||
|
- Added testing to the CI.
|
||||||
|
|
||||||
|
- Code cleaning
|
||||||
|
|
||||||
|
- Display the pull to refresh loader on api call
|
||||||
|
|
||||||
|
- Fixes :
|
||||||
|
|
||||||
|
- Can't pull down to refresh on first launch
|
||||||
|
|
||||||
|
- Recurring crash because of the url
|
||||||
|
|
||||||
|
- Couldn't open some urls because of missing "http"
|
||||||
|
|
||||||
|
- Adding a source with invalid url would crash
|
||||||
|
|
||||||
|
|
||||||
|
**1.5.1.1**
|
||||||
|
|
||||||
|
- Fixed an issue when trying to add a source without being logged in.
|
||||||
|
|
||||||
|
- Reloading drawer tags badges on slide to refresh.
|
||||||
|
|
||||||
**1.5.1**
|
**1.5.1**
|
||||||
|
|
||||||
- Added a drawer for filtering sources and tags.
|
- Added a drawer for filtering sources and tags.
|
||||||
@ -159,4 +555,4 @@ _Updates_
|
|||||||
|
|
||||||
**1.3.3.4**
|
**1.3.3.4**
|
||||||
|
|
||||||
...
|
...
|
||||||
|
45
README.md
@ -1,30 +1,47 @@
|
|||||||
# ReaderForSelfoss
|
# ReaderForSelfoss **(Only available from F-Droid)**
|
||||||
|
|
||||||
[](https://circleci.com/gh/aminecmi/ReaderforSelfoss/tree/master)
|
[](https://crowdin.com/project/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/)
|
It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/)
|
||||||
|
|
||||||
|
**The project is not dead at all.**
|
||||||
|
|
||||||
## Build
|
I still want to work on it, but for the last few months, I didn't have that much time to do so.
|
||||||
|
|
||||||
You can directly import this project into IntellIJ/Android Studio.
|
If you are a developer, don't hesitate to help with PRs.
|
||||||
|
|
||||||
You'll have to:
|
If you are a user, you can still create new issues. I'll fix them when I can.
|
||||||
|
|
||||||
- [Create your own launcher icon](https://developer.android.com/studio/write/image-asset-studio.html#creating-launcher)
|
<a href="https://f-droid.org/packages/apps.amine.bou.readerforselfoss"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
|
||||||
|
|
||||||
- Configure Fabric, or [remove it](https://docs.fabric.io/android/fabric/settings/removing.html#).
|
## Screen captures
|
||||||
- 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
|
<img src="res//fr-card.png?raw=true" alt="card view" width="400"/> <img src="res//fr-list.png?raw=true" alt="list view" width="400"/>
|
||||||
- feedback_email: An email to receive users feedback.
|
|
||||||
- source_url: an url to the source code, used in the settings
|
## Like my app ?
|
||||||
- tracker_url: an url to the tracker, used in the settings
|
|
||||||
|
<a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
||||||
|
|
||||||
|
## Want to help ?
|
||||||
|
|
||||||
|
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
|
||||||
|
|
||||||
|
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
||||||
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
|
- [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)
|
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||||
|
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
|
||||||
|
|
||||||
|
## Contributors (Alphabetical order) ❤️
|
||||||
|
|
||||||
|
- [@aancel](https://github.com/aancel)
|
||||||
|
- [@Binnette](https://github.com/Binnette)
|
||||||
|
- [@davidoskky](https://github.com/davidoskky)
|
||||||
|
- [@hectorgabucio](https://github.com/hectorgabucio)
|
||||||
|
- [@licaon-kter](https://github.com/licaon-kter)
|
||||||
|
- [@sergey-babkin](https://github.com/sergey-babkin)
|
||||||
|
201
app/build.gradle
@ -1,32 +1,54 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
}
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
def gitVersion() {
|
||||||
classpath 'io.fabric.tools:gradle:1.+'
|
def process
|
||||||
|
def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute()
|
||||||
|
if (maybeTagOfCurrentCommit.text.isEmpty()) {
|
||||||
|
println "No tag on current commit. Will take the latest one."
|
||||||
|
process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
|
||||||
|
} else {
|
||||||
|
println "Tag found on current commit"
|
||||||
|
process = 'git describe --contains HEAD'.execute()
|
||||||
}
|
}
|
||||||
|
return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
def versionCodeFromGit() {
|
||||||
|
println "version code " + gitVersion()
|
||||||
|
return gitVersion().toInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
def versionNameFromGit() {
|
||||||
|
println "version name " + gitVersion()
|
||||||
|
return gitVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
apply plugin: 'io.fabric'
|
|
||||||
|
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
repositories {
|
apply plugin: 'kotlin-kapt'
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileOptions {
|
||||||
buildToolsVersion "25.0.3"
|
// Flag to enable support for the new language APIs
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
compileSdkVersion 31
|
||||||
|
buildToolsVersion '31.0.0'
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "apps.amine.bou.readerforselfoss"
|
applicationId "apps.amine.bou.readerforselfoss"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 25
|
targetSdkVersion 31
|
||||||
versionCode 1510
|
versionCode versionCodeFromGit()
|
||||||
versionName "1.5.1"
|
versionName versionNameFromGit()
|
||||||
|
|
||||||
// Enabling multidex support.
|
// Enabling multidex support.
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@ -35,106 +57,115 @@ android {
|
|||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
}
|
}
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
// tests
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
javaCompileOptions {
|
||||||
|
annotationProcessorOptions {
|
||||||
|
arguments = ["room.schemaLocation":
|
||||||
|
"$projectDir/schemas".toString()]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||||
'proguard-rules.pro'
|
'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
debug {
|
||||||
|
buildConfigField "String", "LOGIN_URL", appLoginUrl
|
||||||
|
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
|
||||||
|
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions "build"
|
flavorDimensions "build"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
githubConfig {
|
githubConfig {
|
||||||
versionNameSuffix '-github'
|
versionNameSuffix '-github'
|
||||||
dimension "build"
|
dimension "build"
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "true"
|
|
||||||
}
|
|
||||||
storeConfig {
|
|
||||||
versionNameSuffix '-store'
|
|
||||||
dimension "build"
|
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "false"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.3.1-alpha02'
|
||||||
|
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02'
|
||||||
|
// Espresso-intents for validation and stubbing of Intents
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02'
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
// Android Support
|
// Android Support
|
||||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
implementation "androidx.appcompat:appcompat:1.4.0-beta01"
|
||||||
compile 'com.android.support:design:25.3.1'
|
implementation 'com.google.android.material:material:1.5.0-alpha04'
|
||||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
|
||||||
compile 'com.android.support:support-v4:25.3.1'
|
implementation "androidx.legacy:legacy-support-v4:$android_version"
|
||||||
compile 'com.android.support:support-vector-drawable:25.3.1'
|
implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
|
||||||
compile 'com.android.support:customtabs:25.3.1'
|
implementation "androidx.browser:browser:1.3.0"
|
||||||
compile 'com.android.support:cardview-v7:25.3.1'
|
implementation "androidx.cardview:cardview:$android_version"
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
|
implementation 'androidx.work:work-runtime-ktx:2.7.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||||
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
|
|
||||||
// Firebase + crashlytics
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||||
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
|
//multidex
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
|
||||||
// Intro
|
|
||||||
compile 'agency.tango.android:material-intro-screen:0.0.5'
|
|
||||||
|
|
||||||
// About
|
// About
|
||||||
compile('com.mikepenz:aboutlibraries:5.9.6@aar') {
|
implementation 'com.mikepenz:aboutlibraries-core:8.9.4'
|
||||||
transitive = true
|
implementation 'com.mikepenz:aboutlibraries:8.9.4'
|
||||||
}
|
implementation "com.mikepenz:aboutlibraries-definitions:8.9.4"
|
||||||
|
|
||||||
|
// Async
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||||
|
|
||||||
// Retrofit + http logging + okhttp
|
// Retrofit + http logging + okhttp
|
||||||
compile 'com.squareup.retrofit2:retrofit:2.3.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||||
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
|
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
|
||||||
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
compile 'com.burgstaller:okhttp-digest:1.12'
|
implementation 'com.burgstaller:okhttp-digest:2.5'
|
||||||
|
|
||||||
// Material-ish things
|
// Material-ish things
|
||||||
compile 'com.roughike:bottom-bar:2.3.1'
|
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.2.0'
|
||||||
compile 'com.melnykov:floatingactionbutton:1.3.0'
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
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
|
// glide
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
|
||||||
// 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
|
// Drawer
|
||||||
compile('com.mikepenz:materialdrawer:5.9.2@aar') {
|
implementation 'com.mikepenz:materialdrawer:8.4.4'
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
compile 'com.anupcowkur:reservoir:3.1.0'
|
|
||||||
|
|
||||||
|
// Themes
|
||||||
|
implementation 'com.52inc:scoops:1.0.0'
|
||||||
|
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||||
|
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
|
||||||
|
|
||||||
|
// Pager
|
||||||
|
implementation 'me.relex:circleindicator:2.1.6'
|
||||||
|
|
||||||
|
//PhotoView
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
|
|
||||||
|
implementation 'androidx.core:core-ktx:1.7.0-rc01'
|
||||||
|
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
|
||||||
|
|
||||||
|
implementation "androidx.room:room-ktx:2.4.0-beta01"
|
||||||
|
kapt "androidx.room:room-compiler:2.4.0-beta01"
|
||||||
|
|
||||||
|
implementation "android.arch.work:work-runtime-ktx:$work_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
42
app/proguard-rules.pro
vendored
@ -30,25 +30,13 @@
|
|||||||
<fields>;
|
<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 okio.**
|
||||||
-dontwarn retrofit2.Platform$Java8
|
-dontwarn retrofit2.Platform$Java8
|
||||||
-keepattributes Signature
|
-keep class retrofit.** { *; }
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit.http.* <methods>;
|
||||||
|
}
|
||||||
|
-keepattributes *Annotation*,Signature
|
||||||
-keepattributes Exceptions
|
-keepattributes Exceptions
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn javax.annotation.Nullable
|
-dontwarn javax.annotation.Nullable
|
||||||
@ -56,4 +44,22 @@
|
|||||||
|
|
||||||
|
|
||||||
#Bottom bar lib
|
#Bottom bar lib
|
||||||
-dontwarn com.roughike.bottombar.**
|
-dontwarn com.roughike.bottombar.**
|
||||||
|
|
||||||
|
|
||||||
|
# self signed glidemodule
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn com.anupcowkur.reservoir.**
|
||||||
|
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
|
||||||
|
-keep class android.support.v7.widget.SearchView { *; }
|
||||||
|
|
||||||
|
# maybe remove later ?
|
||||||
|
-keep class * extends androidx.fragment.app.Fragment
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "08ca537d7ac9d4dd216e8e395d70801a",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "tag",
|
||||||
|
"columnName": "tag",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"tag"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spout",
|
||||||
|
"columnName": "spout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "error",
|
||||||
|
"columnName": "error",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "6fa6944b04100d68eab61039876a8804",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "tag",
|
||||||
|
"columnName": "tag",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"tag"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spout",
|
||||||
|
"columnName": "spout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "error",
|
||||||
|
"columnName": "error",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "items",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail",
|
||||||
|
"columnName": "thumbnail",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "link",
|
||||||
|
"columnName": "link",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourcetitle",
|
||||||
|
"columnName": "sourcetitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6fa6944b04100d68eab61039876a8804\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "7ad9c4961992c13b670128485ebb3efc",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "tag",
|
||||||
|
"columnName": "tag",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"tag"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spout",
|
||||||
|
"columnName": "spout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "error",
|
||||||
|
"columnName": "error",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "items",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail",
|
||||||
|
"columnName": "thumbnail",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "link",
|
||||||
|
"columnName": "link",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourcetitle",
|
||||||
|
"columnName": "sourcetitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "actions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "articleId",
|
||||||
|
"columnName": "articleid",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "read",
|
||||||
|
"columnName": "read",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unstarred",
|
||||||
|
"columnName": "unstarred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7ad9c4961992c13b670128485ebb3efc\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "tag",
|
||||||
|
"columnName": "tag",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"tag"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spout",
|
||||||
|
"columnName": "spout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "error",
|
||||||
|
"columnName": "error",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "items",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail",
|
||||||
|
"columnName": "thumbnail",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "link",
|
||||||
|
"columnName": "link",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourcetitle",
|
||||||
|
"columnName": "sourcetitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "actions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "articleId",
|
||||||
|
"columnName": "articleid",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "read",
|
||||||
|
"columnName": "read",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unstarred",
|
||||||
|
"columnName": "unstarred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9cf8b03d32f80dfd58160599a1df197d\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
// TODO: test source adding
|
@ -0,0 +1,100 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.InstrumentationRegistry
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.action.ViewActions.pressBack
|
||||||
|
import androidx.test.espresso.action.ViewActions.pressKey
|
||||||
|
import androidx.test.espresso.action.ViewActions.typeText
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.contrib.DrawerActions
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.Intents.times
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class HomeActivityEspressoTest {
|
||||||
|
lateinit var context: Context
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(HomeActivity::class.java, true, false)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun clearData() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val editor =
|
||||||
|
context
|
||||||
|
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
editor.clear()
|
||||||
|
|
||||||
|
editor.putString("url", BuildConfig.LOGIN_URL)
|
||||||
|
editor.putString("login", BuildConfig.LOGIN_USERNAME)
|
||||||
|
editor.putString("password", BuildConfig.LOGIN_PASSWORD)
|
||||||
|
|
||||||
|
editor.commit()
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun menuItems() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(
|
||||||
|
withMenu(
|
||||||
|
id = R.id.action_search,
|
||||||
|
titleId = R.string.menu_home_search
|
||||||
|
)
|
||||||
|
).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.search_bar)).check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
onView(withId(R.id.search_src_text)).perform(
|
||||||
|
typeText("android"),
|
||||||
|
pressKey(KeyEvent.KEYCODE_SEARCH),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withText(R.string.action_disconnect)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name), times(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test articles opening and actions for cards and lists
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.InstrumentationRegistry
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.action.ViewActions.pressBack
|
||||||
|
import androidx.test.espresso.action.ViewActions.typeText
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.Intents.times
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import com.mikepenz.aboutlibraries.ui.LibsActivity
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class LoginActivityEspressoTest {
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(LoginActivity::class.java, true, false)
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var url: String
|
||||||
|
private lateinit var username: String
|
||||||
|
private lateinit var password: String
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val editor =
|
||||||
|
context
|
||||||
|
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
editor.clear()
|
||||||
|
editor.commit()
|
||||||
|
|
||||||
|
|
||||||
|
url = BuildConfig.LOGIN_URL
|
||||||
|
username = BuildConfig.LOGIN_USERNAME
|
||||||
|
password = BuildConfig.LOGIN_PASSWORD
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun menuItems() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withText(R.string.action_about)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(LibsActivity::class.java.name), times(1))
|
||||||
|
|
||||||
|
onView(isRoot()).perform(pressBack())
|
||||||
|
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongLoginUrl() {
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginProgress))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests for multiple false urls with dialog
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyAuthData() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordLayout)).check(
|
||||||
|
matches(
|
||||||
|
isHintOrErrorEnabled()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongAuthData() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||||
|
typeText("WRONGPASS"),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun workingAuth() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||||
|
typeText(password),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(HomeActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import androidx.test.InstrumentationRegistry.getInstrumentation
|
||||||
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.espresso.intent.Intents.intended
|
||||||
|
import androidx.test.espresso.intent.Intents.times
|
||||||
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import org.junit.After
|
||||||
|
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MainActivityEspressoTest {
|
||||||
|
|
||||||
|
lateinit var intent: Intent
|
||||||
|
lateinit var preferencesEditor: SharedPreferences.Editor
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(MainActivity::class.java, true, false)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
intent = Intent()
|
||||||
|
val context = getInstrumentation().targetContext
|
||||||
|
|
||||||
|
// create a SharedPreferences editor
|
||||||
|
preferencesEditor = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkFirstOpenLaunchesIntro() {
|
||||||
|
preferencesEditor.putBoolean("firstStart", true)
|
||||||
|
preferencesEditor.commit()
|
||||||
|
|
||||||
|
rule.launchActivity(intent)
|
||||||
|
|
||||||
|
intended(hasComponent(MainActivity::class.java.name))
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name), times(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkNotFirstOpenLaunchesLogin() {
|
||||||
|
preferencesEditor.putBoolean("firstStart", false)
|
||||||
|
preferencesEditor.commit()
|
||||||
|
|
||||||
|
rule.launchActivity(intent)
|
||||||
|
|
||||||
|
intended(hasComponent(MainActivity::class.java.name))
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import android.view.View
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.hamcrest.TypeSafeMatcher
|
||||||
|
|
||||||
|
fun isHintOrErrorEnabled(): Matcher<View> =
|
||||||
|
object : TypeSafeMatcher<View>() {
|
||||||
|
override fun describeTo(description: Description?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: View?): Boolean {
|
||||||
|
if (item !is TextInputLayout) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.isHintEnabled || item.isErrorEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withMenu(id: Int, titleId: Int): Matcher<View> =
|
||||||
|
Matchers.anyOf(
|
||||||
|
ViewMatchers.withId(id),
|
||||||
|
ViewMatchers.withText(titleId)
|
||||||
|
)
|
@ -2,11 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="apps.amine.bou.readerforselfoss">
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MyApp"
|
android:name=".MyApp"
|
||||||
@ -14,25 +11,26 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:theme="@style/NoBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".IntroActivity"
|
android:name=".LoginActivity"
|
||||||
android:theme="@style/Theme.Intro">
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".LoginActivity"
|
|
||||||
android:label="@string/title_activity_login">
|
android:label="@string/title_activity_login">
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".HomeActivity"
|
<activity android:name=".HomeActivity">
|
||||||
android:theme="@style/NoBar">
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
@ -40,31 +38,50 @@
|
|||||||
android:parentActivityName=".HomeActivity">
|
android:parentActivityName=".HomeActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
|
android:value=".HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".SourcesActivity"
|
<activity
|
||||||
|
android:name=".SourcesActivity"
|
||||||
android:parentActivityName=".HomeActivity">
|
android:parentActivityName=".HomeActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value=".HomeActivity" />
|
android:value=".HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".AddSourceActivity"
|
<activity
|
||||||
android:parentActivityName=".SourcesActivity">
|
android:name=".AddSourceActivity"
|
||||||
|
android:parentActivityName=".SourcesActivity"
|
||||||
|
android:exported="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value=".SourcesActivity" />
|
android:value=".SourcesActivity" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ReaderActivity"
|
<activity
|
||||||
android:theme="@style/DragDismissTheme">
|
android:name=".ReaderActivity">
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ImageActivity">
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
|
||||||
|
android:value="GlideModule" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||||
|
<meta-data
|
||||||
|
android:name="preloaded_fonts"
|
||||||
|
android:resource="@array/preloaded_fonts" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
BIN
app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,53 +1,124 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.constraint.ConstraintLayout
|
import androidx.preference.PreferenceManager
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
|
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AddSourceActivity : AppCompatActivity() {
|
class AddSourceActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var mSpoutsValue: String? = null
|
private var mSpoutsValue: String? = null
|
||||||
|
private lateinit var api: SelfossApi
|
||||||
|
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
private lateinit var binding: ActivityAddSourceBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@AddSourceActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_add_source)
|
binding = ActivityAddSourceBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
|
||||||
val mProgress = findViewById(R.id.progress) as ProgressBar
|
setContentView(view)
|
||||||
val mForm = findViewById(R.id.formContainer) as ConstraintLayout
|
|
||||||
val mNameInput = findViewById(R.id.nameInput) as EditText
|
val scoop = Scoop.getInstance()
|
||||||
val mSourceUri = findViewById(R.id.sourceUri) as EditText
|
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
|
||||||
val mTags = findViewById(R.id.tags) as EditText
|
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||||
val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner
|
|
||||||
val mSaveBtn = findViewById(R.id.saveBtn) as Button
|
val drawable = binding.nameInput.background
|
||||||
val api = SelfossApi(this)
|
drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
|
||||||
|
|
||||||
val intent = intent
|
// TODO: clean
|
||||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
binding.nameInput.background = drawable
|
||||||
mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
|
||||||
mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
val drawable1 = binding.sourceUri.background
|
||||||
|
drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
|
||||||
|
binding.sourceUri.background = drawable1
|
||||||
|
|
||||||
|
val drawable2 = binding.tags.background
|
||||||
|
drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
|
||||||
|
binding.tags.background = drawable2
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val settings =
|
||||||
|
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@AddSourceActivity,
|
||||||
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||||
|
)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
mustLoginToAddSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api) }
|
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
|
||||||
|
|
||||||
|
binding.saveBtn.setTextColor(appColors.colorAccent)
|
||||||
|
|
||||||
|
binding.saveBtn.setOnClickListener {
|
||||||
|
handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val config = Config(this)
|
||||||
|
|
||||||
|
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
|
||||||
|
mustLoginToAddSource()
|
||||||
|
} else {
|
||||||
|
handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSpoutsSpinner(
|
||||||
|
spoutsSpinner: Spinner,
|
||||||
|
api: SelfossApi?,
|
||||||
|
mProgress: ProgressBar,
|
||||||
|
formContainer: ConstraintLayout
|
||||||
|
) {
|
||||||
val spoutsKV = HashMap<String, String>()
|
val spoutsKV = HashMap<String, String>()
|
||||||
mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
|
||||||
val spoutName = (view as TextView).text.toString()
|
if (view != null) {
|
||||||
mSpoutsValue = spoutsKV[spoutName]
|
val spoutName = (view as TextView).text.toString()
|
||||||
|
mSpoutsValue = spoutsKV[spoutName]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||||
@ -55,68 +126,143 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = Config(this)
|
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()!!
|
||||||
|
|
||||||
if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) {
|
val itemsStrings = items.map { it.value.name }
|
||||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
for ((key, value) in items) {
|
||||||
val i = Intent(this, LoginActivity::class.java)
|
spoutsKV[value.name] = key
|
||||||
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) {
|
mProgress.visibility = View.GONE
|
||||||
|
formContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val spinnerArrayAdapter =
|
||||||
|
ArrayAdapter(
|
||||||
|
this@AddSourceActivity,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
itemsStrings
|
||||||
|
)
|
||||||
|
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
spoutsSpinner.adapter = spinnerArrayAdapter
|
||||||
|
} else {
|
||||||
handleProblemWithSpouts()
|
handleProblemWithSpouts()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleProblemWithSpouts() {
|
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
|
handleProblemWithSpouts()
|
||||||
mProgress.visibility = View.GONE
|
}
|
||||||
}
|
|
||||||
})
|
private fun handleProblemWithSpouts() {
|
||||||
|
Toast.makeText(
|
||||||
|
this@AddSourceActivity,
|
||||||
|
R.string.cant_get_spouts,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
mProgress.visibility = View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeGetDetailsFromIntentSharing(
|
||||||
|
intent: Intent,
|
||||||
|
sourceUri: EditText,
|
||||||
|
nameInput: EditText
|
||||||
|
) {
|
||||||
|
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
||||||
|
sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||||
|
nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
|
private fun mustLoginToAddSource() {
|
||||||
|
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
||||||
|
val i = Intent(this, LoginActivity::class.java)
|
||||||
|
startActivity(i)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
|
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
|
||||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
val sourceDetailsUnavailable =
|
||||||
api.createSource(title, url, mSpoutsValue!!, mTags.text.toString(), "").enqueue(object : Callback<SuccessResponse> {
|
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
when {
|
||||||
finish()
|
sourceDetailsUnavailable -> {
|
||||||
} else {
|
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
|
}
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
|
||||||
|
val tagList = tags.text.toString().split(",").map { it.trim() }
|
||||||
|
api.createSourceApi2(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
mSpoutsValue!!,
|
||||||
|
tagList,
|
||||||
|
""
|
||||||
|
).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) {
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
}
|
this@AddSourceActivity,
|
||||||
})
|
R.string.cant_create_source,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
api.createSource(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
mSpoutsValue!!,
|
||||||
|
tags.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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.fragments.ImageFragment
|
||||||
|
|
||||||
|
class ImageActivity : AppCompatActivity() {
|
||||||
|
private lateinit var allImages : ArrayList<String>
|
||||||
|
private var position : Int = 0
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityImageBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityImageBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolBar)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
|
||||||
|
position = intent.getIntExtra("position", 0)
|
||||||
|
|
||||||
|
binding.pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager)
|
||||||
|
binding.pager.currentItem = position
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return allImages.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): ImageFragment {
|
||||||
|
return ImageFragment.newInstance(allImages[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,114 +6,112 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.TextInputLayout
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.support.v7.app.AlertDialog
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Switch
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.ActivityLoginBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
|
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.mikepenz.aboutlibraries.Libs
|
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
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 inValidCount: Int = 0
|
||||||
|
private var isWithSelfSignedCert = false
|
||||||
private var isWithLogin = false
|
private var isWithLogin = false
|
||||||
private var isWithHTTPLogin = false
|
private var isWithHTTPLogin = false
|
||||||
private var mLoginFormView: View? = null
|
|
||||||
private var mFirebaseAnalytics: FirebaseAnalytics? = null
|
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var settings: SharedPreferences
|
||||||
|
private lateinit var editor: SharedPreferences.Editor
|
||||||
|
private lateinit var userIdentifier: String
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@LoginActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_login)
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
|
||||||
|
handleBaseUrlFail()
|
||||||
|
|
||||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
if (settings!!.getString("url", "").isNotEmpty()) {
|
userIdentifier = settings.getString("unique_id", "")!!
|
||||||
|
|
||||||
|
editor = settings.edit()
|
||||||
|
|
||||||
|
if (settings.getString("url", "")!!.isNotEmpty()) {
|
||||||
goToMain()
|
goToMain()
|
||||||
} else {
|
|
||||||
checkAndDisplayStoreApk(this@LoginActivity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isWithLogin = false
|
handleActions()
|
||||||
isWithHTTPLogin = false
|
}
|
||||||
inValidCount = 0
|
|
||||||
|
|
||||||
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
|
private fun handleActions() {
|
||||||
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
|
binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
|
||||||
val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch
|
isWithSelfSignedCert = !isWithSelfSignedCert
|
||||||
val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
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, _ ->
|
binding.warningText.visibility = visi
|
||||||
if (id == R.id.login || id == EditorInfo.IME_NULL) {
|
}
|
||||||
attemptLogin()
|
|
||||||
return@OnEditorActionListener true
|
binding.passwordView.setOnEditorActionListener(
|
||||||
|
TextView.OnEditorActionListener { _, id, _ ->
|
||||||
|
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
|
||||||
|
attemptLogin()
|
||||||
|
return@OnEditorActionListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
false
|
)
|
||||||
})
|
|
||||||
|
|
||||||
mEmailSignInButton.setOnClickListener { attemptLogin() }
|
binding.signInButton.setOnClickListener { attemptLogin() }
|
||||||
|
|
||||||
mSwitch.setOnCheckedChangeListener { _, b ->
|
binding.withLogin.setOnCheckedChangeListener { _, b ->
|
||||||
isWithLogin = !isWithLogin
|
isWithLogin = !isWithLogin
|
||||||
val visi: Int
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
binding.loginView.visibility = visi
|
||||||
visi = View.GONE
|
binding.passwordView.visibility = visi
|
||||||
}
|
|
||||||
mLoginLayout.visibility = visi
|
|
||||||
mPasswordLayout.visibility = visi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mHTTPSwitch.setOnCheckedChangeListener { _, b ->
|
binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
|
||||||
isWithHTTPLogin = !isWithHTTPLogin
|
isWithHTTPLogin = !isWithHTTPLogin
|
||||||
val visi: Int
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
binding.httpLoginView.visibility = visi
|
||||||
visi = View.GONE
|
binding.httpPasswordView.visibility = visi
|
||||||
}
|
}
|
||||||
mHTTPLoginLayout.visibility = visi
|
}
|
||||||
mHTTPPasswordLayout.visibility = visi
|
|
||||||
|
private fun handleBaseUrlFail() {
|
||||||
|
if (intent.getBooleanExtra("baseUrlFail", false)) {
|
||||||
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
|
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||||
|
alertDialog.setMessage(getString(R.string.base_url_error))
|
||||||
|
alertDialog.setButton(
|
||||||
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
"OK"
|
||||||
|
) { dialog, _ -> dialog.dismiss() }
|
||||||
|
alertDialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,138 +124,171 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
private fun attemptLogin() {
|
private fun attemptLogin() {
|
||||||
|
|
||||||
// Reset errors.
|
// Reset errors.
|
||||||
mUrlView!!.error = null
|
binding.urlView.error = null
|
||||||
mLoginView!!.error = null
|
binding.loginView.error = null
|
||||||
mHTTPLoginView!!.error = null
|
binding.httpLoginView.error = null
|
||||||
mPasswordView!!.error = null
|
binding.passwordView.error = null
|
||||||
mHTTPPasswordView!!.error = null
|
binding.httpPasswordView.error = null
|
||||||
|
|
||||||
// Store values at the time of the login attempt.
|
// Store values at the time of the login attempt.
|
||||||
val url = mUrlView!!.text.toString()
|
val url = binding.urlView.text.toString()
|
||||||
val login = mLoginView!!.text.toString()
|
val login = binding.loginView.text.toString()
|
||||||
val httpLogin = mHTTPLoginView!!.text.toString()
|
val httpLogin = binding.httpLoginView.text.toString()
|
||||||
val password = mPasswordView!!.text.toString()
|
val password = binding.passwordView.text.toString()
|
||||||
val httpPassword = mHTTPPasswordView!!.text.toString()
|
val httpPassword = binding.httpPasswordView.text.toString()
|
||||||
|
|
||||||
var cancel = false
|
var cancel = false
|
||||||
var focusView: View? = null
|
var focusView: View? = null
|
||||||
|
|
||||||
if (!isUrlValid(url)) {
|
if (!url.isBaseUrlValid(this@LoginActivity)) {
|
||||||
mUrlView!!.error = getString(R.string.login_url_problem)
|
binding.urlView.error = getString(R.string.login_url_problem)
|
||||||
focusView = mUrlView
|
focusView = binding.urlView
|
||||||
cancel = true
|
cancel = true
|
||||||
inValidCount++
|
inValidCount++
|
||||||
if (inValidCount == 3) {
|
if (inValidCount == 3) {
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
alertDialog.setButton(
|
||||||
{ dialog, _ -> dialog.dismiss() })
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
"OK"
|
||||||
|
) { dialog, _ -> dialog.dismiss() }
|
||||||
alertDialog.show()
|
alertDialog.show()
|
||||||
inValidCount = 0
|
inValidCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWithLogin || isWithHTTPLogin) {
|
if (isWithLogin) {
|
||||||
if (TextUtils.isEmpty(password)) {
|
if (TextUtils.isEmpty(password)) {
|
||||||
mPasswordView!!.error = getString(R.string.error_invalid_password)
|
binding.passwordView.error = getString(R.string.error_invalid_password)
|
||||||
focusView = mPasswordView
|
focusView = binding.passwordView
|
||||||
cancel = true
|
cancel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(login)) {
|
if (TextUtils.isEmpty(login)) {
|
||||||
mLoginView!!.error = getString(R.string.error_field_required)
|
binding.loginView.error = getString(R.string.error_field_required)
|
||||||
focusView = mLoginView
|
focusView = binding.loginView
|
||||||
|
cancel = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWithHTTPLogin) {
|
||||||
|
if (TextUtils.isEmpty(httpPassword)) {
|
||||||
|
binding.httpPasswordView.error = getString(R.string.error_invalid_password)
|
||||||
|
focusView = binding.httpPasswordView
|
||||||
|
cancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(httpLogin)) {
|
||||||
|
binding.httpLoginView.error = getString(R.string.error_field_required)
|
||||||
|
focusView = binding.httpLoginView
|
||||||
cancel = true
|
cancel = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
focusView!!.requestFocus()
|
focusView?.requestFocus()
|
||||||
} else {
|
} else {
|
||||||
showProgress(true)
|
showProgress(true)
|
||||||
|
|
||||||
val editor = settings!!.edit()
|
|
||||||
editor.putString("url", url)
|
editor.putString("url", url)
|
||||||
editor.putString("login", login)
|
editor.putString("login", login)
|
||||||
editor.putString("httpUserName", httpLogin)
|
editor.putString("httpUserName", httpLogin)
|
||||||
editor.putString("password", password)
|
editor.putString("password", password)
|
||||||
editor.putString("httpPassword", httpPassword)
|
editor.putString("httpPassword", httpPassword)
|
||||||
|
editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
|
||||||
editor.apply()
|
editor.apply()
|
||||||
|
|
||||||
val api = SelfossApi(this@LoginActivity)
|
val api = SelfossApi(
|
||||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
this,
|
||||||
private fun preferenceError() {
|
this@LoginActivity,
|
||||||
editor.remove("url")
|
isWithSelfSignedCert,
|
||||||
editor.remove("login")
|
-1L
|
||||||
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 (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
api.login().enqueue(object : Callback<SuccessResponse> {
|
||||||
mFirebaseAnalytics!!.logEvent(FirebaseAnalytics.Event.LOGIN, Bundle())
|
private fun preferenceError(t: Throwable) {
|
||||||
goToMain()
|
editor.remove("url")
|
||||||
} else {
|
editor.remove("login")
|
||||||
preferenceError()
|
editor.remove("httpUserName")
|
||||||
|
editor.remove("password")
|
||||||
|
editor.remove("httpPassword")
|
||||||
|
editor.apply()
|
||||||
|
binding.urlView.error = getString(R.string.wrong_infos)
|
||||||
|
binding.loginView.error = getString(R.string.wrong_infos)
|
||||||
|
binding.passwordView.error = getString(R.string.wrong_infos)
|
||||||
|
binding.httpLoginView.error = getString(R.string.wrong_infos)
|
||||||
|
binding.httpPasswordView.error = getString(R.string.wrong_infos)
|
||||||
|
showProgress(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onResponse(
|
||||||
preferenceError()
|
call: Call<SuccessResponse>,
|
||||||
}
|
response: Response<SuccessResponse>
|
||||||
})
|
) {
|
||||||
|
if (response.body() != null && response.body()!!.isSuccess) {
|
||||||
|
goToMain()
|
||||||
|
} else {
|
||||||
|
preferenceError(Exception("No response body..."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
|
preferenceError(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showProgress(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress UI and hides the login form.
|
|
||||||
*/
|
|
||||||
private fun showProgress(show: Boolean) {
|
private fun showProgress(show: Boolean) {
|
||||||
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||||
|
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
binding.loginForm
|
||||||
if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() {
|
.animate()
|
||||||
|
.setDuration(shortAnimTime.toLong())
|
||||||
|
.alpha(
|
||||||
|
if (show) 0F else 1F
|
||||||
|
).setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
binding.loginProgress
|
||||||
if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() {
|
.animate()
|
||||||
|
.setDuration(shortAnimTime.toLong())
|
||||||
|
.alpha(
|
||||||
|
if (show) 1F else 0F
|
||||||
|
).setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
val inflater = menuInflater
|
menuInflater.inflate(R.menu.login_menu, menu)
|
||||||
inflater.inflate(R.menu.login_menu, menu)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.about -> {
|
R.id.about -> {
|
||||||
LibsBuilder()
|
LibsBuilder()
|
||||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
.withAboutIconShown(true)
|
||||||
.withAboutIconShown(true)
|
.withAboutVersionShown(true)
|
||||||
.withAboutVersionShown(true)
|
.start(this)
|
||||||
.start(this)
|
true
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,22 @@ package apps.amine.bou.readerforselfoss
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.support.v7.app.AppCompatActivity
|
import apps.amine.bou.readerforselfoss.databinding.ActivityMainBinding
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
|
val intent = Intent(this, LoginActivity::class.java)
|
||||||
val i = Intent(this@MainActivity, IntroActivity::class.java)
|
|
||||||
startActivity(i)
|
|
||||||
} else {
|
|
||||||
val intent = Intent(this, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
startActivity(intent)
|
||||||
finish()
|
finish()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,101 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.multidex.MultiDexApplication
|
import android.os.Build
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.crashlytics.android.Crashlytics
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.github.stkent.amplify.tracking.Amplify
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import io.fabric.sdk.android.Fabric
|
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
|
||||||
import com.anupcowkur.reservoir.Reservoir
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
import java.io.IOException
|
import java.util.UUID.randomUUID
|
||||||
|
|
||||||
|
|
||||||
class MyApp : MultiDexApplication() {
|
class MyApp : MultiDexApplication() {
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (!BuildConfig.DEBUG)
|
config = Config(baseContext)
|
||||||
Fabric.with(this, Crashlytics())
|
|
||||||
|
|
||||||
Amplify.initSharedInstance(this)
|
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
.setFeedbackEmailAddress(getString(R.string.feedback_email))
|
if (prefs.getString("unique_id", "")!!.isEmpty()) {
|
||||||
.setAlwaysShow(BuildConfig.DEBUG)
|
val editor = prefs.edit()
|
||||||
.applyAllDefaultRules()
|
editor.putString("unique_id", randomUUID().toString())
|
||||||
|
editor.apply()
|
||||||
try {
|
|
||||||
Reservoir.init(this, 8192) //in bytes
|
|
||||||
} catch (e: IOException) {
|
|
||||||
//failure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDrawerImageLoader()
|
||||||
|
|
||||||
|
initTheme()
|
||||||
|
|
||||||
|
tryToHandleBug()
|
||||||
|
|
||||||
|
handleNotificationChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNotificationChannels() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
val name = getString(R.string.notification_channel_sync)
|
||||||
|
val importance = NotificationManager.IMPORTANCE_LOW
|
||||||
|
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
|
||||||
|
|
||||||
|
val newItemsChannelname = getString(R.string.new_items_channel_sync)
|
||||||
|
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(mChannel)
|
||||||
|
notificationManager.createNotificationChannel(newItemsChannelmChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDrawerImageLoader() {
|
||||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||||
override fun set(imageView: ImageView?, uri: Uri?, placeholder: Drawable?, tag: String?) {
|
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||||
Glide.with(imageView?.context).load(uri).placeholder(placeholder).into(imageView)
|
Glide.with(imageView?.context)
|
||||||
|
.loadMaybeBasicAuth(config, uri.toString())
|
||||||
|
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
|
||||||
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel(imageView: ImageView?) {
|
override fun cancel(imageView: ImageView) {
|
||||||
Glide.clear(imageView)
|
Glide.with(imageView?.context).clear(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun placeholder(ctx: Context?, tag: String?): Drawable {
|
override fun placeholder(ctx: Context, tag: String?): Drawable {
|
||||||
return applicationContext.resources.getDrawable(R.mipmap.ic_launcher)
|
return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initTheme() {
|
||||||
|
Scoop.waffleCone()
|
||||||
|
.addFlavor(getString(R.string.default_theme), R.style.NoBar, true)
|
||||||
|
.addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
|
||||||
|
.setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
|
||||||
|
.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryToHandleBug() {
|
||||||
|
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||||
|
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
|
||||||
|
it.toString().contains("android.view.ViewDebug")
|
||||||
|
}) {
|
||||||
|
Unit
|
||||||
|
} else {
|
||||||
|
oldHandler.uncaughtException(thread, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,105 +1,281 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.preference.PreferenceManager
|
||||||
import android.view.View
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
import androidx.room.Room
|
||||||
import android.widget.ImageView
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import android.widget.TextView
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
|
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
|
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
|
||||||
import com.bumptech.glide.Glide
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
|
||||||
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
|
||||||
import org.sufficientlysecure.htmltextview.HtmlTextView
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
import retrofit2.Call
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
import retrofit2.Callback
|
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
|
||||||
import retrofit2.Response
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toggleStar
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
|
import me.relex.circleindicator.CircleIndicator
|
||||||
|
|
||||||
|
class ReaderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
class ReaderActivity : DragDismissActivity() {
|
private var markOnScroll: Boolean = false
|
||||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
private var currentItem: Int = 0
|
||||||
|
private lateinit var userIdentifier: String
|
||||||
|
|
||||||
override fun onStart() {
|
private lateinit var api: SelfossApi
|
||||||
super.onStart()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
private lateinit var toolbarMenu: Menu
|
||||||
|
|
||||||
|
private lateinit var db: AppDatabase
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private lateinit var binding: ActivityReaderBinding
|
||||||
|
|
||||||
|
private var activeAlignment: Int = 1
|
||||||
|
val JUSTIFY = 1
|
||||||
|
val ALIGN_LEFT = 2
|
||||||
|
|
||||||
|
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||||
|
if (willAddToFavorite) {
|
||||||
|
toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
} else {
|
||||||
|
toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
private fun canFavorite() {
|
||||||
super.onStop()
|
showMenuItem(true)
|
||||||
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
|
private fun canRemoveFromFavorite() {
|
||||||
val v = inflater.inflate(R.layout.activity_reader, parent, false)
|
showMenuItem(false)
|
||||||
showProgressBar()
|
}
|
||||||
|
|
||||||
val image = v.findViewById(R.id.imageView) as ImageView
|
private lateinit var editor: SharedPreferences.Editor
|
||||||
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
|
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityReaderBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
|
||||||
val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity)
|
setContentView(view)
|
||||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
|
||||||
|
|
||||||
|
db = Room.databaseBuilder(
|
||||||
|
applicationContext,
|
||||||
|
AppDatabase::class.java, "selfoss-database"
|
||||||
|
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
|
||||||
|
|
||||||
parser.parseUrl(url).enqueue(object : Callback<ParsedContent> {
|
val scoop = Scoop.getInstance()
|
||||||
override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
|
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
|
||||||
if (response.body() != null && response.body()!!.content != null && response.body()!!.content.isNotEmpty()) {
|
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||||
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 {
|
setSupportActionBar(binding.toolBar)
|
||||||
val sendIntent = Intent()
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, response.body()!!.url)
|
val settings =
|
||||||
sendIntent.type = "text/plain"
|
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
editor = prefs.edit()
|
||||||
|
|
||||||
|
userIdentifier = prefs.getString("unique_id", "")!!
|
||||||
|
markOnScroll = prefs.getBoolean("mark_on_scroll", false)
|
||||||
|
activeAlignment = prefs.getInt("text_align", JUSTIFY)
|
||||||
|
|
||||||
|
api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@ReaderActivity,
|
||||||
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (allItems.isEmpty()) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentItem = intent.getIntExtra("currentItem", 0)
|
||||||
|
|
||||||
|
readItem(allItems[currentItem])
|
||||||
|
|
||||||
|
binding.pager.adapter =
|
||||||
|
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
|
||||||
|
binding.pager.currentItem = currentItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
notifyAdapter()
|
||||||
|
|
||||||
|
binding.pager.setPageTransformer(true, DepthPageTransformer())
|
||||||
|
(binding.indicator as CircleIndicator).setViewPager(binding.pager)
|
||||||
|
|
||||||
|
binding.pager.addOnPageChangeListener(
|
||||||
|
object : ViewPager.SimpleOnPageChangeListener() {
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
|
||||||
|
if (allItems[position].starred) {
|
||||||
|
canRemoveFromFavorite()
|
||||||
|
} else {
|
||||||
|
canFavorite()
|
||||||
}
|
}
|
||||||
|
readItem(allItems[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
browserBtn.setOnClickListener {
|
private fun readItem(item: Item) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
if (markOnScroll) {
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
SharedItems.readItem(applicationContext, api, db, item)
|
||||||
intent.data = Uri.parse(response.body()!!.url)
|
}
|
||||||
startActivity(intent)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
hideProgressBar()
|
private fun notifyAdapter() {
|
||||||
|
(binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
if (markOnScroll) {
|
||||||
|
binding.pager.clearOnPageChangeListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(oldInstanceState: Bundle) {
|
||||||
|
super.onSaveInstanceState(oldInstanceState)
|
||||||
|
oldInstanceState.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
|
||||||
|
FragmentStatePagerAdapter(fm) {
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return allItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): ArticleFragment {
|
||||||
|
return ArticleFragment.newInstance(position, allItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdate(container: ViewGroup) {
|
||||||
|
super.startUpdate(container)
|
||||||
|
|
||||||
|
container.background = ColorDrawable(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
this@ReaderActivity,
|
||||||
|
appColors.colorBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun alignmentMenu(showJustify: Boolean) {
|
||||||
|
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
|
||||||
|
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
inflater.inflate(R.menu.reader_menu, menu)
|
||||||
|
toolbarMenu = menu
|
||||||
|
|
||||||
|
if (!allItems.isEmpty() && allItems[currentItem].starred) {
|
||||||
|
canRemoveFromFavorite()
|
||||||
|
} else {
|
||||||
|
canFavorite()
|
||||||
|
}
|
||||||
|
if (activeAlignment == JUSTIFY) {
|
||||||
|
alignmentMenu(false)
|
||||||
|
} else {
|
||||||
|
alignmentMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
fun afterSave() {
|
||||||
|
allItems[binding.pager.currentItem] =
|
||||||
|
allItems[binding.pager.currentItem].toggleStar()
|
||||||
|
notifyAdapter()
|
||||||
|
canRemoveFromFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun afterUnsave() {
|
||||||
|
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
|
||||||
|
notifyAdapter()
|
||||||
|
canFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.star -> {
|
||||||
|
if (allItems[binding.pager.currentItem].starred) {
|
||||||
|
SharedItems.unstarItem(
|
||||||
|
this@ReaderActivity,
|
||||||
|
api,
|
||||||
|
db,
|
||||||
|
allItems[binding.pager.currentItem]
|
||||||
|
)
|
||||||
|
afterUnsave()
|
||||||
} else {
|
} else {
|
||||||
errorAfterMercuryCall()
|
SharedItems.starItem(
|
||||||
|
this@ReaderActivity,
|
||||||
|
api,
|
||||||
|
db,
|
||||||
|
allItems[binding.pager.currentItem]
|
||||||
|
)
|
||||||
|
afterSave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
R.id.align_left -> {
|
||||||
override fun onFailure(call: Call<ParsedContent>, t: Throwable) {
|
editor.putInt("text_align", ALIGN_LEFT)
|
||||||
errorAfterMercuryCall()
|
editor.apply()
|
||||||
|
alignmentMenu(true)
|
||||||
|
refreshFragment()
|
||||||
}
|
}
|
||||||
|
R.id.align_justify -> {
|
||||||
private fun errorAfterMercuryCall() {
|
editor.putInt("text_align", JUSTIFY)
|
||||||
CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url)
|
editor.apply()
|
||||||
) { _, uri ->
|
alignmentMenu(false)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
refreshFragment()
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return v
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshFragment() {
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(0, 0)
|
||||||
|
startActivity(intent)
|
||||||
|
overridePendingTransition(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var allItems: ArrayList<Item> = ArrayList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,108 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.preference.PreferenceManager
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.support.v7.widget.RecyclerView
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
import apps.amine.bou.readerforselfoss.api.selfoss.Source
|
||||||
import com.melnykov.fab.FloatingActionButton
|
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
|
|
||||||
class SourcesActivity : AppCompatActivity() {
|
class SourcesActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
private lateinit var binding: ActivitySourcesBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@SourcesActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_sources)
|
binding = ActivitySourcesBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
val scoop = Scoop.getInstance()
|
||||||
|
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
|
||||||
|
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
binding.fab.rippleColor = appColors.colorAccentDark
|
||||||
|
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
binding.recyclerView.clearOnScrollListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.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 mLayoutManager = LinearLayoutManager(this)
|
||||||
val api = SelfossApi(this)
|
|
||||||
var items: ArrayList<Sources> = ArrayList()
|
|
||||||
|
|
||||||
mFab.attachToRecyclerView(mRecyclerView)
|
val settings =
|
||||||
mRecyclerView.setHasFixedSize(true)
|
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
mRecyclerView.layoutManager = mLayoutManager
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
api.sources.enqueue(object : Callback<List<Sources>> {
|
val api = SelfossApi(
|
||||||
override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
|
this,
|
||||||
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
this@SourcesActivity,
|
||||||
items = response.body() as ArrayList<Sources>
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||||
|
)
|
||||||
|
var items: ArrayList<Source> = ArrayList()
|
||||||
|
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.layoutManager = mLayoutManager
|
||||||
|
|
||||||
|
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
|
||||||
|
api.sources.enqueue(object : Callback<List<Source>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<List<Source>>,
|
||||||
|
response: Response<List<Source>>
|
||||||
|
) {
|
||||||
|
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
||||||
|
items = response.body() as ArrayList<Source>
|
||||||
|
}
|
||||||
|
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
|
||||||
|
binding.recyclerView.adapter = mAdapter
|
||||||
|
mAdapter.notifyDataSetChanged()
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@SourcesActivity,
|
||||||
|
R.string.nothing_here,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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) {
|
override fun onFailure(call: Call<List<Source>>, t: Throwable) {
|
||||||
Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
}
|
this@SourcesActivity,
|
||||||
})
|
R.string.cant_get_sources,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
mFab.setOnClickListener {
|
binding.fab.setOnClickListener {
|
||||||
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,251 +1,163 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ImageView.ScaleType
|
import android.widget.ImageView.ScaleType
|
||||||
import android.widget.TextView
|
import androidx.core.content.ContextCompat
|
||||||
import android.widget.Toast
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.shareLink
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
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(
|
||||||
class ItemCardAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
override val app: Activity,
|
||||||
private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean,
|
override var items: ArrayList<Item>,
|
||||||
private val articleViewer: Boolean, private val fullHeightCards: Boolean) : RecyclerView.Adapter<ItemCardAdapter.ViewHolder>() {
|
override val api: SelfossApi,
|
||||||
private val c: Context = app.applicationContext
|
override val db: AppDatabase,
|
||||||
|
private val helper: CustomTabActivityHelper,
|
||||||
|
private val internalBrowser: Boolean,
|
||||||
|
private val articleViewer: Boolean,
|
||||||
|
private val fullHeightCards: Boolean,
|
||||||
|
override val appColors: AppColors,
|
||||||
|
override val userIdentifier: String,
|
||||||
|
override val config: Config,
|
||||||
|
override val updateItems: (ArrayList<Item>) -> Unit
|
||||||
|
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
|
||||||
|
private val c: Context = app.baseContext
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
|
private val imageMaxHeight: Int =
|
||||||
|
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as ConstraintLayout
|
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ViewHolder(v)
|
return ViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val itm = items[position]
|
with(holder) {
|
||||||
|
val itm = items[position]
|
||||||
|
|
||||||
|
binding.favButton.isSelected = itm.starred
|
||||||
|
binding.title.text = itm.getTitleDecoded()
|
||||||
|
binding.title.setTextColor(ContextCompat.getColor(
|
||||||
|
c,
|
||||||
|
appColors.textColor
|
||||||
|
))
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
binding.title.setOnTouchListener(LinkOnTouchListener())
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
binding.title.setLinkTextColor(appColors.colorAccent)
|
||||||
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
|
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
|
||||||
Glide.clear(holder.itemImage)
|
c,
|
||||||
holder.itemImage!!.setImageDrawable(null)
|
appColors.textColor
|
||||||
} else {
|
))
|
||||||
if (fullHeightCards) {
|
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage)
|
if (!fullHeightCards) {
|
||||||
|
binding.itemImage.maxHeight = imageMaxHeight
|
||||||
|
binding.itemImage.scaleType = ScaleType.CENTER_CROP
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itm.getThumbnail(c).isEmpty()) {
|
||||||
|
binding.itemImage.visibility = View.GONE
|
||||||
|
Glide.with(c).clear(binding.itemImage)
|
||||||
|
binding.itemImage.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage)
|
binding.itemImage.visibility = View.VISIBLE
|
||||||
}
|
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.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()
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
|
val color = generator.getColor(itm.getSourceTitle())
|
||||||
|
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
val drawable =
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
TextDrawable
|
||||||
} else {
|
.builder()
|
||||||
|
.round()
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
.build(itm.getSourceTitle().toTextDrawableString(c), color)
|
||||||
override fun setResource(resource: Bitmap) {
|
binding.sourceImage.setImageDrawable(drawable)
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
} else {
|
||||||
circularBitmapDrawable.isCircular = true
|
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return items.size
|
return items.size
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doUnmark(i: Item, position: Int) {
|
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
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 {
|
init {
|
||||||
|
binding.root.setCardBackgroundColor(appColors.cardBackgroundColor)
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
handleCustomTabActions()
|
handleCustomTabActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
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) {
|
binding.favButton.setOnClickListener {
|
||||||
itemImage!!.maxHeight = c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
val item = items[bindingAdapterPosition]
|
||||||
itemImage!!.scaleType = ScaleType.CENTER_CROP
|
if (isNetworkAvailable(c)) {
|
||||||
|
if (item.starred) {
|
||||||
|
SharedItems.unstarItem(c, api, db, item)
|
||||||
|
item.starred = false
|
||||||
|
binding.favButton.isSelected = false
|
||||||
|
} else {
|
||||||
|
SharedItems.starItem(c, api, db, item)
|
||||||
|
item.starred = true
|
||||||
|
binding.favButton.isSelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveBtn!!.setOnLikeListener(object : OnLikeListener {
|
binding.shareBtn.setOnClickListener {
|
||||||
override fun liked(likeButton: LikeButton) {
|
val item = items[bindingAdapterPosition]
|
||||||
val (id) = items[adapterPosition]
|
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
|
||||||
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) {
|
binding.browserBtn.setOnClickListener {
|
||||||
val (id) = items[adapterPosition]
|
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
|
||||||
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() {
|
private fun handleCustomTabActions() {
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
val customTabsIntent = c.buildCustomTabsIntent()
|
||||||
helper.bindCustomTabsService(app)
|
helper.bindCustomTabsService(app)
|
||||||
|
|
||||||
mView.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
openItemUrl(items[adapterPosition],
|
c.openItemUrl(
|
||||||
customTabsIntent,
|
items,
|
||||||
internalBrowser,
|
bindingAdapterPosition,
|
||||||
articleViewer,
|
items[bindingAdapterPosition].getLinkDecoded(),
|
||||||
app,
|
customTabsIntent,
|
||||||
c)
|
internalBrowser,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,290 +1,117 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import androidx.core.content.ContextCompat
|
||||||
import apps.amine.bou.readerforselfoss.R
|
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
import kotlin.collections.ArrayList
|
||||||
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(
|
||||||
class ItemListAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
override val app: Activity,
|
||||||
private val helper: CustomTabActivityHelper, private val clickBehavior: Boolean,
|
override var items: ArrayList<Item>,
|
||||||
private val internalBrowser: Boolean, private val articleViewer: Boolean) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {
|
override val api: SelfossApi,
|
||||||
|
override val db: AppDatabase,
|
||||||
|
private val helper: CustomTabActivityHelper,
|
||||||
|
private val internalBrowser: Boolean,
|
||||||
|
private val articleViewer: Boolean,
|
||||||
|
override val userIdentifier: String,
|
||||||
|
override val appColors: AppColors,
|
||||||
|
override val config: Config,
|
||||||
|
override val updateItems: (ArrayList<Item>) -> Unit
|
||||||
|
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
private val c: Context = app.applicationContext
|
private val c: Context = app.baseContext
|
||||||
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.list_item, parent, false) as ConstraintLayout
|
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ViewHolder(v)
|
return ViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val itm = items[position]
|
with(holder) {
|
||||||
|
val itm = items[position]
|
||||||
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
binding.title.text = itm.getTitleDecoded()
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
binding.title.setTextColor(ContextCompat.getColor(
|
||||||
val d: Long
|
c,
|
||||||
try {
|
appColors.textColor
|
||||||
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
|
binding.title.setOnTouchListener(LinkOnTouchListener())
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
binding.title.setLinkTextColor(appColors.colorAccent)
|
||||||
val sizeInInt = 46
|
|
||||||
val sizeInDp = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
|
|
||||||
.displayMetrics).toInt()
|
|
||||||
|
|
||||||
val marginInInt = 16
|
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
val marginInDp = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
|
||||||
.displayMetrics).toInt()
|
|
||||||
|
|
||||||
val params = holder.sourceImage!!.layoutParams as ViewGroup.MarginLayoutParams
|
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
|
||||||
params.height = sizeInDp
|
c,
|
||||||
params.width = sizeInDp
|
appColors.textColor
|
||||||
params.setMargins(marginInDp, 0, 0, 0)
|
))
|
||||||
holder.sourceImage!!.layoutParams = params
|
|
||||||
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
if (itm.getThumbnail(c).isEmpty()) {
|
||||||
val color = generator.getColor(itm.sourcetitle)
|
|
||||||
val textDrawable = StringBuilder()
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
val color = generator.getColor(itm.getSourceTitle())
|
||||||
textDrawable.append(s[0])
|
|
||||||
|
val drawable =
|
||||||
|
TextDrawable
|
||||||
|
.builder()
|
||||||
|
.round()
|
||||||
|
.build(itm.getSourceTitle().toTextDrawableString(c), color)
|
||||||
|
|
||||||
|
binding.itemImage.setImageDrawable(drawable)
|
||||||
|
} else {
|
||||||
|
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = TextDrawable.builder().round()
|
|
||||||
|
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
|
||||||
} else {
|
} else {
|
||||||
|
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
|
||||||
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 {
|
override fun getItemCount(): Int = items.size
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
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 {
|
init {
|
||||||
handleClickListeners()
|
|
||||||
handleCustomTabActions()
|
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() {
|
private fun handleCustomTabActions() {
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
val customTabsIntent = c.buildCustomTabsIntent()
|
||||||
helper.bindCustomTabsService(app)
|
helper.bindCustomTabsService(app)
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
if (!clickBehavior) {
|
c.openItemUrl(
|
||||||
mView.setOnClickListener {
|
items,
|
||||||
openItemUrl(items[adapterPosition],
|
bindingAdapterPosition,
|
||||||
customTabsIntent,
|
items[bindingAdapterPosition].getLinkDecoded(),
|
||||||
internalBrowser,
|
customTabsIntent,
|
||||||
articleViewer,
|
internalBrowser,
|
||||||
app,
|
articleViewer,
|
||||||
c)
|
app
|
||||||
}
|
)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
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.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
|
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
|
||||||
|
abstract var items: ArrayList<Item>
|
||||||
|
abstract val api: SelfossApi
|
||||||
|
abstract val db: AppDatabase
|
||||||
|
abstract val userIdentifier: String
|
||||||
|
abstract val app: Activity
|
||||||
|
abstract val appColors: AppColors
|
||||||
|
abstract val config: Config
|
||||||
|
abstract val updateItems: (ArrayList<Item>) -> Unit
|
||||||
|
|
||||||
|
fun updateAllItems() {
|
||||||
|
items = SharedItems.focusedItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
updateItems(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unmarkSnackbar(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) {
|
||||||
|
SharedItems.unreadItem(app, api, db, i)
|
||||||
|
if (SharedItems.displayedItems == "unread") {
|
||||||
|
addItemAtIndex(i, position)
|
||||||
|
} else {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val view = s.view
|
||||||
|
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||||
|
tv.setTextColor(Color.WHITE)
|
||||||
|
s.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markSnackbar(position: Int) {
|
||||||
|
val s = Snackbar
|
||||||
|
.make(
|
||||||
|
app.findViewById(R.id.coordLayout),
|
||||||
|
R.string.marked_as_unread,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.undo_string) {
|
||||||
|
SharedItems.readItem(app, api, db, items[position])
|
||||||
|
items = SharedItems.focusedItems
|
||||||
|
if (SharedItems.displayedItems == "unread") {
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
updateItems(items)
|
||||||
|
} else {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val view = s.view
|
||||||
|
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||||
|
tv.setTextColor(Color.WHITE)
|
||||||
|
s.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleItemAtIndex(position: Int) {
|
||||||
|
if (SharedItems.unreadItemStatusAtIndex(position)) {
|
||||||
|
readItemAtIndex(position)
|
||||||
|
} else {
|
||||||
|
unreadItemAtIndex(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readItemAtIndex(position: Int) {
|
||||||
|
val i = items[position]
|
||||||
|
SharedItems.readItem(app, api, db, i)
|
||||||
|
if (SharedItems.displayedItems == "unread") {
|
||||||
|
items.remove(i)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
updateItems(items)
|
||||||
|
} else {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
unmarkSnackbar(i, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unreadItemAtIndex(position: Int) {
|
||||||
|
SharedItems.unreadItem(app, api, db, items[position])
|
||||||
|
notifyItemChanged(position)
|
||||||
|
markSnackbar(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemAtIndex(item: Item, position: Int) {
|
||||||
|
items.add(position, item)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemsAtEnd(newItems: List<Item>) {
|
||||||
|
val oldSize = items.size
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyItemRangeInserted(oldSize, newItems.size)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -2,104 +2,105 @@ package apps.amine.bou.readerforselfoss.adapters
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import android.support.constraint.ConstraintLayout
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
import apps.amine.bou.readerforselfoss.api.selfoss.Source
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.SourceListItemBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class SourcesListAdapter(private val app: Activity, private val items: ArrayList<Sources>, private val api: SelfossApi) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
class SourcesListAdapter(
|
||||||
|
private val app: Activity,
|
||||||
|
private val items: ArrayList<Source>,
|
||||||
|
private val api: SelfossApi
|
||||||
|
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
||||||
private val c: Context = app.baseContext
|
private val c: Context = app.baseContext
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
|
private lateinit var config: Config
|
||||||
|
private lateinit var binding: SourceListItemBinding
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false) as ConstraintLayout
|
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return ViewHolder(v)
|
return ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val itm = items[position]
|
val itm = items[position]
|
||||||
|
config = Config(c)
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
val color = generator.getColor(itm.title)
|
val color = generator.getColor(itm.getTitleDecoded())
|
||||||
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 =
|
||||||
|
TextDrawable
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
.builder()
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
.round()
|
||||||
|
.build(itm.getTitleDecoded().toTextDrawableString(c), color)
|
||||||
|
binding.itemImage.setImageDrawable(drawable)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.sourceTitle!!.text = itm.title
|
binding.sourceTitle.text = itm.getTitleDecoded()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = items.size
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var sourceTitle: TextView? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun 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
|
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||||
|
|
||||||
deleteBtn.setOnClickListener {
|
deleteBtn.setOnClickListener {
|
||||||
val (id) = items[adapterPosition]
|
if (c.isNetworkAccessible(null)) {
|
||||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
val (id) = items[adapterPosition]
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
override fun onResponse(
|
||||||
items.removeAt(adapterPosition)
|
call: Call<SuccessResponse>,
|
||||||
notifyItemRemoved(adapterPosition)
|
response: Response<SuccessResponse>
|
||||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
) {
|
||||||
} else {
|
if (response.body() != null && response.body()!!.isSuccess) {
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
items.removeAt(adapterPosition)
|
||||||
|
notifyItemRemoved(adapterPosition)
|
||||||
|
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
R.string.can_delete_source,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
}
|
app,
|
||||||
})
|
R.string.can_delete_source,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
package apps.amine.bou.readerforselfoss.api.mercury
|
||||||
|
|
||||||
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
@ -8,25 +7,29 @@ import retrofit2.Call
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
class MercuryApi() {
|
||||||
class MercuryApi(private val key: String) {
|
|
||||||
private val service: MercuryService
|
private val service: MercuryService
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
val interceptor = HttpLoggingInterceptor()
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
interceptor.level = HttpLoggingInterceptor.Level.NONE
|
||||||
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.setLenient()
|
.setLenient()
|
||||||
.create()
|
.create()
|
||||||
val retrofit = Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client)
|
val retrofit =
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.baseUrl("https://www.amine-bou.fr")
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
service = retrofit.create(MercuryService::class.java)
|
service = retrofit.create(MercuryService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseUrl(url: String): Call<ParsedContent> {
|
fun parseUrl(url: String): Call<ParsedContent> {
|
||||||
return service.parseUrl(url, this.key)
|
return service.parseUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,39 +2,43 @@ package apps.amine.bou.readerforselfoss.api.mercury
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class ParsedContent(
|
||||||
class ParsedContent(val title: String,
|
@SerializedName("title") val title: String,
|
||||||
val content: String,
|
@SerializedName("content") val content: String?,
|
||||||
val date_published: String,
|
@SerializedName("date_published") val date_published: String,
|
||||||
val lead_image_url: String,
|
@SerializedName("lead_image_url") val lead_image_url: String?,
|
||||||
val dek: String,
|
@SerializedName("dek") val dek: String,
|
||||||
val url: String,
|
@SerializedName("url") val url: String,
|
||||||
val domain: String,
|
@SerializedName("domain") val domain: String,
|
||||||
val excerpt: String,
|
@SerializedName("excerpt") val excerpt: String,
|
||||||
val total_pages: Int,
|
@SerializedName("total_pages") val total_pages: Int,
|
||||||
val rendered_pages: Int,
|
@SerializedName("rendered_pages") val rendered_pages: Int,
|
||||||
val next_page_url: String) : Parcelable {
|
@SerializedName("next_page_url") val next_page_url: String
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
|
@JvmField
|
||||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
val CREATOR: Parcelable.Creator<ParsedContent> =
|
||||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
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(
|
constructor(source: Parcel) : this(
|
||||||
title = source.readString(),
|
title = source.readString().orEmpty(),
|
||||||
content = source.readString(),
|
content = source.readString(),
|
||||||
date_published = source.readString(),
|
date_published = source.readString().orEmpty(),
|
||||||
lead_image_url = source.readString(),
|
lead_image_url = source.readString(),
|
||||||
dek = source.readString(),
|
dek = source.readString().orEmpty(),
|
||||||
url = source.readString(),
|
url = source.readString().orEmpty(),
|
||||||
domain = source.readString(),
|
domain = source.readString().orEmpty(),
|
||||||
excerpt = source.readString(),
|
excerpt = source.readString().orEmpty(),
|
||||||
total_pages = source.readInt(),
|
total_pages = source.readInt(),
|
||||||
rendered_pages = source.readInt(),
|
rendered_pages = source.readInt(),
|
||||||
next_page_url = source.readString()
|
next_page_url = source.readString().orEmpty()
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
package apps.amine.bou.readerforselfoss.api.mercury
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
interface MercuryService {
|
interface MercuryService {
|
||||||
@GET("parser")
|
@GET("parser.php")
|
||||||
fun parseUrl(@Query("url") url: String, @Header("x-api-key") key: String): Call<ParsedContent>
|
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.google.gson.JsonDeserializationContext
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonDeserializer
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
|
||||||
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
||||||
|
|
||||||
@Throws(JsonParseException::class)
|
@Throws(JsonParseException::class)
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): Boolean? =
|
||||||
try {
|
try {
|
||||||
json.asInt == 1
|
json.asInt == 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
||||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
||||||
import com.burgstaller.okhttp.DispatchingAuthenticator
|
import com.burgstaller.okhttp.DispatchingAuthenticator
|
||||||
@ -11,124 +13,233 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator
|
|||||||
import com.burgstaller.okhttp.digest.Credentials
|
import com.burgstaller.okhttp.digest.Credentials
|
||||||
import com.burgstaller.okhttp.digest.DigestAuthenticator
|
import com.burgstaller.okhttp.digest.DigestAuthenticator
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.*
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class SelfossApi(
|
||||||
|
c: Context,
|
||||||
|
callingActivity: Activity?,
|
||||||
|
isWithSelfSignedCert: Boolean,
|
||||||
|
timeout: Long
|
||||||
|
) {
|
||||||
|
|
||||||
class SelfossApi(c: Context) {
|
private lateinit var service: SelfossService
|
||||||
|
|
||||||
private val service: SelfossService
|
|
||||||
private val config: Config = Config(c)
|
private val config: Config = Config(c)
|
||||||
private val userName: String
|
private val userName: String
|
||||||
private val password: String
|
private val password: String
|
||||||
|
|
||||||
init {
|
fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder =
|
||||||
|
if (isWithSelfSignedCert) {
|
||||||
|
getUnsafeHttpClient()
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
if (timeout != -1L) {
|
||||||
|
this.readTimeout(timeout, TimeUnit.SECONDS)
|
||||||
|
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
val httpBuilder = OkHttpClient.Builder()
|
fun Credentials.createAuthenticator(): DispatchingAuthenticator =
|
||||||
|
DispatchingAuthenticator.Builder()
|
||||||
|
.with("digest", DigestAuthenticator(this))
|
||||||
|
.with("basic", BasicAuthenticator(this))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
|
||||||
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
||||||
|
return OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.maybeWithSettingsTimeout(timeout)
|
||||||
|
.maybeWithSelfSigned(isWithSelfSignedCert)
|
||||||
|
.authenticator(CachingAuthenticatorDecorator(this, authCache))
|
||||||
|
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
||||||
|
.addInterceptor(object: Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request: Request = chain.request()
|
||||||
|
val response: Response = chain.proceed(request)
|
||||||
|
|
||||||
val httpUserName = config.httpUserLogin
|
if (response.code == 408) {
|
||||||
val httpPassword = config.httpUserPassword
|
return response
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val credentials = Credentials(httpUserName, httpPassword)
|
init {
|
||||||
val basicAuthenticator = BasicAuthenticator(credentials)
|
userName = config.userLogin
|
||||||
val digestAuthenticator = DigestAuthenticator(credentials)
|
password = config.userPassword
|
||||||
|
|
||||||
// note that all auth schemes should be registered as lowercase!
|
val authenticator =
|
||||||
val authenticator = DispatchingAuthenticator.Builder()
|
Credentials(
|
||||||
.with("digest", digestAuthenticator)
|
config.httpUserLogin,
|
||||||
.with("basic", basicAuthenticator)
|
config.httpUserPassword
|
||||||
.build()
|
).createAuthenticator()
|
||||||
|
|
||||||
val client = httpBuilder
|
val gson =
|
||||||
.authenticator(CachingAuthenticatorDecorator(authenticator, authCache))
|
GsonBuilder()
|
||||||
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
||||||
.addInterceptor(interceptor)
|
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
val builder = GsonBuilder()
|
|
||||||
builder.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
|
||||||
|
|
||||||
val gson = builder
|
|
||||||
.setLenient()
|
.setLenient()
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
userName = config.userLogin
|
val logging = HttpLoggingInterceptor()
|
||||||
password = config.userPassword
|
|
||||||
val retrofit = Retrofit.Builder().baseUrl(config.baseUrl).client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
logging.level = HttpLoggingInterceptor.Level.NONE
|
||||||
service = retrofit.create(SelfossService::class.java)
|
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
|
||||||
|
|
||||||
|
val timeoutCode = 504
|
||||||
|
httpClient
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val res = chain.proceed(chain.request())
|
||||||
|
if (res.code == timeoutCode) {
|
||||||
|
throw SocketTimeoutException("timeout")
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
.addInterceptor(logging)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request()
|
||||||
|
try {
|
||||||
|
chain.proceed(request)
|
||||||
|
} catch (e: SocketTimeoutException) {
|
||||||
|
Response.Builder()
|
||||||
|
.code(timeoutCode)
|
||||||
|
.protocol(Protocol.HTTP_2)
|
||||||
|
.body("".toResponseBody("text/plain".toMediaTypeOrNull()))
|
||||||
|
.message("")
|
||||||
|
.request(request)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val retrofit =
|
||||||
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.baseUrl(config.baseUrl)
|
||||||
|
.client(httpClient.build())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
|
service = retrofit.create(SelfossService::class.java)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
if (callingActivity != null) {
|
||||||
|
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(): Call<SuccessResponse> {
|
fun login(): Call<SuccessResponse> =
|
||||||
return service.loginToSelfoss(config.userLogin, config.userPassword)
|
service.loginToSelfoss(config.userLogin, config.userPassword)
|
||||||
}
|
|
||||||
|
|
||||||
fun readItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
suspend fun readItems(
|
||||||
getItems("read", tag, sourceId, search)
|
itemsNumber: Int,
|
||||||
|
offset: Int
|
||||||
|
): retrofit2.Response<List<Item>> =
|
||||||
|
getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
|
||||||
|
|
||||||
fun unreadItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
suspend fun newItems(
|
||||||
getItems("unread", tag, sourceId, search)
|
itemsNumber: Int,
|
||||||
|
offset: Int
|
||||||
|
): retrofit2.Response<List<Item>> =
|
||||||
|
getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
|
||||||
|
|
||||||
fun starredItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
suspend fun starredItems(
|
||||||
getItems("starred", tag, sourceId, search)
|
itemsNumber: Int,
|
||||||
|
offset: Int
|
||||||
|
): retrofit2.Response<List<Item>> =
|
||||||
|
getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
|
||||||
|
|
||||||
private fun getItems(type: String, tag: String?, sourceId: Long?, search: String?): Call<List<Item>> {
|
fun allItems(): Call<List<Item>> =
|
||||||
return service.getItems(type, tag, sourceId, search, userName, password)
|
service.allItems(userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun markItem(itemId: String): Call<SuccessResponse> {
|
suspend fun allNewItems(): retrofit2.Response<List<Item>> =
|
||||||
return service.markAsRead(itemId, userName, password)
|
getItems("unread", null, null, null, 200, 0)
|
||||||
}
|
|
||||||
|
|
||||||
fun unmarkItem(itemId: String): Call<SuccessResponse> {
|
suspend fun allReadItems(): retrofit2.Response<List<Item>> =
|
||||||
return service.unmarkAsRead(itemId, userName, password)
|
getItems("read", null, null, null, 200, 0)
|
||||||
}
|
|
||||||
|
|
||||||
fun readAll(ids: List<String>): Call<SuccessResponse> {
|
suspend fun allStarredItems(): retrofit2.Response<List<Item>> =
|
||||||
return service.markAllAsRead(ids, userName, password)
|
getItems("read", null, null, null, 200, 0)
|
||||||
}
|
|
||||||
|
|
||||||
fun starrItem(itemId: String): Call<SuccessResponse> {
|
private suspend fun getItems(
|
||||||
return service.starr(itemId, userName, password)
|
type: String,
|
||||||
}
|
tag: String?,
|
||||||
|
sourceId: Long?,
|
||||||
|
search: String?,
|
||||||
|
items: Int,
|
||||||
|
offset: Int
|
||||||
|
): retrofit2.Response<List<Item>> =
|
||||||
|
service.getItems(type, tag, sourceId, search, null, userName, password, items, offset)
|
||||||
|
|
||||||
|
suspend fun updateItems(
|
||||||
|
updatedSince: String
|
||||||
|
): retrofit2.Response<List<Item>> =
|
||||||
|
service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0)
|
||||||
|
|
||||||
fun unstarrItem(itemId: String): Call<SuccessResponse> {
|
fun markItem(itemId: String): Call<SuccessResponse> =
|
||||||
return service.unstarr(itemId, userName, password)
|
service.markAsRead(itemId, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
val stats: Call<Stats>
|
fun unmarkItem(itemId: String): Call<SuccessResponse> =
|
||||||
get() = service.stats(userName, password)
|
service.unmarkAsRead(itemId, userName, password)
|
||||||
|
|
||||||
|
suspend fun readAll(ids: List<String>): SuccessResponse =
|
||||||
|
service.markAllAsRead(ids, userName, password)
|
||||||
|
|
||||||
|
fun starrItem(itemId: String): Call<SuccessResponse> =
|
||||||
|
service.starr(itemId, userName, password)
|
||||||
|
|
||||||
|
fun unstarrItem(itemId: String): Call<SuccessResponse> =
|
||||||
|
service.unstarr(itemId, userName, password)
|
||||||
|
|
||||||
|
suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
|
||||||
|
|
||||||
val tags: Call<List<Tag>>
|
val tags: Call<List<Tag>>
|
||||||
get() = service.tags(userName, password)
|
get() = service.tags(userName, password)
|
||||||
|
|
||||||
fun update(): Call<String> {
|
fun update(): Call<String> =
|
||||||
return service.update(userName, password)
|
service.update(userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
val sources: Call<List<Sources>>
|
val apiVersion: Call<ApiVersion>
|
||||||
|
get() = service.version()
|
||||||
|
|
||||||
|
val sources: Call<List<Source>>
|
||||||
get() = service.sources(userName, password)
|
get() = service.sources(userName, password)
|
||||||
|
|
||||||
fun deleteSource(id: String): Call<SuccessResponse> {
|
fun deleteSource(id: String): Call<SuccessResponse> =
|
||||||
return service.deleteSource(id, userName, password)
|
service.deleteSource(id, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun spouts(): Call<Map<String, Spout>> {
|
fun spouts(): Call<Map<String, Spout>> =
|
||||||
return service.spouts(userName, password)
|
service.spouts(userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun createSource(title: String, url: String, spout: String, tags: String, filter: String): Call<SuccessResponse> {
|
fun createSource(
|
||||||
return service.createSource(title, url, spout, tags, filter, userName, password)
|
title: String,
|
||||||
}
|
url: String,
|
||||||
|
spout: String,
|
||||||
|
tags: String,
|
||||||
|
filter: String
|
||||||
|
): Call<SuccessResponse> =
|
||||||
|
service.createSource(title, url, spout, tags, filter, userName, password)
|
||||||
|
|
||||||
|
fun createSourceApi2(
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
spout: String,
|
||||||
|
tags: List<String>,
|
||||||
|
filter: String
|
||||||
|
): Call<SuccessResponse> =
|
||||||
|
service.createSourceApi2(title, url, spout, tags, filter, userName, password)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
suspend fun getAndStoreAllItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.allNewItems(), db, true)
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.allReadItems(), db, false)
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.allStarredItems(), db, false)
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launch { SharedItems.updateDatabase(db) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateItems(context: Context, api: SelfossApi, db: AppDatabase) = coroutineScope {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.updateItems(SharedItems.items[0].datetime), db, true)
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshFocusedItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
val response = when (SharedItems.displayedItems) {
|
||||||
|
"read" -> api.readItems(200, 0)
|
||||||
|
"unread" -> api.newItems(200, 0)
|
||||||
|
"starred" -> api.starredItems(200, 0)
|
||||||
|
else -> api.readItems(200, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>)
|
||||||
|
SharedItems.updateDatabase(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.readItems( 200, offset), db, false)
|
||||||
|
SharedItems.fetchedAll = true
|
||||||
|
SharedItems.updateDatabase(db)
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
try {
|
||||||
|
if (!SharedItems.fetchedUnread) {
|
||||||
|
SharedItems.clearDBItems(db)
|
||||||
|
}
|
||||||
|
enqueueArticles(api.newItems(200, offset), db, false)
|
||||||
|
SharedItems.fetchedUnread = true
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
SharedItems.updateDatabase(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
try {
|
||||||
|
enqueueArticles(api.starredItems(200, offset), db, false)
|
||||||
|
SharedItems.fetchedStarred = true
|
||||||
|
SharedItems.updateDatabase(db)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun readAll(context: Context, api: SelfossApi, db: AppDatabase): Boolean {
|
||||||
|
var success = false
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
try {
|
||||||
|
val ids = SharedItems.focusedItems.map { it.id }
|
||||||
|
if (ids.isNotEmpty()) {
|
||||||
|
val result = api.readAll(ids)
|
||||||
|
SharedItems.readItems(db, ids)
|
||||||
|
success = result.isSuccess
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reloadBadges(context: Context, api: SelfossApi) = withContext(Dispatchers.IO) {
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
try {
|
||||||
|
val response = api.stats()
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val badges = response.body()
|
||||||
|
SharedItems.badgeUnread = badges!!.unread
|
||||||
|
SharedItems.badgeAll = badges.total
|
||||||
|
SharedItems.badgeStarred = badges.starred
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {}
|
||||||
|
} else {
|
||||||
|
SharedItems.computeBadges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enqueueArticles(response: Response<List<Item>>, db: AppDatabase, clearDatabase: Boolean) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
if (clearDatabase) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
SharedItems.clearDBItems(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val allItems = response.body() as ArrayList<Item>
|
||||||
|
SharedItems.appendNewItems(allItems)
|
||||||
|
}
|
||||||
|
}
|
@ -4,56 +4,102 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.text.Html
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
private fun constructUrl(config: Config?, path: String, file: String?): String {
|
||||||
|
return if (file.isEmptyOrNullOrNullString()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
||||||
|
baseUriBuilder.appendPath(path).appendPath(file)
|
||||||
|
|
||||||
private fun constructUrl(config: Config?, path: String, file: String): String {
|
baseUriBuilder.toString()
|
||||||
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
}
|
||||||
baseUriBuilder.appendPath(path).appendPath(file)
|
|
||||||
|
|
||||||
return if (isEmptyOrNullOrNullString(file)) ""
|
|
||||||
else baseUriBuilder.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Tag(
|
||||||
|
@SerializedName("tag") val tag: String,
|
||||||
|
@SerializedName("color") val color: String,
|
||||||
|
@SerializedName("unread") val unread: Int
|
||||||
|
) {
|
||||||
|
fun getTitleDecoded(): String {
|
||||||
|
return Html.fromHtml(tag).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class Tag(val tag: String, val color: String, val unread: Int)
|
class SuccessResponse(@SerializedName("success") val success: Boolean) {
|
||||||
|
|
||||||
class SuccessResponse(val success: Boolean) {
|
|
||||||
val isSuccess: Boolean
|
val isSuccess: Boolean
|
||||||
get() = success
|
get() = success
|
||||||
}
|
}
|
||||||
|
|
||||||
class Stats(val total: Int, val unread: Int, val starred: Int)
|
class Stats(
|
||||||
|
@SerializedName("total") val total: Int,
|
||||||
|
@SerializedName("unread") val unread: Int,
|
||||||
|
@SerializedName("starred") val starred: Int
|
||||||
|
)
|
||||||
|
|
||||||
data class Spout(val name: String, val description: String)
|
data class Spout(
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("description") val description: String
|
||||||
|
)
|
||||||
|
|
||||||
data class Sources(val id: String,
|
data class ApiVersion(
|
||||||
val title: String,
|
@SerializedName("version") val version: String?,
|
||||||
val tags: String,
|
@SerializedName("apiversion") val apiversion: String?
|
||||||
val spout: String,
|
) {
|
||||||
val error: String,
|
fun getApiMajorVersion() : Int {
|
||||||
val icon: String) {
|
var versionNumber = 0
|
||||||
|
if (apiversion != null) {
|
||||||
|
versionNumber = apiversion.substringBefore(".").toInt()
|
||||||
|
}
|
||||||
|
return versionNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
@SerializedName("id") val id: String,
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("tags") val tags: SelfossTagType,
|
||||||
|
@SerializedName("spout") val spout: String,
|
||||||
|
@SerializedName("error") val error: String,
|
||||||
|
@SerializedName("icon") val icon: String
|
||||||
|
) {
|
||||||
var config: Config? = null
|
var config: Config? = null
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
fun getIcon(app: Context): String {
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
config = Config(app)
|
config = Config(app)
|
||||||
}
|
}
|
||||||
return constructUrl(config,"favicons", icon)
|
return constructUrl(config, "favicons", icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTitleDecoded(): String {
|
||||||
|
return Html.fromHtml(title).toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Item(val id: String,
|
data class Item(
|
||||||
val datetime: String,
|
@SerializedName("id") val id: String,
|
||||||
val title: String,
|
@SerializedName("datetime") val datetime: String,
|
||||||
val unread: Boolean,
|
@SerializedName("title") val title: String,
|
||||||
val starred: Boolean,
|
@SerializedName("content") val content: String,
|
||||||
val thumbnail: String,
|
@SerializedName("unread") var unread: Boolean,
|
||||||
val icon: String,
|
@SerializedName("starred") var starred: Boolean,
|
||||||
val link: String,
|
@SerializedName("thumbnail") val thumbnail: String?,
|
||||||
val sourcetitle: String) : Parcelable {
|
@SerializedName("icon") val icon: String?,
|
||||||
|
@SerializedName("link") val link: String,
|
||||||
|
@SerializedName("sourcetitle") val sourcetitle: String,
|
||||||
|
@SerializedName("tags") val tags: SelfossTagType
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
var config: Config? = null
|
var config: Config? = null
|
||||||
|
|
||||||
@ -65,15 +111,17 @@ data class Item(val id: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: Parcel) : this(
|
constructor(source: Parcel) : this(
|
||||||
id = source.readString(),
|
id = source.readString().orEmpty(),
|
||||||
datetime = source.readString(),
|
datetime = source.readString().orEmpty(),
|
||||||
title = source.readString(),
|
title = source.readString().orEmpty(),
|
||||||
unread = 0.toByte() != source.readByte(),
|
content = source.readString().orEmpty(),
|
||||||
starred = 0.toByte() != source.readByte(),
|
unread = 0.toByte() != source.readByte(),
|
||||||
thumbnail = source.readString(),
|
starred = 0.toByte() != source.readByte(),
|
||||||
icon = source.readString(),
|
thumbnail = source.readString(),
|
||||||
link = source.readString(),
|
icon = source.readString(),
|
||||||
sourcetitle = source.readString()
|
link = source.readString().orEmpty(),
|
||||||
|
sourcetitle = source.readString().orEmpty(),
|
||||||
|
tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("")
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
@ -82,12 +130,14 @@ data class Item(val id: String,
|
|||||||
dest.writeString(id)
|
dest.writeString(id)
|
||||||
dest.writeString(datetime)
|
dest.writeString(datetime)
|
||||||
dest.writeString(title)
|
dest.writeString(title)
|
||||||
|
dest.writeString(content)
|
||||||
dest.writeByte((if (unread) 1 else 0))
|
dest.writeByte((if (unread) 1 else 0))
|
||||||
dest.writeByte((if (starred) 1 else 0))
|
dest.writeByte((if (starred) 1 else 0))
|
||||||
dest.writeString(thumbnail)
|
dest.writeString(thumbnail)
|
||||||
dest.writeString(icon)
|
dest.writeString(icon)
|
||||||
dest.writeString(link)
|
dest.writeString(link)
|
||||||
dest.writeString(sourcetitle)
|
dest.writeString(sourcetitle)
|
||||||
|
dest.writeParcelable(tags, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
fun getIcon(app: Context): String {
|
||||||
@ -104,24 +154,98 @@ data class Item(val id: String,
|
|||||||
return constructUrl(config, "thumbnails", thumbnail)
|
return constructUrl(config, "thumbnails", thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getImages() : ArrayList<String> {
|
||||||
|
var allImages = ArrayList<String>()
|
||||||
|
|
||||||
|
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
|
||||||
|
val url = image.attr("src")
|
||||||
|
if (url.toLowerCase().contains(".jpg") ||
|
||||||
|
url.toLowerCase().contains(".jpeg") ||
|
||||||
|
url.toLowerCase().contains(".png") ||
|
||||||
|
url.toLowerCase().contains(".webp"))
|
||||||
|
{
|
||||||
|
allImages.add(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allImages
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preloadImages(context: Context) : Boolean {
|
||||||
|
val imageUrls = this.getImages()
|
||||||
|
|
||||||
|
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (url in imageUrls) {
|
||||||
|
if ( URLUtil.isValidUrl(url)) {
|
||||||
|
val image = Glide.with(context).asBitmap()
|
||||||
|
.apply(glideOptions)
|
||||||
|
.load(url).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e : Error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTitleDecoded(): String {
|
||||||
|
return Html.fromHtml(title).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceTitle(): String {
|
||||||
|
return Html.fromHtml(sourcetitle).toString()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: maybe find a better way to handle these kind of urls
|
// TODO: maybe find a better way to handle these kind of urls
|
||||||
fun getLinkDecoded(): String {
|
fun getLinkDecoded(): String {
|
||||||
var stringUrl: String
|
var stringUrl: String
|
||||||
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
stringUrl =
|
||||||
if (link.contains("&url=")) {
|
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
||||||
stringUrl = link.substringAfter("&url=")
|
if (link.contains("&url=")) {
|
||||||
} else {
|
link.substringAfter("&url=")
|
||||||
stringUrl = this.link.replace("&", "&")
|
} else {
|
||||||
}
|
this.link.replace("&", "&")
|
||||||
} else {
|
}
|
||||||
stringUrl = this.link.replace("&", "&")
|
} else {
|
||||||
}
|
this.link.replace("&", "&")
|
||||||
|
}
|
||||||
|
|
||||||
// handle :443 => https
|
// handle :443 => https
|
||||||
if (stringUrl.contains(":443")) {
|
if (stringUrl.contains(":443")) {
|
||||||
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle url not starting with http
|
||||||
|
if (stringUrl.startsWith("//")) {
|
||||||
|
stringUrl = "http:$stringUrl"
|
||||||
|
}
|
||||||
|
|
||||||
return stringUrl
|
return stringUrl
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SelfossTagType(val tags: String) : Parcelable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField val CREATOR: Parcelable.Creator<SelfossTagType> =
|
||||||
|
object : Parcelable.Creator<SelfossTagType> {
|
||||||
|
override fun createFromParcel(source: Parcel): SelfossTagType =
|
||||||
|
SelfossTagType(source)
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: Parcel) : this(
|
||||||
|
tags = source.readString().orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun describeContents() = 0
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeString(tags)
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,70 +1,141 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
import retrofit2.Response
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.Field
|
import retrofit2.http.Field
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
internal interface SelfossService {
|
internal interface SelfossService {
|
||||||
|
|
||||||
@GET("login")
|
@GET("login")
|
||||||
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
||||||
|
|
||||||
@GET("items")
|
@GET("items")
|
||||||
fun getItems(@Query("type") type: String,
|
suspend fun getItems(
|
||||||
@Query("tag") tag: String?,
|
@Query("type") type: String,
|
||||||
@Query("source") source: Long?,
|
@Query("tag") tag: String?,
|
||||||
@Query("search") search: String?,
|
@Query("source") source: Long?,
|
||||||
@Query("username") username: String,
|
@Query("search") search: String?,
|
||||||
@Query("password") password: String): Call<List<Item>>
|
@Query("updatedsince") updatedSince: String?,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String,
|
||||||
|
@Query("items") items: Int,
|
||||||
|
@Query("offset") offset: Int
|
||||||
|
): Response<List<Item>>
|
||||||
|
|
||||||
|
@GET("items")
|
||||||
|
fun allItems(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<List<Item>>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("mark/{id}")
|
@POST("mark/{id}")
|
||||||
fun markAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun markAsRead(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("unmark/{id}")
|
@POST("unmark/{id}")
|
||||||
fun unmarkAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun unmarkAsRead(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("mark")
|
@POST("mark")
|
||||||
fun markAllAsRead(@Field("ids[]") ids: List<String>, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
suspend fun markAllAsRead(
|
||||||
|
@Field("ids[]") ids: List<String>,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): SuccessResponse
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("starr/{id}")
|
@POST("starr/{id}")
|
||||||
fun starr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun starr(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("unstarr/{id}")
|
@POST("unstarr/{id}")
|
||||||
fun unstarr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun unstarr(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@GET("stats")
|
@GET("stats")
|
||||||
fun stats(@Query("username") username: String, @Query("password") password: String): Call<Stats>
|
suspend fun stats(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Response<Stats>
|
||||||
|
|
||||||
@GET("tags")
|
@GET("tags")
|
||||||
fun tags(@Query("username") username: String, @Query("password") password: String): Call<List<Tag>>
|
fun tags(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<List<Tag>>
|
||||||
|
|
||||||
@GET("update")
|
@GET("update")
|
||||||
fun update(@Query("username") username: String, @Query("password") password: String): Call<String>
|
fun update(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<String>
|
||||||
|
|
||||||
@GET("sources/spouts")
|
@GET("sources/spouts")
|
||||||
fun spouts(@Query("username") username: String, @Query("password") password: String): Call<Map<String, Spout>>
|
fun spouts(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<Map<String, Spout>>
|
||||||
|
|
||||||
@GET("sources/list")
|
@GET("sources/list")
|
||||||
fun sources(@Query("username") username: String, @Query("password") password: String): Call<List<Sources>>
|
fun sources(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<List<Source>>
|
||||||
|
|
||||||
|
@GET("api/about")
|
||||||
|
fun version(): Call<ApiVersion>
|
||||||
|
|
||||||
@DELETE("source/{id}")
|
@DELETE("source/{id}")
|
||||||
fun deleteSource(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun deleteSource(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("source")
|
@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>
|
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>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("source")
|
||||||
|
fun createSourceApi2(
|
||||||
|
@Field("title") title: String,
|
||||||
|
@Field("url") url: String,
|
||||||
|
@Field("spout") spout: String,
|
||||||
|
@Field("tags[]") tags: List<String>,
|
||||||
|
@Field("filter") filter: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext
|
||||||
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> {
|
||||||
|
|
||||||
|
@Throws(JsonParseException::class)
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): SelfossTagType? =
|
||||||
|
if (json.isJsonArray) {
|
||||||
|
SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() })
|
||||||
|
} else {
|
||||||
|
SelfossTagType(json.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.background
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import apps.amine.bou.readerforselfoss.MainActivity
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.SharedItems
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
|
lateinit var db: AppDatabase
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val settings =
|
||||||
|
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
|
||||||
|
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
|
||||||
|
if (periodicRefresh) {
|
||||||
|
val api = SelfossApi(
|
||||||
|
this.context,
|
||||||
|
null,
|
||||||
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
sharedPref.getString("api_timeout", "-1")!!.toLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isNetworkAvailable(context)) {
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val notificationManager =
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
val notification =
|
||||||
|
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
|
||||||
|
.setContentTitle(context.getString(R.string.loading_notification_title))
|
||||||
|
.setContentText(context.getString(R.string.loading_notification_text))
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(PRIORITY_LOW)
|
||||||
|
.setChannelId(Config.syncChannelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
|
||||||
|
|
||||||
|
notificationManager.notify(1, notification.build())
|
||||||
|
|
||||||
|
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
|
||||||
|
|
||||||
|
db = Room.databaseBuilder(
|
||||||
|
applicationContext,
|
||||||
|
AppDatabase::class.java, "selfoss-database"
|
||||||
|
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
|
||||||
|
.addMigrations(MIGRATION_3_4).build()
|
||||||
|
|
||||||
|
val actions = db.actionsDao().actions()
|
||||||
|
|
||||||
|
actions.forEach { action ->
|
||||||
|
when {
|
||||||
|
action.read -> doAndReportOnFail(
|
||||||
|
api.markItem(action.articleId),
|
||||||
|
action
|
||||||
|
)
|
||||||
|
action.unread -> doAndReportOnFail(
|
||||||
|
api.unmarkItem(action.articleId),
|
||||||
|
action
|
||||||
|
)
|
||||||
|
action.starred -> doAndReportOnFail(
|
||||||
|
api.starrItem(action.articleId),
|
||||||
|
action
|
||||||
|
)
|
||||||
|
action.unstarred -> doAndReportOnFail(
|
||||||
|
api.unstarrItem(action.articleId),
|
||||||
|
action
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAndStoreAllItems(context, api, db)
|
||||||
|
SharedItems.updateDatabase(db)
|
||||||
|
storeItems(notifyNewItems, notificationManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val apiItems = SharedItems.items
|
||||||
|
|
||||||
|
|
||||||
|
val newSize = apiItems.filter { it.unread }.size
|
||||||
|
if (notifyNewItems && newSize > 0) {
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
|
||||||
|
|
||||||
|
val newItemsNotification =
|
||||||
|
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
|
||||||
|
.setContentTitle(context.getString(R.string.new_items_notification_title))
|
||||||
|
.setContentText(
|
||||||
|
context.getString(
|
||||||
|
R.string.new_items_notification_text,
|
||||||
|
newSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setPriority(PRIORITY_DEFAULT)
|
||||||
|
.setChannelId(Config.newItemsChannelId)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||||
|
|
||||||
|
Timer("", false).schedule(4000) {
|
||||||
|
notificationManager.notify(2, newItemsNotification.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiItems.map { it.preloadImages(context) }
|
||||||
|
Timer("", false).schedule(4000) {
|
||||||
|
notificationManager.cancel(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
|
||||||
|
call.enqueue(object : Callback<T> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<T>,
|
||||||
|
response: Response<T>
|
||||||
|
) {
|
||||||
|
thread {
|
||||||
|
db.actionsDao().delete(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,603 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.fragments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import android.view.*
|
||||||
|
import android.webkit.*
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.room.Room
|
||||||
|
import apps.amine.bou.readerforselfoss.ImageActivity
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
|
||||||
|
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.*
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class ArticleFragment : Fragment() {
|
||||||
|
private lateinit var pageNumber: Number
|
||||||
|
private var fontSize: Int = 16
|
||||||
|
private lateinit var allItems: ArrayList<Item>
|
||||||
|
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null;
|
||||||
|
private lateinit var url: String
|
||||||
|
private lateinit var contentText: String
|
||||||
|
private lateinit var contentSource: String
|
||||||
|
private lateinit var contentImage: String
|
||||||
|
private lateinit var contentTitle: String
|
||||||
|
private lateinit var allImages : ArrayList<String>
|
||||||
|
private lateinit var editor: SharedPreferences.Editor
|
||||||
|
private lateinit var fab: FloatingActionButton
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
private lateinit var db: AppDatabase
|
||||||
|
private lateinit var textAlignment: String
|
||||||
|
private lateinit var config: Config
|
||||||
|
private var _binding: FragmentArticleBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
|
||||||
|
private var typeface: Typeface? = null
|
||||||
|
private var resId: Int = 0
|
||||||
|
private var font = ""
|
||||||
|
private var staticBar = false
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (mCustomTabActivityHelper != null) {
|
||||||
|
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(requireActivity())
|
||||||
|
config = Config(requireActivity())
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
pageNumber = requireArguments().getInt(ARG_POSITION)
|
||||||
|
allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
|
||||||
|
|
||||||
|
db = Room.databaseBuilder(
|
||||||
|
requireContext(),
|
||||||
|
AppDatabase::class.java, "selfoss-database"
|
||||||
|
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
try {
|
||||||
|
_binding = FragmentArticleBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
url = allItems[pageNumber.toInt()].getLinkDecoded()
|
||||||
|
contentText = allItems[pageNumber.toInt()].content
|
||||||
|
contentTitle = allItems[pageNumber.toInt()].getTitleDecoded()
|
||||||
|
contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity())
|
||||||
|
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
|
||||||
|
allImages = allItems[pageNumber.toInt()].getImages()
|
||||||
|
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
editor = prefs.edit()
|
||||||
|
fontSize = prefs.getString("reader_font_size", "16")!!.toInt()
|
||||||
|
staticBar = prefs.getBoolean("reader_static_bar", false)
|
||||||
|
|
||||||
|
font = prefs.getString("reader_font", "")!!
|
||||||
|
if (font.isNotEmpty()) {
|
||||||
|
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
|
||||||
|
typeface = try {
|
||||||
|
ResourcesCompat.getFont(requireContext(), resId)!!
|
||||||
|
} catch (e: java.lang.Exception) {
|
||||||
|
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
|
||||||
|
// Just to be sure
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAlignment()
|
||||||
|
|
||||||
|
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
val api = SelfossApi(
|
||||||
|
requireContext(),
|
||||||
|
requireActivity(),
|
||||||
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
fab = binding.fab
|
||||||
|
|
||||||
|
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||||
|
|
||||||
|
fab.rippleColor = appColors.colorAccentDark
|
||||||
|
|
||||||
|
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
||||||
|
floatingToolbar.attachFab(fab)
|
||||||
|
|
||||||
|
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
|
||||||
|
|
||||||
|
val customTabsIntent = requireActivity().buildCustomTabsIntent()
|
||||||
|
mCustomTabActivityHelper = CustomTabActivityHelper()
|
||||||
|
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
|
||||||
|
|
||||||
|
|
||||||
|
floatingToolbar.setClickListener(
|
||||||
|
object : FloatingToolbar.ItemClickListener {
|
||||||
|
override fun onItemClick(item: MenuItem) {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
|
||||||
|
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||||
|
R.id.open_action -> requireActivity().openItemUrl(
|
||||||
|
allItems,
|
||||||
|
pageNumber.toInt(),
|
||||||
|
url,
|
||||||
|
customTabsIntent,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
requireActivity()
|
||||||
|
)
|
||||||
|
R.id.unread_action -> if (context != null) {
|
||||||
|
if (allItems[pageNumber.toInt()].unread) {
|
||||||
|
SharedItems.readItem(
|
||||||
|
context!!,
|
||||||
|
api,
|
||||||
|
db,
|
||||||
|
allItems[pageNumber.toInt()]
|
||||||
|
)
|
||||||
|
allItems[pageNumber.toInt()].unread = false
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.marked_as_read,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
SharedItems.unreadItem(
|
||||||
|
context!!,
|
||||||
|
api,
|
||||||
|
db,
|
||||||
|
allItems[pageNumber.toInt()]
|
||||||
|
)
|
||||||
|
allItems[pageNumber.toInt()].unread = true
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.marked_as_unread,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(item: MenuItem?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (staticBar) {
|
||||||
|
fab.hide()
|
||||||
|
floatingToolbar.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.source.text = contentSource
|
||||||
|
if (typeface != null) {
|
||||||
|
binding.source.typeface = typeface
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentText.isEmptyOrNullOrNullString()) {
|
||||||
|
getContentFromMercury(customTabsIntent, prefs)
|
||||||
|
} else {
|
||||||
|
binding.titleView.text = contentTitle
|
||||||
|
if (typeface != null) {
|
||||||
|
binding.titleView.typeface = typeface
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlToWebview()
|
||||||
|
|
||||||
|
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||||
|
binding.imageView.visibility = View.VISIBLE
|
||||||
|
Glide
|
||||||
|
.with(requireContext())
|
||||||
|
.asBitmap()
|
||||||
|
.loadMaybeBasicAuth(config, contentImage)
|
||||||
|
.apply(RequestOptions.fitCenterTransform())
|
||||||
|
.into(binding.imageView)
|
||||||
|
} else {
|
||||||
|
binding.imageView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.nestedScrollView.setOnScrollChangeListener(
|
||||||
|
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||||
|
if (scrollY > oldScrollY) {
|
||||||
|
floatingToolbar.hide()
|
||||||
|
fab.hide()
|
||||||
|
} else {
|
||||||
|
if (staticBar) {
|
||||||
|
floatingToolbar.show()
|
||||||
|
} else {
|
||||||
|
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: InflateException) {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||||
|
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||||
|
.setPositiveButton(android.R.string.ok
|
||||||
|
) { dialog, which ->
|
||||||
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
val editor = sharedPref.edit()
|
||||||
|
editor.putBoolean("prefer_article_viewer", false)
|
||||||
|
editor.commit()
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshAlignment() {
|
||||||
|
textAlignment = when (prefs.getInt("text_align", 1)) {
|
||||||
|
1 -> "justify"
|
||||||
|
2 -> "left"
|
||||||
|
else -> "justify"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getContentFromMercury(
|
||||||
|
customTabsIntent: CustomTabsIntent,
|
||||||
|
prefs: SharedPreferences
|
||||||
|
) {
|
||||||
|
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
|
||||||
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
val parser = MercuryApi()
|
||||||
|
|
||||||
|
parser.parseUrl(url).enqueue(
|
||||||
|
object : Callback<ParsedContent> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<ParsedContent>,
|
||||||
|
response: Response<ParsedContent>
|
||||||
|
) {
|
||||||
|
// TODO: clean all the following after finding the mercury content issue
|
||||||
|
try {
|
||||||
|
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
|
||||||
|
try {
|
||||||
|
binding.titleView.text = response.body()!!.title
|
||||||
|
if (typeface != null) {
|
||||||
|
binding.titleView.typeface = typeface
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Note: Mercury may return relative urls... If it does the url val will not be changed.
|
||||||
|
URL(response.body()!!.url)
|
||||||
|
url = response.body()!!.url
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
// Mercury returned a relative url. We do nothing.
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
contentText = response.body()!!.content.orEmpty()
|
||||||
|
htmlToWebview()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
|
||||||
|
binding.imageView.visibility = View.VISIBLE
|
||||||
|
try {
|
||||||
|
Glide
|
||||||
|
.with(requireContext())
|
||||||
|
.asBitmap()
|
||||||
|
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
|
||||||
|
.apply(RequestOptions.fitCenterTransform())
|
||||||
|
.into(binding.imageView)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.imageView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (context != null) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
binding.nestedScrollView.scrollTo(0, 0)
|
||||||
|
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (context != null) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
openInBrowserAfterFailing(customTabsIntent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (context != null) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (context != null) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<ParsedContent>,
|
||||||
|
t: Throwable
|
||||||
|
) = openInBrowserAfterFailing(customTabsIntent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun htmlToWebview() {
|
||||||
|
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
|
||||||
|
|
||||||
|
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||||
|
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
|
||||||
|
|
||||||
|
|
||||||
|
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||||
|
binding.webcontent.visibility = View.VISIBLE
|
||||||
|
val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
|
||||||
|
if (context != null) {
|
||||||
|
binding.webcontent.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
requireContext(),
|
||||||
|
R.color.dark_webview
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview))
|
||||||
|
} else {
|
||||||
|
Pair(null, null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (context != null) {
|
||||||
|
binding.webcontent.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
requireContext(),
|
||||||
|
R.color.light_webview
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Pair(ContextCompat.getColor(requireContext(), R.color.light_webview_text), ContextCompat.getColor(requireContext(), R.color.light_webview))
|
||||||
|
} else {
|
||||||
|
Pair(null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stringTextColor: String = if (textColor != null) {
|
||||||
|
String.format("#%06X", 0xFFFFFF and textColor)
|
||||||
|
} else {
|
||||||
|
"#000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
val stringBackgroundColor = if (backgroundColor != null) {
|
||||||
|
String.format("#%06X", 0xFFFFFF and backgroundColor)
|
||||||
|
} else {
|
||||||
|
"#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.webcontent.settings.useWideViewPort = true
|
||||||
|
binding.webcontent.settings.loadWithOverviewMode = true
|
||||||
|
binding.webcontent.settings.javaScriptEnabled = false
|
||||||
|
|
||||||
|
binding.webcontent.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
|
||||||
|
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||||
|
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
||||||
|
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||||
|
if (url.toLowerCase().contains(".jpg") || url.toLowerCase().contains(".jpeg")) {
|
||||||
|
try {
|
||||||
|
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||||
|
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
|
||||||
|
}catch ( e : ExecutionException) {}
|
||||||
|
}
|
||||||
|
else if (url.toLowerCase().contains(".png")) {
|
||||||
|
try {
|
||||||
|
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||||
|
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
|
||||||
|
}catch ( e : ExecutionException) {}
|
||||||
|
}
|
||||||
|
else if (url.toLowerCase().contains(".webp")) {
|
||||||
|
try {
|
||||||
|
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||||
|
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
|
||||||
|
}catch ( e : ExecutionException) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.shouldInterceptRequest(view, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||||
|
return performClick()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
|
||||||
|
|
||||||
|
binding.webcontent.settings.layoutAlgorithm =
|
||||||
|
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||||
|
|
||||||
|
var baseUrl: String? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val itemUrl = URL(url)
|
||||||
|
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontName = when (font) {
|
||||||
|
getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||||
|
getString(R.string.roboto_font_id) -> "Roboto"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontLinkAndStyle = if (font.isNotEmpty()) {
|
||||||
|
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|
||||||
|
|<style>
|
||||||
|
| * {
|
||||||
|
| font-family: '$fontName';
|
||||||
|
| }
|
||||||
|
|</style>
|
||||||
|
""".trimMargin()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.webcontent.loadDataWithBaseURL(
|
||||||
|
baseUrl,
|
||||||
|
"""<html>
|
||||||
|
|<head>
|
||||||
|
| <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
| <style>
|
||||||
|
| img {
|
||||||
|
| display: inline-block;
|
||||||
|
| height: auto;
|
||||||
|
| width: 100%;
|
||||||
|
| max-width: 100%;
|
||||||
|
| }
|
||||||
|
| a {
|
||||||
|
| color: $stringColor !important;
|
||||||
|
| }
|
||||||
|
| *:not(a) {
|
||||||
|
| color: $stringTextColor;
|
||||||
|
| }
|
||||||
|
| * {
|
||||||
|
| font-size: ${fontSize}px;
|
||||||
|
| text-align: $textAlignment;
|
||||||
|
| word-break: break-word;
|
||||||
|
| overflow:hidden;
|
||||||
|
| line-height: 1.5em;
|
||||||
|
| background-color: $stringBackgroundColor;
|
||||||
|
| }
|
||||||
|
| body, html {
|
||||||
|
| background-color: $stringBackgroundColor !important;
|
||||||
|
| border-color: $stringBackgroundColor !important;
|
||||||
|
| padding: 0 !important;
|
||||||
|
| margin: 0 !important;
|
||||||
|
| }
|
||||||
|
| a, pre, code {
|
||||||
|
| text-align: $textAlignment;
|
||||||
|
| }
|
||||||
|
| pre, code {
|
||||||
|
| white-space: pre-wrap;
|
||||||
|
| width:100%;
|
||||||
|
| background-color: $stringBackgroundColor;
|
||||||
|
| }
|
||||||
|
| </style>
|
||||||
|
| $fontLinkAndStyle
|
||||||
|
|</head>
|
||||||
|
|<body>
|
||||||
|
| $contentText
|
||||||
|
|</body>""".trimMargin(),
|
||||||
|
"text/html",
|
||||||
|
"utf-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
requireActivity().openItemUrl(
|
||||||
|
allItems,
|
||||||
|
pageNumber.toInt(),
|
||||||
|
url,
|
||||||
|
customTabsIntent,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
requireActivity()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_POSITION = "position"
|
||||||
|
private const val ARG_ITEMS = "items"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
position: Int,
|
||||||
|
allItems: ArrayList<Item>
|
||||||
|
): ArticleFragment {
|
||||||
|
val fragment = ArticleFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putInt(ARG_POSITION, position)
|
||||||
|
args.putParcelableArrayList(ARG_ITEMS, allItems)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performClick(): Boolean {
|
||||||
|
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||||
|
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||||
|
|
||||||
|
val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
|
||||||
|
|
||||||
|
val intent = Intent(activity, ImageActivity::class.java)
|
||||||
|
intent.putExtra("allImages", allImages)
|
||||||
|
intent.putExtra("position", position)
|
||||||
|
startActivity(intent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.*
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import apps.amine.bou.readerforselfoss.databinding.FragmentImageBinding
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
|
||||||
|
class ImageFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var imageUrl : String
|
||||||
|
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||||
|
private var _binding: FragmentImageBinding? = null
|
||||||
|
private val binding get() = _binding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
imageUrl = requireArguments().getString("imageUrl")!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
_binding = FragmentImageBinding.inflate(inflater, container, false)
|
||||||
|
val view = binding?.root
|
||||||
|
|
||||||
|
binding!!.photoView.visibility = View.VISIBLE
|
||||||
|
Glide.with(activity)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(glideOptions)
|
||||||
|
.load(imageUrl)
|
||||||
|
.into(binding!!.photoView)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_IMAGE = "imageUrl"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
imageUrl : String
|
||||||
|
): ImageFragment {
|
||||||
|
val fragment = ImageFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(ARG_IMAGE, imageUrl)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ActionsDao {
|
||||||
|
@Query("SELECT * FROM actions order by id asc")
|
||||||
|
suspend fun actions(): List<ActionEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAllActions(vararg actions: ActionEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
|
||||||
|
fun deleteReadActionForArticle(article_id: String)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(action: ActionEntity)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.dao
|
||||||
|
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DrawerDataDao {
|
||||||
|
@Query("SELECT * FROM tags")
|
||||||
|
fun tags(): List<TagEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources")
|
||||||
|
fun sources(): List<SourceEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAllTags(vararg tags: TagEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAllSources(vararg sources: SourceEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM tags")
|
||||||
|
fun deleteAllTags()
|
||||||
|
|
||||||
|
@Query("DELETE FROM sources")
|
||||||
|
fun deleteAllSources()
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteTag(tag: TagEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteSource(source: SourceEntity)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
|
||||||
|
import androidx.room.Update
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ItemsDao {
|
||||||
|
@Query("SELECT * FROM items order by id desc")
|
||||||
|
suspend fun items(): List<ItemEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAllItems(vararg items: ItemEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM items")
|
||||||
|
suspend fun deleteAllItems()
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(item: ItemEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateItem(item: ItemEntity)
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.database
|
||||||
|
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.Database
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.dao.ActionsDao
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.dao.DrawerDataDao
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.dao.ItemsDao
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
|
||||||
|
|
||||||
|
@Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 4)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun drawerDataDao(): DrawerDataDao
|
||||||
|
|
||||||
|
abstract fun itemsDao(): ItemsDao
|
||||||
|
|
||||||
|
abstract fun actionsDao(): ActionsDao
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.entities
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "actions")
|
||||||
|
data class ActionEntity(
|
||||||
|
@ColumnInfo(name = "articleid")
|
||||||
|
val articleId: String,
|
||||||
|
@ColumnInfo(name = "read")
|
||||||
|
val read: Boolean,
|
||||||
|
@ColumnInfo(name = "unread")
|
||||||
|
val unread: Boolean,
|
||||||
|
@ColumnInfo(name = "starred")
|
||||||
|
var starred: Boolean,
|
||||||
|
@ColumnInfo(name = "unstarred")
|
||||||
|
var unstarred: Boolean
|
||||||
|
) {
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Int = 0
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.entities
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "tags")
|
||||||
|
data class TagEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "tag")
|
||||||
|
val tag: String,
|
||||||
|
@ColumnInfo(name = "color")
|
||||||
|
val color: String,
|
||||||
|
@ColumnInfo(name = "unread")
|
||||||
|
val unread: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Entity(tableName = "sources")
|
||||||
|
data class SourceEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
val title: String,
|
||||||
|
@ColumnInfo(name = "tags")
|
||||||
|
val tags: String,
|
||||||
|
@ColumnInfo(name = "spout")
|
||||||
|
val spout: String,
|
||||||
|
@ColumnInfo(name = "error")
|
||||||
|
val error: String,
|
||||||
|
@ColumnInfo(name = "icon")
|
||||||
|
val icon: String
|
||||||
|
)
|
@ -0,0 +1,32 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.entities
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "items")
|
||||||
|
data class ItemEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "datetime")
|
||||||
|
val datetime: String,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
val title: String,
|
||||||
|
@ColumnInfo(name = "content")
|
||||||
|
val content: String,
|
||||||
|
@ColumnInfo(name = "unread")
|
||||||
|
val unread: Boolean,
|
||||||
|
@ColumnInfo(name = "starred")
|
||||||
|
var starred: Boolean,
|
||||||
|
@ColumnInfo(name = "thumbnail")
|
||||||
|
val thumbnail: String?,
|
||||||
|
@ColumnInfo(name = "icon")
|
||||||
|
val icon: String?,
|
||||||
|
@ColumnInfo(name = "link")
|
||||||
|
val link: String,
|
||||||
|
@ColumnInfo(name = "sourcetitle")
|
||||||
|
val sourcetitle: String,
|
||||||
|
@ColumnInfo(name = "tags")
|
||||||
|
val tags: String
|
||||||
|
)
|
@ -0,0 +1,34 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.persistence.migrations
|
||||||
|
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
|
||||||
|
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database
|
||||||
|
// Create the new table
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||||
|
|
||||||
|
// Copy the data
|
||||||
|
database.execSQL(
|
||||||
|
"INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items")
|
||||||
|
|
||||||
|
// Remove the old table
|
||||||
|
database.execSQL("DROP TABLE items")
|
||||||
|
|
||||||
|
// Change the table name to the correct one
|
||||||
|
database.execSQL("ALTER TABLE itemstmp RENAME TO items")
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,212 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.settings
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.*
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import android.view.*
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import java.lang.NumberFormatException
|
||||||
|
|
||||||
|
private const val TITLE_TAG = "settingsActivityTitle"
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity(),
|
||||||
|
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) {
|
||||||
|
setTheme(R.style.NoBarDark)
|
||||||
|
}
|
||||||
|
setContentView(R.layout.activity_settings)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings, MainPreferenceFragment())
|
||||||
|
.commit()
|
||||||
|
} else {
|
||||||
|
title = savedInstanceState.getCharSequence(TITLE_TAG)
|
||||||
|
}
|
||||||
|
supportFragmentManager.addOnBackStackChangedListener {
|
||||||
|
if (supportFragmentManager.backStackEntryCount == 0) {
|
||||||
|
setTitle(R.string.title_activity_settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
|
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
supportActionBar?.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
// Save current activity title so we can set it again after a configuration change
|
||||||
|
outState.putCharSequence(TITLE_TAG, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
if (supportFragmentManager.popBackStackImmediate()) {
|
||||||
|
supportActionBar?.title = getText(R.string.title_activity_settings)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onSupportNavigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreferenceStartFragment(
|
||||||
|
caller: PreferenceFragmentCompat,
|
||||||
|
pref: Preference
|
||||||
|
): Boolean {
|
||||||
|
// Instantiate the new Fragment
|
||||||
|
val args = pref.extras
|
||||||
|
val fragment = supportFragmentManager.fragmentFactory.instantiate(
|
||||||
|
classLoader,
|
||||||
|
pref.fragment
|
||||||
|
).apply {
|
||||||
|
arguments = args
|
||||||
|
setTargetFragment(caller, 0)
|
||||||
|
}
|
||||||
|
// Replace the existing Fragment with the new Fragment
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.settings, fragment)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
title = pref.title
|
||||||
|
supportActionBar?.title = title
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_main, rootKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_general, rootKey)
|
||||||
|
|
||||||
|
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
|
||||||
|
editTextPreference?.setOnBindEditTextListener { editText ->
|
||||||
|
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||||
|
editText.filters = arrayOf(
|
||||||
|
InputFilter { source, _, _, dest, _, _ ->
|
||||||
|
try {
|
||||||
|
val input: Int = (dest.toString() + source.toString()).toInt()
|
||||||
|
if (input in 1..200) return@InputFilter null
|
||||||
|
} catch (nfe: NumberFormatException) {
|
||||||
|
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_viewer, rootKey)
|
||||||
|
|
||||||
|
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
|
||||||
|
fontSize?.setOnBindEditTextListener { editText ->
|
||||||
|
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||||
|
editText.addTextChangedListener { object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
try {
|
||||||
|
editText.textSize = editable.toString().toInt().toFloat()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
editText.filters = arrayOf(
|
||||||
|
InputFilter { source, _, _, dest, _, _ ->
|
||||||
|
try {
|
||||||
|
val input = (dest.toString() + source.toString()).toInt()
|
||||||
|
if (input > 0) return@InputFilter null
|
||||||
|
} catch (nfe: NumberFormatException) {
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OfflinePreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_offline, rootKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThemePreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_theme, rootKey)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
inflater.inflate(R.menu.settings_theme, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
val id = item.itemId
|
||||||
|
if (id == R.id.clear) {
|
||||||
|
val pref = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
val editor = pref.edit()
|
||||||
|
editor.remove("color_primary")
|
||||||
|
editor.remove("color_primary_dark")
|
||||||
|
editor.remove("color_accent")
|
||||||
|
editor.remove("color_accent_dark")
|
||||||
|
editor.remove("dark_theme")
|
||||||
|
editor.apply()
|
||||||
|
requireActivity().recreate()
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinksPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
private fun openUrl(uri: Uri?) {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
startActivity(browserIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_links, rootKey)
|
||||||
|
|
||||||
|
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
openUrl(Uri.parse(Config.trackerUrl))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
openUrl(Uri.parse(Config.sourceUrl))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
|
openUrl(Uri.parse(Config.translationUrl))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.pref_experimental, rootKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.themes
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import android.util.TypedValue
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
class AppColors(a: Activity) {
|
||||||
|
|
||||||
|
@ColorInt val colorPrimary: Int
|
||||||
|
@ColorInt val colorPrimaryDark: Int
|
||||||
|
@ColorInt val colorAccent: Int
|
||||||
|
@ColorInt val colorAccentDark: Int
|
||||||
|
@ColorInt val cardBackgroundColor: Int
|
||||||
|
@ColorInt val colorBackground: Int
|
||||||
|
@ColorInt val textColor: Int
|
||||||
|
val isDarkTheme: Boolean
|
||||||
|
|
||||||
|
init {
|
||||||
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(a)
|
||||||
|
|
||||||
|
colorPrimary =
|
||||||
|
sharedPref.getInt(
|
||||||
|
"color_primary",
|
||||||
|
a.resources.getColor(R.color.colorPrimary)
|
||||||
|
)
|
||||||
|
colorPrimaryDark =
|
||||||
|
sharedPref.getInt(
|
||||||
|
"color_primary_dark",
|
||||||
|
a.resources.getColor(R.color.colorPrimaryDark)
|
||||||
|
)
|
||||||
|
colorAccent =
|
||||||
|
sharedPref.getInt(
|
||||||
|
"color_accent",
|
||||||
|
a.resources.getColor(R.color.colorAccent)
|
||||||
|
)
|
||||||
|
colorAccentDark =
|
||||||
|
sharedPref.getInt(
|
||||||
|
"color_accent_dark",
|
||||||
|
a.resources.getColor(R.color.colorAccentDark)
|
||||||
|
)
|
||||||
|
isDarkTheme =
|
||||||
|
sharedPref.getBoolean(
|
||||||
|
"dark_theme",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
colorBackground = if (isDarkTheme) {
|
||||||
|
a.setTheme(R.style.NoBarDark)
|
||||||
|
R.color.darkBackground
|
||||||
|
} else {
|
||||||
|
a.setTheme(R.style.NoBar)
|
||||||
|
android.R.color.background_light
|
||||||
|
}
|
||||||
|
|
||||||
|
textColor = if (isDarkTheme) {
|
||||||
|
R.color.white
|
||||||
|
} else {
|
||||||
|
R.color.grey_900
|
||||||
|
}
|
||||||
|
|
||||||
|
val wrapper = Context::class.java
|
||||||
|
val method = wrapper!!.getMethod("getThemeResId")
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
val typedCardBackground = TypedValue()
|
||||||
|
a.theme.resolveAttribute(R.attr.cardBackgroundColor, typedCardBackground, true)
|
||||||
|
|
||||||
|
cardBackgroundColor = typedCardBackground.data
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.themes
|
||||||
|
|
||||||
|
enum class Toppings(val value: Int) {
|
||||||
|
PRIMARY(1),
|
||||||
|
PRIMARY_DARK(2),
|
||||||
|
ACCENT(3),
|
||||||
|
ACCENT_DARK(4)
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.transformers
|
||||||
|
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
class DepthPageTransformer : ViewPager.PageTransformer {
|
||||||
|
|
||||||
|
override fun transformPage(view: View, position: Float) {
|
||||||
|
val pageWidth = view.width
|
||||||
|
|
||||||
|
when {
|
||||||
|
position < -1 -> // [-Infinity,-1)
|
||||||
|
// This page is way off-screen to the left.
|
||||||
|
view.alpha = 0F
|
||||||
|
position <= 0 -> { // [-1,0]
|
||||||
|
// Use the default slide transition when moving to the left page
|
||||||
|
view.alpha = 1F
|
||||||
|
view.translationX = 0F
|
||||||
|
view.scaleX = 1F
|
||||||
|
view.scaleY = 1F
|
||||||
|
}
|
||||||
|
position <= 1 -> { // (0,1]
|
||||||
|
// Fade the page out.
|
||||||
|
view.alpha = 1 - position
|
||||||
|
|
||||||
|
// Counteract the default slide transition
|
||||||
|
view.translationX = pageWidth * -position
|
||||||
|
|
||||||
|
// Scale the page down (between MIN_SCALE and 1)
|
||||||
|
val scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position))
|
||||||
|
view.scaleX = scaleFactor
|
||||||
|
view.scaleY = scaleFactor
|
||||||
|
}
|
||||||
|
else -> // (1,+Infinity]
|
||||||
|
// This page is way off-screen to the right.
|
||||||
|
view.alpha = 0F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val MIN_SCALE = 0.75f
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
fun Response<SuccessResponse>.succeeded(): Boolean =
|
||||||
|
this.code() === 200 && this.body() != null && this.body()!!.isSuccess
|
@ -2,98 +2,40 @@ package apps.amine.bou.readerforselfoss.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
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 apps.amine.bou.readerforselfoss.R
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
private fun isStoreVersion(context: Context): Boolean {
|
fun String?.isEmptyOrNullOrNullString(): Boolean =
|
||||||
var result = false
|
this == null || this == "null" || this.isEmpty()
|
||||||
try {
|
|
||||||
val installer = context.packageManager
|
|
||||||
.getInstallerPackageName(context.packageName)
|
|
||||||
result = !TextUtils.isEmpty(installer)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
fun String.longHash(): Long {
|
||||||
}
|
|
||||||
|
|
||||||
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
|
var h = 98764321261L
|
||||||
val l = string.length
|
val l = this.length
|
||||||
val chars = string.toCharArray()
|
val chars = this.toCharArray()
|
||||||
|
|
||||||
for (i in 0..l - 1) {
|
for (i in 0 until l) {
|
||||||
h = 31 * h + chars[i].toLong()
|
h = 31 * h + chars[i].toLong()
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toStringUriWithHttp(): String =
|
||||||
|
if (!this.startsWith("https://") && !this.startsWith("http://")) {
|
||||||
|
"http://" + this
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.shareLink(itemUrl: String, itemTitle: String) {
|
||||||
|
val sendIntent = Intent()
|
||||||
|
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
|
||||||
|
sendIntent.type = "text/plain"
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
getString(R.string.share)
|
||||||
|
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,34 +1,64 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import apps.amine.bou.readerforselfoss.LoginActivity
|
||||||
|
|
||||||
class Config(c: Context) {
|
class Config(c: Context) {
|
||||||
|
|
||||||
private val settings: SharedPreferences
|
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
init {
|
|
||||||
this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val baseUrl: String
|
val baseUrl: String
|
||||||
get() = settings.getString("url", "")
|
get() = settings.getString("url", "")!!
|
||||||
|
|
||||||
val userLogin: String
|
val userLogin: String
|
||||||
get() = settings.getString("login", "")
|
get() = settings.getString("login", "")!!
|
||||||
|
|
||||||
val userPassword: String
|
val userPassword: String
|
||||||
get() = settings.getString("password", "")
|
get() = settings.getString("password", "")!!
|
||||||
|
|
||||||
val httpUserLogin: String
|
val httpUserLogin: String
|
||||||
get() = settings.getString("httpUserName", "")
|
get() = settings.getString("httpUserName", "")!!
|
||||||
|
|
||||||
val httpUserPassword: String
|
val httpUserPassword: String
|
||||||
get() = settings.getString("httpPassword", "")
|
get() = settings.getString("httpPassword", "")!!
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val settingsName = "paramsselfoss"
|
const val settingsName = "paramsselfoss"
|
||||||
|
|
||||||
|
const val feedbackEmail = "aminecmi@gmail.com"
|
||||||
|
|
||||||
|
const val translationUrl = "https://crwd.in/readerforselfoss"
|
||||||
|
|
||||||
|
const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss"
|
||||||
|
|
||||||
|
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
|
||||||
|
|
||||||
|
const val syncChannelId = "sync-channel-id"
|
||||||
|
|
||||||
|
const val newItemsChannelId = "new-items-channel-id"
|
||||||
|
|
||||||
|
var apiVersion = 0
|
||||||
|
|
||||||
|
fun logoutAndRedirect(
|
||||||
|
c: Context,
|
||||||
|
callingActivity: Activity,
|
||||||
|
editor: SharedPreferences.Editor,
|
||||||
|
baseUrlFail: Boolean = false
|
||||||
|
): Boolean {
|
||||||
|
editor.remove("url")
|
||||||
|
editor.remove("login")
|
||||||
|
editor.remove("password")
|
||||||
|
editor.apply()
|
||||||
|
val intent = Intent(c, LoginActivity::class.java)
|
||||||
|
if (baseUrlFail) {
|
||||||
|
intent.putExtra("baseUrlFail", baseUrlFail)
|
||||||
|
}
|
||||||
|
c.startActivity(intent)
|
||||||
|
callingActivity.finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
fun parseDate(dateString: String): Instant {
|
||||||
|
|
||||||
|
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
|
||||||
|
return if (Config.apiVersion >= 4) {
|
||||||
|
OffsetDateTime.parse(dateString).toInstant()
|
||||||
|
} else {
|
||||||
|
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseRelativeDate(dateString: String): String {
|
||||||
|
|
||||||
|
val date = parseDate(dateString)
|
||||||
|
|
||||||
|
return " " + DateUtils.getRelativeTimeSpanString(
|
||||||
|
date.toEpochMilli(),
|
||||||
|
Instant.now().toEpochMilli(),
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.security.cert.CertificateException
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
fun getUnsafeHttpClient(): OkHttpClient.Builder =
|
||||||
|
try {
|
||||||
|
// Create a trust manager that does not validate certificate chains
|
||||||
|
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> =
|
||||||
|
arrayOf()
|
||||||
|
|
||||||
|
@Throws(CertificateException::class)
|
||||||
|
override fun checkClientTrusted(
|
||||||
|
chain: Array<java.security.cert.X509Certificate>,
|
||||||
|
authType: String
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(CertificateException::class)
|
||||||
|
override fun checkServerTrusted(
|
||||||
|
chain: Array<java.security.cert.X509Certificate>,
|
||||||
|
authType: String
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install the all-trusting trust manager
|
||||||
|
val sslContext = SSLContext.getInstance("SSL")
|
||||||
|
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
|
||||||
|
|
||||||
|
val sslSocketFactory = sslContext.socketFactory
|
||||||
|
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||||
|
.hostnameVerifier { _, _ -> true }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
|
||||||
|
|
||||||
|
fun String.toTextDrawableString(c: Context): String {
|
||||||
|
val textDrawable = StringBuilder()
|
||||||
|
for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
|
||||||
|
try {
|
||||||
|
textDrawable.append(s[0])
|
||||||
|
} catch (e: StringIndexOutOfBoundsException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textDrawable.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Item.sourceAndDateText(): String {
|
||||||
|
val formattedDate = parseRelativeDate(this.datetime)
|
||||||
|
|
||||||
|
return this.getSourceTitle() + formattedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Item.toggleStar(): Item {
|
||||||
|
this.starred = !this.starred
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<Item>.flattenTags(): List<Item> =
|
||||||
|
this.flatMap {
|
||||||
|
val item = it
|
||||||
|
val tags: List<String> = it.tags.tags.split(",")
|
||||||
|
tags.map { t ->
|
||||||
|
item.copy(tags = SelfossTagType(t.trim()))
|
||||||
|
}
|
||||||
|
}
|
@ -2,80 +2,202 @@ package apps.amine.bou.readerforselfoss.utils
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.customtabs.CustomTabsIntent
|
import android.os.Build
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import android.util.Patterns
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.ReaderActivity
|
import apps.amine.bou.readerforselfoss.ReaderActivity
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
fun buildCustomTabsIntent(c: Context): CustomTabsIntent {
|
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||||
|
|
||||||
fun createPendingShareIntent(c: Context): PendingIntent {
|
val actionIntent = Intent(Intent.ACTION_SEND)
|
||||||
val actionIntent = Intent(Intent.ACTION_SEND)
|
actionIntent.type = "text/plain"
|
||||||
actionIntent.type = "text/plain"
|
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
return PendingIntent.getActivity(
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
c, 0, actionIntent, 0)
|
} else {
|
||||||
|
0
|
||||||
}
|
}
|
||||||
|
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
actionIntent,
|
||||||
|
pflags
|
||||||
|
)
|
||||||
|
|
||||||
val intentBuilder = CustomTabsIntent.Builder()
|
val intentBuilder = CustomTabsIntent.Builder()
|
||||||
|
|
||||||
// TODO: change to primary when it's possible to customize custom tabs title color
|
// 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.getResources().getColor(R.color.colorPrimary));
|
||||||
intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark))
|
intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
|
||||||
intentBuilder.setShowTitle(true)
|
intentBuilder.setShowTitle(true)
|
||||||
|
|
||||||
|
|
||||||
intentBuilder.setStartAnimations(c,
|
intentBuilder.setStartAnimations(
|
||||||
R.anim.slide_in_right,
|
this,
|
||||||
R.anim.slide_out_left)
|
R.anim.slide_in_right,
|
||||||
intentBuilder.setExitAnimations(c,
|
R.anim.slide_out_left
|
||||||
android.R.anim.slide_in_left,
|
)
|
||||||
android.R.anim.slide_out_right)
|
intentBuilder.setExitAnimations(
|
||||||
|
this,
|
||||||
|
android.R.anim.slide_in_left,
|
||||||
|
android.R.anim.slide_out_right
|
||||||
|
)
|
||||||
|
|
||||||
val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp)
|
val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
|
||||||
intentBuilder.setCloseButtonIcon(closeicon)
|
intentBuilder.setCloseButtonIcon(closeicon)
|
||||||
|
|
||||||
val shareLabel = c.getString(R.string.label_share)
|
val shareLabel = this.getString(R.string.label_share)
|
||||||
val icon = BitmapFactory.decodeResource(c.resources,
|
val icon = BitmapFactory.decodeResource(
|
||||||
R.drawable.ic_share_white_24dp)
|
resources,
|
||||||
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c))
|
R.drawable.ic_share_white_24dp
|
||||||
|
)
|
||||||
|
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
|
||||||
|
|
||||||
return intentBuilder.build()
|
return intentBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openItemUrl(i: Item,
|
fun Context.openItemUrlInternally(
|
||||||
customTabsIntent: CustomTabsIntent,
|
allItems: ArrayList<Item>,
|
||||||
internalBrowser: Boolean,
|
currentItem: Int,
|
||||||
articleViewer: Boolean,
|
linkDecoded: String,
|
||||||
app: Activity,
|
customTabsIntent: CustomTabsIntent,
|
||||||
c: Context) {
|
articleViewer: Boolean,
|
||||||
if (!internalBrowser) {
|
app: Activity
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
) {
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
if (articleViewer) {
|
||||||
|
ReaderActivity.allItems = allItems
|
||||||
|
SharedItems.position = currentItem
|
||||||
|
val intent = Intent(this, ReaderActivity::class.java)
|
||||||
|
intent.putExtra("currentItem", currentItem)
|
||||||
app.startActivity(intent)
|
app.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
if (articleViewer) {
|
try {
|
||||||
val intent = Intent(c, ReaderActivity::class.java)
|
CustomTabActivityHelper.openCustomTab(
|
||||||
|
app,
|
||||||
DragDismissIntentBuilder(c)
|
customTabsIntent,
|
||||||
.setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side
|
Uri.parse(linkDecoded)
|
||||||
.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 ->
|
) { _, uri ->
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
c.startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
openInBrowser(linkDecoded, app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.openItemUrl(
|
||||||
|
allItems: ArrayList<Item>,
|
||||||
|
currentItem: Int,
|
||||||
|
linkDecoded: String,
|
||||||
|
customTabsIntent: CustomTabsIntent,
|
||||||
|
internalBrowser: Boolean,
|
||||||
|
articleViewer: Boolean,
|
||||||
|
app: Activity
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!linkDecoded.isUrlValid()) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
this.getString(R.string.cant_open_invalid_url),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
if (!internalBrowser) {
|
||||||
|
openInBrowser(linkDecoded, app)
|
||||||
|
} else {
|
||||||
|
this.openItemUrlInternally(
|
||||||
|
allItems,
|
||||||
|
currentItem,
|
||||||
|
linkDecoded,
|
||||||
|
customTabsIntent,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInBrowser(linkDecoded: String, app: Activity) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(linkDecoded)
|
||||||
|
try {
|
||||||
|
app.startActivity(intent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.isUrlValid(): Boolean =
|
||||||
|
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||||
|
|
||||||
|
fun String.isBaseUrlValid(ctx: Context): Boolean {
|
||||||
|
val baseUrl = this.toHttpUrlOrNull()
|
||||||
|
var existsAndEndsWithSlash = false
|
||||||
|
if (baseUrl != null) {
|
||||||
|
val pathSegments = baseUrl.pathSegments
|
||||||
|
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openInBrowserAsNewTask(i: Item) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkOnTouchListener: View.OnTouchListener {
|
||||||
|
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||||
|
var ret = false
|
||||||
|
val widget: TextView = v as TextView
|
||||||
|
val text: CharSequence = widget.text
|
||||||
|
val stext = Spannable.Factory.getInstance().newSpannable(text)
|
||||||
|
|
||||||
|
val action = event!!.action
|
||||||
|
|
||||||
|
if (action == MotionEvent.ACTION_UP ||
|
||||||
|
action == MotionEvent.ACTION_DOWN) {
|
||||||
|
var x: Float = event.x
|
||||||
|
var y: Float = event.y
|
||||||
|
|
||||||
|
x -= widget.totalPaddingLeft
|
||||||
|
y -= widget.totalPaddingTop
|
||||||
|
|
||||||
|
x += widget.scrollX
|
||||||
|
y += widget.scrollY
|
||||||
|
|
||||||
|
val layout = widget.layout
|
||||||
|
val line = layout.getLineForVertical(y.toInt())
|
||||||
|
val off = layout.getOffsetForHorizontal(line, x)
|
||||||
|
|
||||||
|
val link = stext.getSpans(off, off, ClickableSpan::class.java)
|
||||||
|
|
||||||
|
if (link.isNotEmpty()) {
|
||||||
|
if (action == MotionEvent.ACTION_UP) {
|
||||||
|
link[0].onClick(widget)
|
||||||
|
}
|
||||||
|
ret = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,404 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.persistence.database.AppDatabase
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.persistence.toView
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Singleton class that contains the articles fetched from Selfoss, it allows sharing the items list
|
||||||
|
* between Activities and Fragments
|
||||||
|
*/
|
||||||
|
object SharedItems {
|
||||||
|
var items: ArrayList<Item> = arrayListOf<Item>()
|
||||||
|
get() {
|
||||||
|
return ArrayList(field)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
field = ArrayList(value)
|
||||||
|
}
|
||||||
|
var focusedItems: ArrayList<Item> = arrayListOf<Item>()
|
||||||
|
get() {
|
||||||
|
return ArrayList(field)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
field = ArrayList(value)
|
||||||
|
}
|
||||||
|
var position = 0
|
||||||
|
set(value) {
|
||||||
|
field = when {
|
||||||
|
value < 0 -> 0
|
||||||
|
value > items.size -> items.size
|
||||||
|
else -> value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var displayedItems: String = "unread"
|
||||||
|
set(value) {
|
||||||
|
field = when (value) {
|
||||||
|
"all" -> "all"
|
||||||
|
"unread" -> "unread"
|
||||||
|
"read" -> "read"
|
||||||
|
"starred" -> "starred"
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchFilter: String? = null
|
||||||
|
var sourceIDFilter: Long? = null
|
||||||
|
var sourceFilter: String? = null
|
||||||
|
var tagFilter: String? = null
|
||||||
|
var itemsCaching = false
|
||||||
|
|
||||||
|
var fetchedUnread = false
|
||||||
|
var fetchedAll = false
|
||||||
|
var fetchedStarred = false
|
||||||
|
|
||||||
|
var badgeUnread = -1
|
||||||
|
var badgeAll = -1
|
||||||
|
var badgeStarred = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new items to the SharedItems list
|
||||||
|
*
|
||||||
|
* The new items are considered more updated than the ones already in the list.
|
||||||
|
* The old items present in the new list are discarded and replaced by the new ones.
|
||||||
|
* Items are compared according to the selfoss id, which should always be unique.
|
||||||
|
*/
|
||||||
|
fun appendNewItems(newItems: ArrayList<Item>) {
|
||||||
|
var tmpItems = items
|
||||||
|
if (tmpItems != newItems) {
|
||||||
|
tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<Item>
|
||||||
|
tmpItems.addAll(newItems)
|
||||||
|
items = tmpItems
|
||||||
|
|
||||||
|
sortItems()
|
||||||
|
getFocusedItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshFocusedItems(newItems: ArrayList<Item>) {
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems.removeAll(focusedItems)
|
||||||
|
|
||||||
|
appendNewItems(newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearDBItems(db: AppDatabase) {
|
||||||
|
db.itemsDao().deleteAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateDatabase(db: AppDatabase) {
|
||||||
|
if (itemsCaching) {
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
getFromDB(db)
|
||||||
|
}
|
||||||
|
db.itemsDao().deleteAllItems()
|
||||||
|
db.itemsDao().insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filter() {
|
||||||
|
fun filterSearch(item: Item): Boolean {
|
||||||
|
return if (!searchFilter.isEmptyOrNullOrNullString()) {
|
||||||
|
var matched = item.title.contains(searchFilter.toString(), true)
|
||||||
|
matched = matched || item.content.contains(searchFilter.toString(), true)
|
||||||
|
matched = matched || item.sourcetitle.contains(searchFilter.toString(), true)
|
||||||
|
matched
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpItems = focusedItems
|
||||||
|
if (tagFilter != null) {
|
||||||
|
tmpItems = tmpItems.filter { it.tags.tags.contains(tagFilter.toString()) } as ArrayList<Item>
|
||||||
|
}
|
||||||
|
if (searchFilter != null) {
|
||||||
|
tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList<Item>
|
||||||
|
}
|
||||||
|
if (sourceFilter != null) {
|
||||||
|
tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList<Item>
|
||||||
|
}
|
||||||
|
focusedItems = tmpItems
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFocusedItems() {
|
||||||
|
when (displayedItems) {
|
||||||
|
"all" -> getAll()
|
||||||
|
"unread" -> getUnRead()
|
||||||
|
"read" -> getRead()
|
||||||
|
"starred" -> getStarred()
|
||||||
|
else -> getUnRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUnRead() {
|
||||||
|
displayedItems = "unread"
|
||||||
|
focusedItems = items.filter { item -> item.unread } as ArrayList<Item>
|
||||||
|
filter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRead() {
|
||||||
|
displayedItems = "read"
|
||||||
|
focusedItems = items.filter { item -> !item.unread } as ArrayList<Item>
|
||||||
|
filter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStarred() {
|
||||||
|
displayedItems = "starred"
|
||||||
|
focusedItems = items.filter { item -> item.starred } as ArrayList<Item>
|
||||||
|
filter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll() {
|
||||||
|
displayedItems = "all"
|
||||||
|
focusedItems = items
|
||||||
|
filter()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFromDB(db: AppDatabase) {
|
||||||
|
if (itemsCaching) {
|
||||||
|
val dbItems = db.itemsDao().items().map { it.toView() } as ArrayList<Item>
|
||||||
|
appendNewItems(dbItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeItemAtIndex(index: Int) {
|
||||||
|
val i = focusedItems[index]
|
||||||
|
val tmpItems = focusedItems
|
||||||
|
tmpItems.remove(i)
|
||||||
|
focusedItems = tmpItems
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemAtIndex(newItem: Item, index: Int) {
|
||||||
|
val tmpItems = focusedItems
|
||||||
|
tmpItems.add(index, newItem)
|
||||||
|
focusedItems = tmpItems
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
|
||||||
|
if (items.contains(item)) {
|
||||||
|
position = items.indexOf(item)
|
||||||
|
readItemAtPosition(app, api, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readItems(db: AppDatabase, ids: List<String>) {
|
||||||
|
for (id in ids) {
|
||||||
|
val match = items.filter { it -> it.id == id }
|
||||||
|
if (match.isNotEmpty() && match.size == 1) {
|
||||||
|
position = items.indexOf(match[0])
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems[position].unread = false
|
||||||
|
items = tmpItems
|
||||||
|
resetDBItem(db)
|
||||||
|
badgeUnread--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
|
||||||
|
val i = items[position]
|
||||||
|
|
||||||
|
if (app.isNetworkAccessible(null)) {
|
||||||
|
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems[position].unread = false
|
||||||
|
items = tmpItems
|
||||||
|
|
||||||
|
resetDBItem(db)
|
||||||
|
getFocusedItems()
|
||||||
|
badgeUnread--
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
app.getString(R.string.cant_mark_read),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (itemsCaching) {
|
||||||
|
thread {
|
||||||
|
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position > items.size) {
|
||||||
|
position -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unreadItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
|
||||||
|
if (items.contains(item) && !item.unread) {
|
||||||
|
position = items.indexOf(item)
|
||||||
|
unreadItemAtPosition(app, api, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unreadItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
|
||||||
|
val i = items[position]
|
||||||
|
|
||||||
|
if (app.isNetworkAccessible(null)) {
|
||||||
|
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems[position].unread = true
|
||||||
|
items = tmpItems
|
||||||
|
|
||||||
|
resetDBItem(db)
|
||||||
|
getFocusedItems()
|
||||||
|
badgeUnread++
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
app.getString(R.string.cant_mark_unread),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (itemsCaching) {
|
||||||
|
thread {
|
||||||
|
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun starItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
|
||||||
|
if (items.contains(item) && !item.starred) {
|
||||||
|
position = items.indexOf(item)
|
||||||
|
starItemAtPosition(app, api, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun starItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
|
||||||
|
val i = items[position]
|
||||||
|
|
||||||
|
if (app.isNetworkAccessible(null)) {
|
||||||
|
api.starrItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems[position].starred = true
|
||||||
|
items = tmpItems
|
||||||
|
|
||||||
|
resetDBItem(db)
|
||||||
|
getFocusedItems()
|
||||||
|
badgeStarred++
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
app.getString(R.string.cant_mark_favortie),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
thread {
|
||||||
|
db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, true, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unstarItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
|
||||||
|
if (items.contains(item) && item.starred) {
|
||||||
|
position = items.indexOf(item)
|
||||||
|
unstarItemAtPosition(app, api, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unstarItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
|
||||||
|
val i = items[position]
|
||||||
|
|
||||||
|
if (app.isNetworkAccessible(null)) {
|
||||||
|
api.unstarrItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
val tmpItems = items
|
||||||
|
tmpItems[position].starred = false
|
||||||
|
items = tmpItems
|
||||||
|
|
||||||
|
resetDBItem(db)
|
||||||
|
getFocusedItems()
|
||||||
|
badgeStarred--
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
app.getString(R.string.cant_unmark_favortie),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
thread {
|
||||||
|
db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, false, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetDBItem(db: AppDatabase) {
|
||||||
|
if (itemsCaching) {
|
||||||
|
val i = items[position]
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
db.itemsDao().delete(i.toEntity())
|
||||||
|
db.itemsDao().insertAllItems(i.toEntity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unreadItemStatusAtIndex(position: Int): Boolean {
|
||||||
|
return focusedItems[position].unread
|
||||||
|
}
|
||||||
|
|
||||||
|
fun computeBadges() {
|
||||||
|
badgeUnread = items.filter { item -> item.unread }.size
|
||||||
|
badgeStarred = items.filter { item -> item.starred }.size
|
||||||
|
badgeAll = items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortItems() {
|
||||||
|
val tmpItems = ArrayList(items.sortedByDescending { parseDate(it.datetime) })
|
||||||
|
items = tmpItems
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
|
||||||
|
val Int.toPx: Int
|
||||||
|
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
val Int.toDp: Int
|
||||||
|
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
@ -0,0 +1,12 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.bottombar
|
||||||
|
|
||||||
|
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||||
|
|
||||||
|
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||||
|
this.setText("")
|
||||||
|
this.hide()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextBadgeItem.maybeShow(): TextBadgeItem =
|
||||||
|
if (this.isHidden) this.show() else this
|
@ -1,12 +1,13 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.customtabs.CustomTabsClient;
|
import androidx.browser.customtabs.CustomTabsClient;
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import android.support.customtabs.CustomTabsServiceConnection;
|
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||||
import android.support.customtabs.CustomTabsSession;
|
import androidx.browser.customtabs.CustomTabsSession;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -22,15 +23,15 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
/**
|
/**
|
||||||
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
|
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
|
||||||
*
|
*
|
||||||
* @param activity The host activity.
|
* @param activity The host activity.
|
||||||
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
||||||
* @param uri the Uri to be opened.
|
* @param uri the Uri to be opened.
|
||||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
||||||
*/
|
*/
|
||||||
public static void openCustomTab(Activity activity,
|
public static void openCustomTab(Activity activity,
|
||||||
CustomTabsIntent customTabsIntent,
|
CustomTabsIntent customTabsIntent,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
CustomTabFallback fallback) {
|
CustomTabFallback fallback) {
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||||
|
|
||||||
//If we cant find a package name, it means theres no browser that supports
|
//If we cant find a package name, it means theres no browser that supports
|
||||||
@ -47,6 +48,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbinds the Activity from the Custom Tabs Service.
|
* Unbinds the Activity from the Custom Tabs Service.
|
||||||
|
*
|
||||||
* @param activity the activity that is connected to the service.
|
* @param activity the activity that is connected to the service.
|
||||||
*/
|
*/
|
||||||
public void unbindCustomTabsService(Activity activity) {
|
public void unbindCustomTabsService(Activity activity) {
|
||||||
@ -73,6 +75,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
||||||
|
*
|
||||||
* @param connectionCallback
|
* @param connectionCallback
|
||||||
*/
|
*/
|
||||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
||||||
@ -81,6 +84,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the Activity to the Custom Tabs Service.
|
* Binds the Activity to the Custom Tabs Service.
|
||||||
|
*
|
||||||
* @param activity the activity to be binded to the service.
|
* @param activity the activity to be binded to the service.
|
||||||
*/
|
*/
|
||||||
public void bindCustomTabsService(Activity activity) {
|
public void bindCustomTabsService(Activity activity) {
|
||||||
@ -94,16 +98,15 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
|
||||||
* @return true if call to mayLaunchUrl was accepted.
|
* @return true if call to mayLaunchUrl was accepted.
|
||||||
|
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
||||||
*/
|
*/
|
||||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
||||||
if (mClient == null) return false;
|
if (mClient == null) return false;
|
||||||
|
|
||||||
CustomTabsSession session = getSession();
|
CustomTabsSession session = getSession();
|
||||||
if (session == null) return false;
|
return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
||||||
|
|
||||||
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -141,9 +144,8 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
|||||||
*/
|
*/
|
||||||
public interface CustomTabFallback {
|
public interface CustomTabFallback {
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param activity The Activity that wants to open the Uri.
|
* @param activity The Activity that wants to open the Uri.
|
||||||
* @param uri The uri to be opened by the fallback.
|
* @param uri The uri to be opened by the fallback.
|
||||||
*/
|
*/
|
||||||
void openUri(Activity activity, Uri uri);
|
void openUri(Activity activity, Uri uri);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.customtabs.CustomTabsService;
|
import androidx.browser.customtabs.CustomTabsService;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
@SuppressWarnings("ALL")
|
||||||
class CustomTabsHelper {
|
class CustomTabsHelper {
|
||||||
private static final String TAG = "CustomTabsHelper";
|
private static final String TAG = "CustomTabsHelper";
|
||||||
@ -26,7 +28,8 @@ class CustomTabsHelper {
|
|||||||
|
|
||||||
private static String sPackageNameToUse;
|
private static String sPackageNameToUse;
|
||||||
|
|
||||||
private CustomTabsHelper() {}
|
private CustomTabsHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
public static void addKeepAliveExtra(Context context, Intent intent) {
|
||||||
Intent keepAliveIntent = new Intent().setClassName(
|
Intent keepAliveIntent = new Intent().setClassName(
|
||||||
@ -38,7 +41,7 @@ class CustomTabsHelper {
|
|||||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
* 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
|
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||||
* valid package name.
|
* valid package name.
|
||||||
*
|
* <p>
|
||||||
* This is <strong>not</strong> threadsafe.
|
* This is <strong>not</strong> threadsafe.
|
||||||
*
|
*
|
||||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
||||||
@ -92,6 +95,7 @@ class CustomTabsHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to check whether there is a specialized handler for a given intent.
|
* Used to check whether there is a specialized handler for a given intent.
|
||||||
|
*
|
||||||
* @param intent The intent to check with.
|
* @param intent The intent to check with.
|
||||||
* @return Whether there is a specialized handler for the given intent.
|
* @return Whether there is a specialized handler for the given intent.
|
||||||
*/
|
*/
|
||||||
@ -101,7 +105,7 @@ class CustomTabsHelper {
|
|||||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||||
intent,
|
intent,
|
||||||
PackageManager.GET_RESOLVED_FILTER);
|
PackageManager.GET_RESOLVED_FILTER);
|
||||||
if (handlers == null || handlers.size() == 0) {
|
if (handlers == null || handlers.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (ResolveInfo resolveInfo : handlers) {
|
for (ResolveInfo resolveInfo : handlers) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.support.customtabs.CustomTabsClient;
|
import androidx.browser.customtabs.CustomTabsClient;
|
||||||
import android.support.customtabs.CustomTabsServiceConnection;
|
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.support.customtabs.CustomTabsClient;
|
import androidx.browser.customtabs.CustomTabsClient;
|
||||||
|
|
||||||
|
|
||||||
public interface ServiceConnectionCallback {
|
public interface ServiceConnectionCallback {
|
||||||
/**
|
/**
|
||||||
* Called when the service is connected.
|
* Called when the service is connected.
|
||||||
|
*
|
||||||
* @param client a CustomTabsClient
|
* @param client a CustomTabsClient
|
||||||
*/
|
*/
|
||||||
void onServiceConnected(CustomTabsClient client);
|
void onServiceConnected(CustomTabsClient client);
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
|
/* 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
|
package apps.amine.bou.readerforselfoss.utils.drawer
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
|
||||||
|
|
||||||
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
||||||
var icon: ImageView = view.findViewById(R.id.material_drawer_icon) as ImageView
|
var icon: ImageView = view.findViewById(R.id.material_drawer_icon)
|
||||||
var name: TextView = view.findViewById(R.id.material_drawer_name) as TextView
|
var name: TextView = view.findViewById(R.id.material_drawer_name)
|
||||||
var description: TextView = view.findViewById(R.id.material_drawer_description) as TextView
|
var description: TextView = view.findViewById(R.id.material_drawer_description)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,69 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
|
import android.widget.ImageView
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) =
|
||||||
|
Glide.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.loadMaybeBasicAuth(config, url)
|
||||||
|
.apply(RequestOptions.centerCropTransform())
|
||||||
|
.into(iv)
|
||||||
|
|
||||||
|
fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
|
||||||
|
Glide.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.loadMaybeBasicAuth(config, url)
|
||||||
|
.apply(RequestOptions.centerCropTransform())
|
||||||
|
.into(object : BitmapImageViewTarget(iv) {
|
||||||
|
override fun setResource(resource: Bitmap?) {
|
||||||
|
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
|
||||||
|
resources,
|
||||||
|
resource
|
||||||
|
)
|
||||||
|
circularBitmapDrawable.isCircular = true
|
||||||
|
iv.setImageDrawable(circularBitmapDrawable)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
|
||||||
|
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
|
||||||
|
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
|
||||||
|
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
|
||||||
|
builder.addHeader("Authorization", basicAuth)
|
||||||
|
}
|
||||||
|
val glideUrl = GlideUrl(url, builder.build())
|
||||||
|
return this.load(glideUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
|
||||||
|
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
|
||||||
|
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
|
||||||
|
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
|
||||||
|
builder.addHeader("Authorization", basicAuth)
|
||||||
|
}
|
||||||
|
val glideUrl = GlideUrl(url, builder.build())
|
||||||
|
return this.load(glideUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
|
||||||
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
|
||||||
|
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
||||||
|
return ByteArrayInputStream(bitmapData)
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.GlideBuilder
|
||||||
|
import com.bumptech.glide.Registry
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.module.GlideModule
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class SelfSignedGlideModule : GlideModule {
|
||||||
|
|
||||||
|
override fun applyOptions(context: Context?, builder: GlideBuilder?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
if (pref.getBoolean("isSelfSignedCert", false)) {
|
||||||
|
val client = getUnsafeHttpClient().build()
|
||||||
|
|
||||||
|
registry?.append(
|
||||||
|
GlideUrl::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
|
var snackBarShown = false
|
||||||
|
var view: View? = null
|
||||||
|
lateinit var s: Snackbar
|
||||||
|
|
||||||
|
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
|
||||||
|
val networkIsAccessible = isNetworkAvailable(this)
|
||||||
|
|
||||||
|
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
|
||||||
|
view = v
|
||||||
|
s = Snackbar
|
||||||
|
.make(
|
||||||
|
v,
|
||||||
|
R.string.no_network_connectivity,
|
||||||
|
Snackbar.LENGTH_INDEFINITE
|
||||||
|
)
|
||||||
|
|
||||||
|
s.setAction(android.R.string.ok) {
|
||||||
|
snackBarShown = false
|
||||||
|
s.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val view = s.view
|
||||||
|
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||||
|
tv.setTextColor(Color.WHITE)
|
||||||
|
s.show()
|
||||||
|
snackBarShown = true
|
||||||
|
}
|
||||||
|
if (snackBarShown && networkIsAccessible && !overrideOffline) {
|
||||||
|
s.dismiss()
|
||||||
|
}
|
||||||
|
return if(overrideOffline) overrideOffline else networkIsAccessible
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNetworkAvailable(context: Context): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
|
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
|
||||||
|
return when {
|
||||||
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||||
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||||
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val network = connectivityManager.activeNetworkInfo ?: return false
|
||||||
|
return network.isConnectedOrConnecting
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.persistence
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Source
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Tag
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
|
||||||
|
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
|
||||||
|
|
||||||
|
fun TagEntity.toView(): Tag =
|
||||||
|
Tag(
|
||||||
|
this.tag,
|
||||||
|
this.color,
|
||||||
|
this.unread
|
||||||
|
)
|
||||||
|
|
||||||
|
fun SourceEntity.toView(): Source =
|
||||||
|
Source(
|
||||||
|
this.id,
|
||||||
|
this.title,
|
||||||
|
SelfossTagType(this.tags),
|
||||||
|
this.spout,
|
||||||
|
this.error,
|
||||||
|
this.icon
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Source.toEntity(): SourceEntity =
|
||||||
|
SourceEntity(
|
||||||
|
this.id,
|
||||||
|
this.getTitleDecoded(),
|
||||||
|
this.tags.tags,
|
||||||
|
this.spout,
|
||||||
|
this.error,
|
||||||
|
this.icon.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Tag.toEntity(): TagEntity =
|
||||||
|
TagEntity(
|
||||||
|
this.tag,
|
||||||
|
this.color,
|
||||||
|
this.unread
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ItemEntity.toView(): Item =
|
||||||
|
Item(
|
||||||
|
this.id,
|
||||||
|
this.datetime,
|
||||||
|
this.title,
|
||||||
|
this.content,
|
||||||
|
this.unread,
|
||||||
|
this.starred,
|
||||||
|
this.thumbnail,
|
||||||
|
this.icon,
|
||||||
|
this.link,
|
||||||
|
this.sourcetitle,
|
||||||
|
SelfossTagType(this.tags)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Item.toEntity(): ItemEntity =
|
||||||
|
ItemEntity(
|
||||||
|
this.id,
|
||||||
|
this.datetime,
|
||||||
|
this.getTitleDecoded(),
|
||||||
|
this.content,
|
||||||
|
this.unread,
|
||||||
|
this.starred,
|
||||||
|
this.thumbnail,
|
||||||
|
this.icon,
|
||||||
|
this.link,
|
||||||
|
this.getSourceTitle(),
|
||||||
|
this.tags.tags
|
||||||
|
)
|
8
app/src/main/res/color/ic_menu_heart_color.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_selected="true"
|
||||||
|
android:color="@color/red"/>
|
||||||
|
|
||||||
|
<item android:state_selected="false"
|
||||||
|
android:color="?android:attr/textColorPrimary" />
|
||||||
|
</selector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<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="#FFFFFFFF"
|
||||||
|
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<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="#FFFFFFFF"
|
||||||
|
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
|
||||||
|
</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 |