Compare commits
444 Commits
v1.5.2.15
...
v171811325
Author | SHA1 | Date | |
---|---|---|---|
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 |
32
.github/CONTRIBUTING.md
vendored
@ -8,9 +8,9 @@ Please read the guidelines before contributing, and follow them (or try to) when
|
||||
|
||||
### What you can do to help.
|
||||
|
||||
There are many ways to contribute to this project, you could 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.
|
||||
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/labels/help%20wanted) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||
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.
|
||||
|
||||
@ -28,10 +28,11 @@ Always check if the web version of your instance is working.
|
||||
|
||||
### 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 official one](https://kotlinlang.org/docs/reference/coding-conventions.html) ([some idoms for reference](http://kotlinlang.org/docs/reference/idioms.html)) with more to come.
|
||||
* 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.
|
||||
@ -46,12 +47,21 @@ You can directly import this project into IntellIJ/Android Studio.
|
||||
|
||||
You'll have to:
|
||||
|
||||
- Configure Fabric, or [remove it](https://docs.fabric.io/android/fabric/settings/removing.html#).
|
||||
- Create a firebase project and add the `google-services.json` to the `app/` folder.
|
||||
- Define the following in `res/values/strings.xml` or create `res/values/secrets.xml`
|
||||
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
|
||||
|
||||
- mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser
|
||||
- feedback_email: An email to receive users feedback.
|
||||
- source_url: an url to the source code, used in the settings
|
||||
- tracker_url: an url to the tracker, used in the settings
|
||||
- To run your app, you'll have to add `-P appLoginUrl="your.selfoss-instance.url" -P appLoginUsername="Username" -P appLoginPassword="password"`. (These are only used to run the espresso tests. You should be able to use empty strings or fake values to build the app)
|
||||
- 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"
|
||||
```
|
||||
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -5,6 +5,7 @@
|
||||
- [ ] 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
|
||||
|
||||
|
4
.gitignore
vendored
@ -214,6 +214,6 @@ gradle-app.setting
|
||||
|
||||
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
|
||||
|
||||
secrets.xml
|
||||
release/
|
||||
|
||||
release/
|
||||
crowdin.properties
|
265
CHANGELOG.md
@ -1,4 +1,265 @@
|
||||
**1.5.2.13**
|
||||
**1.7.x**
|
||||
|
||||
- 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
@ -264,4 +525,4 @@ _Updates_
|
||||
|
||||
**1.3.3.4**
|
||||
|
||||
...
|
||||
...
|
||||
|
23
README.md
@ -1,17 +1,20 @@
|
||||
# ReaderForSelfoss
|
||||
|
||||
[](http://jenkins.amine-bou.fr/job/ReaderForSelfoss/)
|
||||
|
||||
[](https://gitter.im/amine-bou/ReaderForSelfoss)
|
||||
|
||||
[](https://codebeat.co/projects/github-com-aminecmi-readerforselfoss-master)
|
||||
|
||||
[](https://www.codetriage.com/aminecmi/readerforselfoss)
|
||||
|
||||
This is the repo of [Reader For Selfoss](https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss&hl=en).
|
||||
[](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ) [](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/) [](https://www.codetriage.com/aminecmi/readerforselfoss) [](https://crowdin.com/project/readerforselfoss)
|
||||
|
||||
It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/)
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="100"/></a> <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>
|
||||
|
||||
Also, the last APK built from source is available [here](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/lastSuccessfulBuild/artifact/SignApksBuilder-out/selfoss-key/selfoss/app-githubConfig-release-unsigned.apk/app-githubConfig-release.apk).
|
||||
|
||||
## Join the alpha channel
|
||||
|
||||
**Keep in mind, it could be instable, but you'll have the new updates faster**
|
||||
|
||||
- First, join the google [group](https://groups.google.com/d/forum/reader-for-selfoss-alpha-testing).
|
||||
- Then, join the [alpha channel](https://play.google.com/apps/testing/apps.amine.bou.readerforselfoss) of the app.
|
||||
- You'll be able to update the app for the current alpha version.
|
||||
|
||||
## Want to help ?
|
||||
|
||||
@ -22,3 +25,5 @@ Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob
|
||||
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
||||
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
|
||||
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
|
||||
- [Ask for help](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ)
|
||||
|
164
app/build.gradle
@ -1,45 +1,40 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'io.fabric.tools:gradle:1.+'
|
||||
}
|
||||
}
|
||||
|
||||
def gitVersion() {
|
||||
def process = "git describe --abbrev=0 --tags".execute()
|
||||
return process.text.substring(1).replaceAll("\\.", "")
|
||||
def process = "git for-each-ref refs/tags --sort=-taggerdate --format='%(refname:short)' --count=1".execute()
|
||||
return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
|
||||
}
|
||||
|
||||
def versionCodeFromGit() {
|
||||
println "version code " + gitVersion().toInteger()
|
||||
println "version code " + gitVersion()
|
||||
return gitVersion().toInteger()
|
||||
}
|
||||
|
||||
def versionNameFromGit() {
|
||||
println "version code " + gitVersion().trim()
|
||||
return gitVersion().trim()
|
||||
println "version name " + gitVersion()
|
||||
return gitVersion()
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
repositories {
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
}
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion "26.0.1"
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
defaultConfig {
|
||||
applicationId "apps.amine.bou.readerforselfoss"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
targetSdkVersion 28
|
||||
versionCode versionCodeFromGit()
|
||||
versionName versionNameFromGit()
|
||||
|
||||
@ -52,11 +47,19 @@ android {
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
// tests
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation":
|
||||
"$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard-rules.pro'
|
||||
}
|
||||
@ -71,113 +74,92 @@ android {
|
||||
githubConfig {
|
||||
versionNameSuffix '-github'
|
||||
dimension "build"
|
||||
buildConfigField "boolean", "GITHUB_VERSION", "true"
|
||||
}
|
||||
storeConfig {
|
||||
// As jenkins publishes to alpha first, this is the default suffix now.
|
||||
versionNameSuffix '-store'
|
||||
dimension "build"
|
||||
buildConfigField "boolean", "GITHUB_VERSION", "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Testing
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.0'
|
||||
androidTestCompile 'com.android.support.test:runner:1.0.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta02'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.0-beta02'
|
||||
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:3.0.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-beta02'
|
||||
// Espresso-intents for validation and stubbing of Intents
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-intents:3.0.0'
|
||||
|
||||
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-beta02'
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
// Android Support
|
||||
compile 'com.android.support:appcompat-v7:26.0.1'
|
||||
compile 'com.android.support:design:26.0.1'
|
||||
compile 'com.android.support:recyclerview-v7:26.0.1'
|
||||
compile 'com.android.support:support-v4:26.0.1'
|
||||
compile 'com.android.support:support-vector-drawable:26.0.1'
|
||||
compile 'com.android.support:customtabs:26.0.1'
|
||||
compile 'com.android.support:cardview-v7:26.0.1'
|
||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
// Firebase + crashlytics
|
||||
compile 'com.google.firebase:firebase-core:11.2.0'
|
||||
compile 'com.google.firebase:firebase-config:11.2.0'
|
||||
compile 'com.google.firebase:firebase-invites:11.2.0'
|
||||
compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation "androidx.appcompat:appcompat:$android_version"
|
||||
implementation "com.google.android.material:material:$android_version"
|
||||
implementation "androidx.recyclerview:recyclerview:$android_version"
|
||||
implementation "androidx.legacy:legacy-support-v4:$android_version"
|
||||
implementation "androidx.vectordrawable:vectordrawable:$android_version"
|
||||
implementation "androidx.browser:browser:$android_version"
|
||||
implementation "androidx.cardview:cardview:$android_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
|
||||
|
||||
//multidex
|
||||
compile 'com.android.support:multidex:1.0.2'
|
||||
|
||||
// Intro
|
||||
compile 'agency.tango.android:material-intro-screen:0.0.5'
|
||||
implementation 'androidx.multidex:multidex:2.0.0'
|
||||
|
||||
// About
|
||||
compile('com.mikepenz:aboutlibraries:5.9.7@aar') {
|
||||
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
// Retrofit + http logging + okhttp
|
||||
compile 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
|
||||
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
|
||||
compile 'com.burgstaller:okhttp-digest:1.12'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
||||
implementation 'com.burgstaller:okhttp-digest:1.12'
|
||||
|
||||
// Material-ish things
|
||||
compile 'com.ashokvarma.android:bottom-navigation-bar:2.0.2'
|
||||
compile 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
compile 'com.github.jd-alexander:LikeButton:0.2.1'
|
||||
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
compile 'org.sufficientlysecure:html-textview:3.3'
|
||||
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5'
|
||||
implementation 'com.github.jd-alexander:LikeButton:0.2.3'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
|
||||
// glide
|
||||
compile('com.github.bumptech.glide:glide:4.1.0@aar') {
|
||||
transitive = true;
|
||||
}
|
||||
compile('com.github.bumptech.glide:okhttp3-integration:4.1.0@aar')
|
||||
implementation 'com.github.bumptech.glide:glide:4.1.1'
|
||||
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.3'
|
||||
implementation 'com.github.stkent:amplify:2.2.0'
|
||||
|
||||
// Drawer
|
||||
compile('com.mikepenz:materialdrawer:5.9.5@aar') {
|
||||
transitive = true
|
||||
}
|
||||
compile 'com.anupcowkur:reservoir:3.1.0'
|
||||
implementation 'co.zsmb:materialdrawer-kt:2.0.1'
|
||||
|
||||
// Themes
|
||||
compile 'com.52inc:scoops:1.0.0'
|
||||
implementation 'com.52inc:scoops:1.0.0'
|
||||
implementation 'com.jaredrummler:colorpicker:1.0.2'
|
||||
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
|
||||
|
||||
// Pager
|
||||
implementation 'me.relex:circleindicator:2.0.0@aar'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.0.0'
|
||||
|
||||
// Crash
|
||||
implementation 'ch.acra:acra-http:5.2.1'
|
||||
implementation 'ch.acra:acra-dialog:5.2.1'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation "android.arch.work:work-runtime-ktx:$work_version"
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
|
||||
afterEvaluate {
|
||||
initFabricPropertiesIfNeeded()
|
||||
initAppLoginPropertiesIfNeeded()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def initAppLoginPropertiesIfNeeded() {
|
||||
def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties')
|
||||
if (!propertiesFile.exists()) {
|
||||
@ -188,4 +170,4 @@ def initAppLoginPropertiesIfNeeded() {
|
||||
entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
app/proguard-rules.pro
vendored
@ -30,25 +30,13 @@
|
||||
<fields>;
|
||||
}
|
||||
|
||||
|
||||
##Retrofit
|
||||
#-keep class com.google.gson.** { *; }
|
||||
#-keep class com.google.inject.** { *; }
|
||||
#-keep class org.apache.http.** { *; }
|
||||
#-keep class org.apache.james.mime4j.** { *; }
|
||||
#-keep class javax.inject.** { *; }
|
||||
#-keep class retrofit.** { *; }
|
||||
#-keepclassmembernames interface * {
|
||||
# @retrofit.http.* <methods>;
|
||||
#}
|
||||
#-keep class retrofit.** { *; }
|
||||
#-keep class apps.amine.bou.readerforselfoss.api.selfoss.model.** { *; }
|
||||
#-keepclassmembernames interface * {
|
||||
# @retrofit.http.* <methods>;
|
||||
#}
|
||||
-dontwarn okio.**
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
-keepattributes Signature
|
||||
-keep class retrofit.** { *; }
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit.http.* <methods>;
|
||||
}
|
||||
-keepattributes *Annotation*,Signature
|
||||
-keepattributes Exceptions
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.Nullable
|
||||
@ -60,4 +48,18 @@
|
||||
|
||||
|
||||
# self signed glidemodule
|
||||
-keep public class * implements com.bumptech.glide.module.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\")"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
|
||||
// TODO: test source adding
|
@ -2,32 +2,35 @@ package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.espresso.Espresso.onView
|
||||
import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import android.support.test.espresso.action.ViewActions.*
|
||||
import android.support.test.espresso.assertion.ViewAssertions.matches
|
||||
import android.support.test.espresso.contrib.DrawerActions
|
||||
import android.support.test.espresso.intent.Intents
|
||||
import android.support.test.espresso.intent.Intents.intended
|
||||
import android.support.test.espresso.intent.Intents.times
|
||||
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import android.support.test.espresso.matcher.ViewMatchers.*
|
||||
import android.support.test.rule.ActivityTestRule
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
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 com.mikepenz.aboutlibraries.ui.LibsActivity
|
||||
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
|
||||
|
||||
import apps.amine.bou.readerforselfoss.settings.SettingsActivity
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import org.junit.After
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class HomeActivityEspressoTest {
|
||||
lateinit var context: Context
|
||||
@ -40,9 +43,9 @@ class HomeActivityEspressoTest {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val editor =
|
||||
context
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
context
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
editor.clear()
|
||||
|
||||
editor.putString("url", BuildConfig.LOGIN_URL)
|
||||
@ -60,58 +63,32 @@ class HomeActivityEspressoTest {
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(
|
||||
withMenu(
|
||||
id = R.id.action_search,
|
||||
titleId = R.string.menu_home_search
|
||||
)
|
||||
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(withId(R.id.search_src_text)).perform(
|
||||
typeText("android"),
|
||||
pressKey(KeyEvent.KEYCODE_SEARCH),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
|
||||
|
||||
|
||||
onView(withMenu(id = R.id.readAll, titleId = R.string.readAll)).perform(click())
|
||||
|
||||
openActionBarOverflowOrOptionsMenu(context)
|
||||
|
||||
onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
openActionBarOverflowOrOptionsMenu(context)
|
||||
|
||||
onView(withText(R.string.action_disconnect)).perform(click())
|
||||
|
||||
intended(hasComponent(LoginActivity::class.java.name), times(1))
|
||||
|
||||
//onView(isRoot()).perform(pressBack())
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun drawerTesting() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open())
|
||||
|
||||
onView(withText(R.string.action_about)).perform(click())
|
||||
intended(hasComponent(LibsActivity::class.java.name))
|
||||
onView(isRoot()).perform(pressBack())
|
||||
intended(hasComponent(HomeActivity::class.java.name))
|
||||
|
||||
onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open())
|
||||
|
||||
onView(withId(R.id.material_drawer_layout)).perform(DrawerActions.open())
|
||||
onView(withText(R.string.drawer_action_clear)).perform(click())
|
||||
|
||||
// bug
|
||||
//onView(withText(R.string.title_activity_settings)).perform(scrollTo(), click())
|
||||
//intended(hasComponent(SettingsActivity::class.java.name))
|
||||
|
||||
}
|
||||
|
||||
// TODO: test articles opening and actions for cards and lists
|
||||
|
@ -1,121 +0,0 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import java.util.*
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.test.InstrumentationRegistry.getInstrumentation
|
||||
import android.support.test.espresso.Espresso.onView
|
||||
import android.support.test.espresso.action.ViewActions.click
|
||||
import android.support.test.espresso.assertion.ViewAssertions.matches
|
||||
import android.support.test.espresso.intent.Intents
|
||||
import android.support.test.espresso.intent.Intents.intended
|
||||
import android.support.test.espresso.intent.Intents.times
|
||||
import android.support.test.espresso.intent.matcher.IntentMatchers.*
|
||||
import android.support.test.espresso.intent.matcher.UriMatchers.hasHost
|
||||
import android.support.test.espresso.matcher.ViewMatchers.*
|
||||
import android.support.test.rule.ActivityTestRule
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import org.junit.After
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class IntroActivityEspressoTest {
|
||||
|
||||
@Rule @JvmField
|
||||
val rule = ActivityTestRule(IntroActivity::class.java, true, false)
|
||||
|
||||
@Before
|
||||
fun clearData() {
|
||||
val editor =
|
||||
getInstrumentation().targetContext
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
editor.clear()
|
||||
editor.commit()
|
||||
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
/*@Test
|
||||
fun nextEachTimes() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
|
||||
intended(hasComponent(IntroActivity::class.java.name), times(1))
|
||||
intended(hasComponent(LoginActivity::class.java.name), times(1))
|
||||
|
||||
}*/
|
||||
|
||||
@Test
|
||||
fun nextBackRandomTimes() {
|
||||
val max = 5
|
||||
val min = 1
|
||||
|
||||
val random = (Random().nextInt(max + 1 - min)) + min
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
|
||||
repeat(random) {_ ->
|
||||
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_back)).perform(click())
|
||||
}
|
||||
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
|
||||
intended(hasComponent(IntroActivity::class.java.name), times(1))
|
||||
intended(hasComponent(LoginActivity::class.java.name), times(1))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clickSelfossUrl() {
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
|
||||
|
||||
onView(withId(R.id.button_next)).perform(click())
|
||||
|
||||
onView(withId(R.id.button_message)).perform(click())
|
||||
|
||||
intended(
|
||||
allOf(
|
||||
hasData(
|
||||
hasHost(
|
||||
equalTo("selfoss.aditu.de")
|
||||
)
|
||||
),
|
||||
hasAction(Intent.ACTION_VIEW)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
@ -2,48 +2,51 @@ package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.test.InstrumentationRegistry
|
||||
import android.support.test.espresso.Espresso.onView
|
||||
import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import android.support.test.espresso.action.ViewActions.*
|
||||
import android.support.test.espresso.assertion.ViewAssertions.matches
|
||||
import android.support.test.espresso.intent.Intents
|
||||
import android.support.test.espresso.intent.Intents.intended
|
||||
import android.support.test.espresso.intent.Intents.times
|
||||
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import android.support.test.espresso.matcher.ViewMatchers
|
||||
import android.support.test.espresso.matcher.ViewMatchers.*
|
||||
import android.support.test.rule.ActivityTestRule
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
|
||||
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
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import org.junit.After
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LoginActivityEspressoTest {
|
||||
|
||||
@Rule @JvmField
|
||||
val rule = ActivityTestRule(LoginActivity::class.java, true, false)
|
||||
|
||||
lateinit var context: Context
|
||||
lateinit var url: String
|
||||
lateinit var username: String
|
||||
lateinit var password: String
|
||||
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()
|
||||
context
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
editor.clear()
|
||||
editor.commit()
|
||||
|
||||
@ -69,20 +72,18 @@ class LoginActivityEspressoTest {
|
||||
onView(isRoot()).perform(pressBack())
|
||||
|
||||
intended(hasComponent(LoginActivity::class.java.name))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun wrongLoginUrl() {
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.login_progress))
|
||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
onView(withId(R.id.loginProgress))
|
||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
|
||||
onView(withId(R.id.url)).perform(click()).perform(typeText("WRONGURL"))
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
|
||||
|
||||
onView(withId(R.id.email_sign_in_button)).perform(click())
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
|
||||
}
|
||||
@ -94,26 +95,29 @@ class LoginActivityEspressoTest {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.email_sign_in_button)).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.login)).perform(click()).perform(typeText(username), closeSoftKeyboard())
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||
|
||||
onView(withId(R.id.email_sign_in_button)).perform(click())
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.passwordLayout)).check(
|
||||
matches(
|
||||
isHintOrErrorEnabled())
|
||||
matches(
|
||||
isHintOrErrorEnabled()
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -121,20 +125,25 @@ class LoginActivityEspressoTest {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.login)).perform(click()).perform(typeText(username), closeSoftKeyboard())
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.password)).perform(click()).perform(typeText("WRONGPASS"), closeSoftKeyboard())
|
||||
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||
typeText("WRONGPASS"),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.email_sign_in_button)).perform(click())
|
||||
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
|
||||
@ -142,23 +151,27 @@ class LoginActivityEspressoTest {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.url)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.login)).perform(click()).perform(typeText(username), closeSoftKeyboard())
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.password)).perform(click()).perform(typeText(password), closeSoftKeyboard())
|
||||
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||
typeText(password),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.email_sign_in_button)).perform(click())
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
intended(hasComponent(HomeActivity::class.java.name))
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
}
|
@ -3,13 +3,13 @@ package apps.amine.bou.readerforselfoss
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.test.InstrumentationRegistry.getInstrumentation
|
||||
import android.support.test.espresso.intent.Intents
|
||||
import android.support.test.espresso.intent.Intents.intended
|
||||
import android.support.test.espresso.intent.Intents.times
|
||||
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import android.support.test.rule.ActivityTestRule
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
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
|
||||
@ -17,7 +17,6 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainActivityEspressoTest {
|
||||
|
||||
@ -46,9 +45,7 @@ class MainActivityEspressoTest {
|
||||
rule.launchActivity(intent)
|
||||
|
||||
intended(hasComponent(MainActivity::class.java.name))
|
||||
intended(hasComponent(IntroActivity::class.java.name))
|
||||
intended(hasComponent(LoginActivity::class.java.name), times(0))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -60,14 +57,10 @@ class MainActivityEspressoTest {
|
||||
|
||||
intended(hasComponent(MainActivity::class.java.name))
|
||||
intended(hasComponent(LoginActivity::class.java.name))
|
||||
intended(hasComponent(IntroActivity::class.java.name), times(0))
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,31 +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 android.support.design.widget.TextInputLayout
|
||||
import android.support.test.espresso.matcher.ViewMatchers
|
||||
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
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
|
||||
object : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description?) {
|
||||
}
|
||||
|
||||
return item.isHintEnabled || item.isErrorEnabled
|
||||
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)
|
||||
)
|
||||
Matchers.anyOf(
|
||||
ViewMatchers.withId(id),
|
||||
ViewMatchers.withText(titleId)
|
||||
)
|
||||
|
@ -1,12 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="apps.amine.bou.readerforselfoss">
|
||||
package="apps.amine.bou.readerforselfoss"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- For firebase only -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MyApp"
|
||||
@ -14,6 +12,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/NoBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -23,12 +22,12 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".IntroActivity"
|
||||
android:theme="@style/Theme.Intro">
|
||||
</activity>
|
||||
<activity android:name=".LoginActivity"
|
||||
android:name=".LoginActivity"
|
||||
android:label="@string/title_activity_login">
|
||||
</activity>
|
||||
<activity android:name=".HomeActivity">
|
||||
@ -41,13 +40,15 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
|
||||
</activity>
|
||||
<activity android:name=".SourcesActivity"
|
||||
<activity
|
||||
android:name=".SourcesActivity"
|
||||
android:parentActivityName=".HomeActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".HomeActivity" />
|
||||
</activity>
|
||||
<activity android:name=".AddSourceActivity"
|
||||
<activity
|
||||
android:name=".AddSourceActivity"
|
||||
android:parentActivityName=".SourcesActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
@ -61,12 +62,21 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ReaderActivity"
|
||||
android:theme="@style/DragDismissTheme">
|
||||
<activity
|
||||
android:name=".ReaderActivity">
|
||||
</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" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@ -1,71 +1,134 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.constraint.ConstraintLayout
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
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.Spout
|
||||
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.isBaseUrlValid
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.android.synthetic.main.activity_add_source.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import android.graphics.PorterDuff
|
||||
|
||||
|
||||
|
||||
class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
private var mSpoutsValue: String? = null
|
||||
private lateinit var api: SelfossApi
|
||||
|
||||
private lateinit var appColors: AppColors
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@AddSourceActivity)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
Scoop.getInstance().apply(this)
|
||||
|
||||
setContentView(R.layout.activity_add_source)
|
||||
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
}
|
||||
|
||||
val drawable = nameInput.background
|
||||
drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||
|
||||
|
||||
// TODO: clean
|
||||
if(Build.VERSION.SDK_INT > 16) {
|
||||
nameInput.background = drawable
|
||||
} else{
|
||||
nameInput.setBackgroundDrawable(drawable)
|
||||
}
|
||||
|
||||
val drawable1 = sourceUri.background
|
||||
drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||
|
||||
if(Build.VERSION.SDK_INT > 16) {
|
||||
sourceUri.background = drawable1
|
||||
} else{
|
||||
sourceUri.setBackgroundDrawable(drawable1)
|
||||
}
|
||||
|
||||
val drawable2 = tags.background
|
||||
drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
|
||||
|
||||
if(Build.VERSION.SDK_INT > 16) {
|
||||
tags.background = drawable2
|
||||
} else{
|
||||
tags.setBackgroundDrawable(drawable2)
|
||||
}
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val mProgress: ProgressBar = findViewById(R.id.progress)
|
||||
val mForm: ConstraintLayout = findViewById(R.id.formContainer)
|
||||
val mNameInput: EditText = findViewById(R.id.nameInput)
|
||||
val mSourceUri: EditText = findViewById(R.id.sourceUri)
|
||||
val mTags: EditText = findViewById(R.id.tags)
|
||||
val mSpoutsSpinner: Spinner = findViewById(R.id.spoutsSpinner)
|
||||
val mSaveBtn: Button = findViewById(R.id.saveBtn)
|
||||
var api: SelfossApi? = null
|
||||
|
||||
try {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
api = SelfossApi(this, this@AddSourceActivity, prefs.getBoolean("isSelfSignedCert", false))
|
||||
api = SelfossApi(
|
||||
this,
|
||||
this@AddSourceActivity,
|
||||
prefs.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1").toLong(),
|
||||
prefs.getBoolean("should_log_everything", false)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
mustLoginToAddSource()
|
||||
}
|
||||
|
||||
val intent = intent
|
||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
||||
mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||
mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
||||
maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput)
|
||||
|
||||
saveBtn.setTextColor(appColors.colorAccent)
|
||||
|
||||
saveBtn.setOnClickListener {
|
||||
handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!)
|
||||
}
|
||||
}
|
||||
|
||||
mSaveBtn.setOnClickListener {
|
||||
handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api!!)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val config = Config(this)
|
||||
|
||||
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false)) {
|
||||
mustLoginToAddSource()
|
||||
} else {
|
||||
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleSpoutsSpinner(
|
||||
spoutsSpinner: Spinner,
|
||||
api: SelfossApi?,
|
||||
mProgress: ProgressBar,
|
||||
formContainer: ConstraintLayout
|
||||
) {
|
||||
val spoutsKV = HashMap<String, String>()
|
||||
mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
||||
val spoutName = (view as TextView).text.toString()
|
||||
mSpoutsValue = spoutsKV[spoutName]
|
||||
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
|
||||
if (view != null) {
|
||||
val spoutName = (view as TextView).text.toString()
|
||||
mSpoutsValue = spoutsKV[spoutName]
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||
@ -73,48 +136,59 @@ 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() || !config.baseUrl.isBaseUrlValid()) {
|
||||
mustLoginToAddSource()
|
||||
} 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()
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProblemWithSpouts() {
|
||||
Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
|
||||
mProgress.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
||||
handleProblemWithSpouts()
|
||||
}
|
||||
|
||||
private fun handleProblemWithSpouts() {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_get_spouts,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
mProgress.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun 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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,28 +199,42 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
|
||||
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
|
||||
|
||||
if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
|
||||
val sourceDetailsAvailable =
|
||||
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
|
||||
|
||||
if (sourceDetailsAvailable) {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
api.createSource(
|
||||
title,
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
mTags.text.toString(),
|
||||
tags.text.toString(),
|
||||
""
|
||||
).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<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()
|
||||
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()
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.View
|
||||
|
||||
import agency.tango.materialintroscreen.MaterialIntroActivity
|
||||
import agency.tango.materialintroscreen.MessageButtonBehaviour
|
||||
import agency.tango.materialintroscreen.SlideFragmentBuilder
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
|
||||
|
||||
class IntroActivity : MaterialIntroActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
|
||||
addSlide(SlideFragmentBuilder()
|
||||
.backgroundColor(R.color.colorPrimary)
|
||||
.buttonsColor(R.color.colorAccent)
|
||||
.image(R.drawable.web_hi_res_512)
|
||||
.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_48px)
|
||||
.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_48px)
|
||||
.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,34 +6,30 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.TextInputLayout
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
import org.acra.ACRA
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
|
||||
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.ftinc.scoop.Scoop
|
||||
import io.fabric.sdk.android.Fabric
|
||||
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private var inValidCount: Int = 0
|
||||
@ -43,27 +39,74 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var settings: SharedPreferences
|
||||
private lateinit var editor: SharedPreferences.Editor
|
||||
private lateinit var mFirebaseAnalytics: FirebaseAnalytics
|
||||
private lateinit var mUrlView: EditText
|
||||
private lateinit var mLoginView: TextView
|
||||
private lateinit var mHTTPLoginView: TextView
|
||||
private lateinit var mProgressView: View
|
||||
private lateinit var mPasswordView: EditText
|
||||
private lateinit var mHTTPPasswordView: EditText
|
||||
private lateinit var mLoginFormView: View
|
||||
private lateinit var userIdentifier: String
|
||||
private var logErrors: Boolean = false
|
||||
|
||||
|
||||
private lateinit var appColors: AppColors
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@LoginActivity)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
Scoop.getInstance().apply(this)
|
||||
|
||||
setContentView(R.layout.activity_login)
|
||||
|
||||
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
handleBaseUrlFail()
|
||||
|
||||
|
||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
userIdentifier = settings.getString("unique_id", "")
|
||||
logErrors = settings.getBoolean("login_debug", false)
|
||||
|
||||
editor = settings.edit()
|
||||
|
||||
if (settings.getString("url", "").isNotEmpty()) {
|
||||
goToMain()
|
||||
}
|
||||
|
||||
handleActions()
|
||||
}
|
||||
|
||||
private fun handleActions() {
|
||||
|
||||
withSelfhostedCert.setOnCheckedChangeListener { _, b ->
|
||||
isWithSelfSignedCert = !isWithSelfSignedCert
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
warningText.visibility = visi
|
||||
}
|
||||
|
||||
passwordView.setOnEditorActionListener(
|
||||
TextView.OnEditorActionListener { _, id, _ ->
|
||||
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
|
||||
attemptLogin()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
)
|
||||
|
||||
signInButton.setOnClickListener { attemptLogin() }
|
||||
|
||||
withLogin.setOnCheckedChangeListener { _, b ->
|
||||
isWithLogin = !isWithLogin
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
loginLayout.visibility = visi
|
||||
passwordLayout.visibility = visi
|
||||
}
|
||||
|
||||
withHttpLogin.setOnCheckedChangeListener { _, b ->
|
||||
isWithHTTPLogin = !isWithHTTPLogin
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
httpLoginInput.visibility = visi
|
||||
httpPasswordInput.visibility = visi
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBaseUrlFail() {
|
||||
if (intent.getBooleanExtra("baseUrlFail", false)) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||
@ -71,74 +114,10 @@ class LoginActivity : AppCompatActivity() {
|
||||
alertDialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
"OK",
|
||||
{ dialog, _ -> dialog.dismiss() })
|
||||
{ dialog, _ -> dialog.dismiss() }
|
||||
)
|
||||
alertDialog.show()
|
||||
}
|
||||
|
||||
|
||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
userIdentifier = settings.getString("unique_id", "")
|
||||
logErrors = settings.getBoolean("loging_debug", false)
|
||||
|
||||
editor = settings.edit()
|
||||
|
||||
if (settings.getString("url", "").isNotEmpty()) {
|
||||
goToMain()
|
||||
} else {
|
||||
this@LoginActivity.checkAndDisplayStoreApk()
|
||||
}
|
||||
|
||||
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
|
||||
mUrlView = findViewById(R.id.url)
|
||||
mLoginView = findViewById(R.id.login)
|
||||
mHTTPLoginView = findViewById(R.id.httpLogin)
|
||||
mPasswordView = findViewById(R.id.password)
|
||||
mHTTPPasswordView = findViewById(R.id.httpPassword)
|
||||
mLoginFormView = findViewById(R.id.login_form)
|
||||
mProgressView = findViewById(R.id.login_progress)
|
||||
|
||||
val mSwitch: Switch = findViewById(R.id.withLogin)
|
||||
val mHTTPSwitch: Switch = findViewById(R.id.withHttpLogin)
|
||||
val mLoginLayout: TextInputLayout = findViewById(R.id.loginLayout)
|
||||
val mHTTPLoginLayout: TextInputLayout = findViewById(R.id.httpLoginInput)
|
||||
val mPasswordLayout: TextInputLayout = findViewById(R.id.passwordLayout)
|
||||
val mHTTPPasswordLayout: TextInputLayout = findViewById(R.id.httpPasswordInput)
|
||||
val mEmailSignInButton: Button = findViewById(R.id.email_sign_in_button)
|
||||
val selfHostedSwitch: Switch = findViewById(R.id.withSelfhostedCert)
|
||||
val warningTextview: TextView = findViewById(R.id.warningText)
|
||||
|
||||
selfHostedSwitch.setOnCheckedChangeListener {_, b ->
|
||||
isWithSelfSignedCert = !isWithSelfSignedCert
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
warningTextview.visibility = visi
|
||||
}
|
||||
|
||||
mPasswordView.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
|
||||
if (id == R.id.login || id == EditorInfo.IME_NULL) {
|
||||
attemptLogin()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
|
||||
mEmailSignInButton.setOnClickListener { attemptLogin() }
|
||||
|
||||
mSwitch.setOnCheckedChangeListener { _, b ->
|
||||
isWithLogin = !isWithLogin
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
mLoginLayout.visibility = visi
|
||||
mPasswordLayout.visibility = visi
|
||||
}
|
||||
|
||||
mHTTPSwitch.setOnCheckedChangeListener { _, b ->
|
||||
isWithHTTPLogin = !isWithHTTPLogin
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
mHTTPLoginLayout.visibility = visi
|
||||
mHTTPPasswordLayout.visibility = visi
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToMain() {
|
||||
@ -150,25 +129,25 @@ class LoginActivity : AppCompatActivity() {
|
||||
private fun attemptLogin() {
|
||||
|
||||
// Reset errors.
|
||||
mUrlView.error = null
|
||||
mLoginView.error = null
|
||||
mHTTPLoginView.error = null
|
||||
mPasswordView.error = null
|
||||
mHTTPPasswordView.error = null
|
||||
urlView.error = null
|
||||
loginView.error = null
|
||||
httpLoginView.error = null
|
||||
passwordView.error = null
|
||||
httpPasswordView.error = null
|
||||
|
||||
// Store values at the time of the login attempt.
|
||||
val url = mUrlView.text.toString()
|
||||
val login = mLoginView.text.toString()
|
||||
val httpLogin = mHTTPLoginView.text.toString()
|
||||
val password = mPasswordView.text.toString()
|
||||
val httpPassword = mHTTPPasswordView.text.toString()
|
||||
val url = urlView.text.toString()
|
||||
val login = loginView.text.toString()
|
||||
val httpLogin = httpLoginView.text.toString()
|
||||
val password = passwordView.text.toString()
|
||||
val httpPassword = httpPasswordView.text.toString()
|
||||
|
||||
var cancel = false
|
||||
var focusView: View? = null
|
||||
|
||||
if (!url.isBaseUrlValid()) {
|
||||
mUrlView.error = getString(R.string.login_url_problem)
|
||||
focusView = mUrlView
|
||||
if (!url.isBaseUrlValid(logErrors)) {
|
||||
urlView.error = getString(R.string.login_url_problem)
|
||||
focusView = urlView
|
||||
cancel = true
|
||||
inValidCount++
|
||||
if (inValidCount == 3) {
|
||||
@ -178,7 +157,8 @@ class LoginActivity : AppCompatActivity() {
|
||||
alertDialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
"OK",
|
||||
{ dialog, _ -> dialog.dismiss() })
|
||||
{ dialog, _ -> dialog.dismiss() }
|
||||
)
|
||||
alertDialog.show()
|
||||
inValidCount = 0
|
||||
}
|
||||
@ -186,14 +166,14 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
if (isWithLogin || isWithHTTPLogin) {
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
mPasswordView.error = getString(R.string.error_invalid_password)
|
||||
focusView = mPasswordView
|
||||
passwordView.error = getString(R.string.error_invalid_password)
|
||||
focusView = passwordView
|
||||
cancel = true
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(login)) {
|
||||
mLoginView.error = getString(R.string.error_field_required)
|
||||
focusView = mLoginView
|
||||
loginView.error = getString(R.string.error_field_required)
|
||||
focusView = loginView
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
@ -211,79 +191,93 @@ class LoginActivity : AppCompatActivity() {
|
||||
editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
|
||||
editor.apply()
|
||||
|
||||
val api = SelfossApi(this, this@LoginActivity, isWithSelfSignedCert)
|
||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
||||
private fun preferenceError(t: Throwable) {
|
||||
editor.remove("url")
|
||||
editor.remove("login")
|
||||
editor.remove("httpUserName")
|
||||
editor.remove("password")
|
||||
editor.remove("httpPassword")
|
||||
editor.apply()
|
||||
mUrlView.error = getString(R.string.wrong_infos)
|
||||
mLoginView.error = getString(R.string.wrong_infos)
|
||||
mPasswordView.error = getString(R.string.wrong_infos)
|
||||
mHTTPLoginView.error = getString(R.string.wrong_infos)
|
||||
mHTTPPasswordView.error = getString(R.string.wrong_infos)
|
||||
if (logErrors) {
|
||||
Crashlytics.setUserIdentifier(userIdentifier)
|
||||
Crashlytics.log(100, "LOGIN_DEBUG_ERRROR", t.message)
|
||||
Crashlytics.logException(t)
|
||||
Toast.makeText(this@LoginActivity, t.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
showProgress(false)
|
||||
}
|
||||
val api = SelfossApi(
|
||||
this,
|
||||
this@LoginActivity,
|
||||
isWithSelfSignedCert,
|
||||
-1L,
|
||||
isWithSelfSignedCert
|
||||
)
|
||||
|
||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, Bundle())
|
||||
goToMain()
|
||||
} else {
|
||||
preferenceError(Exception("No response body..."))
|
||||
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
|
||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
||||
private fun preferenceError(t: Throwable) {
|
||||
editor.remove("url")
|
||||
editor.remove("login")
|
||||
editor.remove("httpUserName")
|
||||
editor.remove("password")
|
||||
editor.remove("httpPassword")
|
||||
editor.apply()
|
||||
urlView.error = getString(R.string.wrong_infos)
|
||||
loginView.error = getString(R.string.wrong_infos)
|
||||
passwordView.error = getString(R.string.wrong_infos)
|
||||
httpLoginView.error = getString(R.string.wrong_infos)
|
||||
httpPasswordView.error = getString(R.string.wrong_infos)
|
||||
if (logErrors) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity)
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
t.message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
preferenceError(t)
|
||||
}
|
||||
})
|
||||
override fun onResponse(
|
||||
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) {
|
||||
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||
|
||||
mLoginFormView.visibility = if (show) View.GONE else View.VISIBLE
|
||||
mLoginFormView
|
||||
loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
loginForm
|
||||
.animate()
|
||||
.setDuration(shortAnimTime.toLong())
|
||||
.alpha(
|
||||
if (show) 0F else 1F
|
||||
).setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mLoginFormView.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
})
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mProgressView.visibility = if (show) View.VISIBLE else View.GONE
|
||||
mProgressView
|
||||
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
loginProgress
|
||||
.animate()
|
||||
.setDuration(shortAnimTime.toLong())
|
||||
.alpha(
|
||||
if (show) 1F else 0F
|
||||
).setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mProgressView.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
})
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.login_menu, menu)
|
||||
menu.findItem(R.id.loging_debug).isChecked = logErrors
|
||||
menu.findItem(R.id.login_debug).isChecked = logErrors
|
||||
return true
|
||||
}
|
||||
|
||||
@ -297,11 +291,11 @@ class LoginActivity : AppCompatActivity() {
|
||||
.start(this)
|
||||
return true
|
||||
}
|
||||
R.id.loging_debug -> {
|
||||
R.id.login_debug -> {
|
||||
val newState = !item.isChecked
|
||||
item.isChecked = newState
|
||||
logErrors = newState
|
||||
editor.putBoolean("loging_debug", newState)
|
||||
editor.putBoolean("login_debug", newState)
|
||||
editor.apply()
|
||||
return true
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ package apps.amine.bou.readerforselfoss
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@ -13,15 +11,9 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
|
||||
val i = Intent(this@MainActivity, IntroActivity::class.java)
|
||||
startActivity(i)
|
||||
} else {
|
||||
val intent = Intent(this, LoginActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
val intent = Intent(this, LoginActivity::class.java)
|
||||
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,55 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.multidex.MultiDexApplication
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import android.widget.ImageView
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import com.anupcowkur.reservoir.Reservoir
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.ftinc.scoop.Scoop
|
||||
import com.github.stkent.amplify.feedback.DefaultEmailFeedbackCollector
|
||||
import com.github.stkent.amplify.feedback.GooglePlayStoreFeedbackCollector
|
||||
import com.github.stkent.amplify.tracking.Amplify
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.annotation.AcraCore
|
||||
import org.acra.annotation.AcraDialog
|
||||
import org.acra.annotation.AcraHttpSender
|
||||
import org.acra.sender.HttpSender
|
||||
import java.io.IOException
|
||||
import java.util.UUID.randomUUID
|
||||
|
||||
|
||||
|
||||
|
||||
@AcraHttpSender(uri = "http://amine-bou.fr:5984/acra-selfoss/_design/acra-storage/_update/report",
|
||||
basicAuthLogin = "selfoss",
|
||||
basicAuthPassword = "selfoss",
|
||||
httpMethod = HttpSender.Method.PUT)
|
||||
@AcraDialog(resText = R.string.crash_dialog_text,
|
||||
resCommentPrompt = R.string.crash_dialog_comment,
|
||||
resTheme = android.R.style.Theme_DeviceDefault_Dialog)
|
||||
@AcraCore(reportContent = [ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
|
||||
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
|
||||
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
|
||||
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
|
||||
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
|
||||
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
|
||||
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
|
||||
buildConfigClass = BuildConfig::class)
|
||||
class MyApp : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Fabric.with(this, Crashlytics())
|
||||
|
||||
initAmplify()
|
||||
|
||||
initCache()
|
||||
|
||||
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
if (prefs.getString("unique_id", "").isEmpty()) {
|
||||
val editor = prefs.edit()
|
||||
@ -45,29 +63,52 @@ class MyApp : MultiDexApplication() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
ACRA.init(this)
|
||||
ACRA.getErrorReporter().putCustomData("unique_id", prefs.getString("unique_id", ""))
|
||||
|
||||
}
|
||||
|
||||
private fun initAmplify() {
|
||||
Amplify.initSharedInstance(this)
|
||||
.setFeedbackEmailAddress(getString(R.string.feedback_email))
|
||||
.setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
|
||||
.setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
|
||||
.applyAllDefaultRules()
|
||||
}
|
||||
|
||||
private fun initCache() {
|
||||
try {
|
||||
Reservoir.init(this, 8192) //in bytes
|
||||
} catch (e: IOException) {
|
||||
//failure
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDrawerImageLoader() {
|
||||
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)
|
||||
.apply(RequestOptions.fitCenterTransform()
|
||||
.placeholder(placeholder))
|
||||
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
@ -84,19 +125,7 @@ class MyApp : MultiDexApplication() {
|
||||
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)
|
||||
.addFlavor(getString(R.string.teal_orange_theme), R.style.NoBarTealOrange)
|
||||
.addFlavor(getString(R.string.teal_orange_dark_theme), R.style.NoBarTealOrangeDark)
|
||||
.addFlavor(getString(R.string.cyan_pink_theme), R.style.NoBarCyanPink)
|
||||
.addFlavor(getString(R.string.cyan_pink_dark_theme), R.style.NoBarCyanPinkDark)
|
||||
.addFlavor(getString(R.string.grey_orange_theme), R.style.NoBarGreyOrange)
|
||||
.addFlavor(getString(R.string.grey_orange_dark_theme), R.style.NoBarGreyOrangeDark)
|
||||
.addFlavor(getString(R.string.blue_amber_theme), R.style.NoBarBlueAmber)
|
||||
.addFlavor(getString(R.string.blue_amber_dark_theme), R.style.NoBarBlueAmberDark)
|
||||
.addFlavor(getString(R.string.indigo_pink_theme), R.style.NoBarIndigoPink)
|
||||
.addFlavor(getString(R.string.indigo_pink_dark_theme), R.style.NoBarIndigoPinkDark)
|
||||
.addFlavor(getString(R.string.red_teal_theme), R.style.NoBarRedTeal)
|
||||
.addFlavor(getString(R.string.red_teal_dark_theme), R.style.NoBarRedTealDark)
|
||||
.addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
|
||||
.setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
|
||||
.initialize()
|
||||
}
|
||||
@ -105,11 +134,13 @@ class MyApp : MultiDexApplication() {
|
||||
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any { it.toString().contains("android.view.ViewDebug") })
|
||||
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
|
||||
it.toString().contains("android.view.ViewDebug")
|
||||
}) {
|
||||
Unit
|
||||
else
|
||||
} else {
|
||||
oldHandler.uncaughtException(thread, e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,116 +1,324 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.preference.PreferenceManager
|
||||
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.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
import com.bumptech.glide.Glide
|
||||
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
|
||||
import org.sufficientlysecure.htmltextview.HtmlTextView
|
||||
import android.widget.Toast
|
||||
import androidx.room.Room
|
||||
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.fragments.ArticleFragment
|
||||
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.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
|
||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
|
||||
import apps.amine.bou.readerforselfoss.utils.succeeded
|
||||
import apps.amine.bou.readerforselfoss.utils.toggleStar
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import me.relex.circleindicator.CircleIndicator
|
||||
import org.acra.ACRA
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
|
||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
|
||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
||||
import apps.amine.bou.readerforselfoss.utils.shareLink
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.ftinc.scoop.Scoop
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private var markOnScroll: Boolean = false
|
||||
private var debugReadingItems: Boolean = false
|
||||
private var currentItem: Int = 0
|
||||
private lateinit var userIdentifier: String
|
||||
|
||||
class ReaderActivity : DragDismissActivity() {
|
||||
private lateinit var mCustomTabActivityHelper: CustomTabActivityHelper
|
||||
private lateinit var api: SelfossApi
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
mCustomTabActivityHelper.bindCustomTabsService(this)
|
||||
private lateinit var toolbarMenu: Menu
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
|
||||
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
mCustomTabActivityHelper.unbindCustomTabsService(this)
|
||||
private fun canFavorite() {
|
||||
showMenuItem(true)
|
||||
}
|
||||
|
||||
override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
|
||||
Scoop.getInstance().apply(this)
|
||||
val v = inflater.inflate(R.layout.activity_reader, parent, false)
|
||||
showProgressBar()
|
||||
private fun canRemoveFromFavorite() {
|
||||
showMenuItem(false)
|
||||
}
|
||||
|
||||
val image: ImageView = v.findViewById(R.id.imageView)
|
||||
val source: TextView = v.findViewById(R.id.source)
|
||||
val title: TextView = v.findViewById(R.id.title)
|
||||
val content: HtmlTextView = v.findViewById(R.id.content)
|
||||
val url = intent.getStringExtra("url")
|
||||
val parser = MercuryApi(getString(R.string.mercury))
|
||||
val browserBtn: ImageButton = v.findViewById(R.id.browserBtn)
|
||||
val shareBtn: ImageButton = v.findViewById(R.id.shareBtn)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
val customTabsIntent = this@ReaderActivity.buildCustomTabsIntent()
|
||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
||||
mCustomTabActivityHelper.bindCustomTabsService(this)
|
||||
db = Room.databaseBuilder(
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "selfoss-database"
|
||||
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
}
|
||||
|
||||
parser.parseUrl(url).enqueue(object : Callback<ParsedContent> {
|
||||
override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
|
||||
if (response.body() != null && response.body()!!.content != null && response.body()!!.content.isNotEmpty()) {
|
||||
source.text = response.body()!!.domain
|
||||
title.text = response.body()!!.title
|
||||
if (response.body()!!.content != null && !response.body()!!.content.isEmpty()) {
|
||||
try {
|
||||
content.setHtml(response.body()!!.content, HtmlHttpImageGetter(content, null, true))
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
openInBrowserAfterFailing()
|
||||
setSupportActionBar(toolBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
debugReadingItems = prefs.getBoolean("read_debug", false)
|
||||
userIdentifier = prefs.getString("unique_id", "")
|
||||
markOnScroll = prefs.getBoolean("mark_on_scroll", false)
|
||||
|
||||
api = SelfossApi(
|
||||
this,
|
||||
this@ReaderActivity,
|
||||
prefs.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1").toLong(),
|
||||
prefs.getBoolean("should_log_everything", false)
|
||||
)
|
||||
|
||||
if (allItems.isEmpty()) {
|
||||
finish()
|
||||
}
|
||||
|
||||
currentItem = intent.getIntExtra("currentItem", 0)
|
||||
|
||||
readItem(allItems[currentItem])
|
||||
|
||||
pager.adapter =
|
||||
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
|
||||
pager.currentItem = currentItem
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
notifyAdapter()
|
||||
|
||||
pager.setPageTransformer(true, DepthPageTransformer())
|
||||
(indicator as CircleIndicator).setViewPager(pager)
|
||||
|
||||
pager.addOnPageChangeListener(
|
||||
object : ViewPager.SimpleOnPageChangeListener() {
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
readItem(allItems[pager.currentItem])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun readItem(item: Item) {
|
||||
if (markOnScroll) {
|
||||
thread {
|
||||
db.itemsDao().delete(item.toEntity())
|
||||
}
|
||||
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
|
||||
api.markItem(item.id).enqueue(
|
||||
object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (!response.succeeded() && debugReadingItems) {
|
||||
val message =
|
||||
"message: ${response.message()} " +
|
||||
"response isSuccess: ${response.isSuccessful} " +
|
||||
"response code: ${response.code()} " +
|
||||
"response message: ${response.message()} " +
|
||||
"response errorBody: ${response.errorBody()?.string()} " +
|
||||
"body success: ${response.body()?.success} " +
|
||||
"body isSuccess: ${response.body()?.isSuccess}"
|
||||
ACRA.getErrorReporter()
|
||||
.maybeHandleSilentException(Exception(message), this@ReaderActivity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
thread {
|
||||
db.itemsDao().insertAllItems(item.toEntity())
|
||||
}
|
||||
if (debugReadingItems) {
|
||||
ACRA.getErrorReporter()
|
||||
.maybeHandleSilentException(t, this@ReaderActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isEmpty())
|
||||
Glide
|
||||
.with(baseContext)
|
||||
.asBitmap()
|
||||
.load(response.body()!!.lead_image_url)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(image)
|
||||
|
||||
shareBtn.setOnClickListener {
|
||||
this@ReaderActivity.shareLink(response.body()!!.url)
|
||||
}
|
||||
|
||||
browserBtn.setOnClickListener {
|
||||
this@ReaderActivity.openItemUrl(
|
||||
response.body()!!.url,
|
||||
customTabsIntent,
|
||||
false,
|
||||
false,
|
||||
this@ReaderActivity)
|
||||
}
|
||||
|
||||
hideProgressBar()
|
||||
} else openInBrowserAfterFailing()
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ParsedContent>, t: Throwable) = openInBrowserAfterFailing()
|
||||
|
||||
private fun openInBrowserAfterFailing() {
|
||||
this@ReaderActivity.openItemUrl(
|
||||
url,
|
||||
customTabsIntent,
|
||||
true,
|
||||
false,
|
||||
this@ReaderActivity
|
||||
)
|
||||
finish()
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
|
||||
}
|
||||
}
|
||||
})
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAdapter() {
|
||||
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (markOnScroll) {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
fun afterSave() {
|
||||
allItems[pager.currentItem] =
|
||||
allItems[pager.currentItem].toggleStar()
|
||||
notifyAdapter()
|
||||
canRemoveFromFavorite()
|
||||
}
|
||||
|
||||
fun afterUnsave() {
|
||||
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
|
||||
notifyAdapter()
|
||||
canFavorite()
|
||||
}
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.save -> {
|
||||
if (this@ReaderActivity.isNetworkAccessible(null)) {
|
||||
api.starrItem(allItems[pager.currentItem].id)
|
||||
.enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
afterSave()
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
Toast.makeText(
|
||||
baseContext,
|
||||
R.string.cant_mark_favortie,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.unsave -> {
|
||||
if (this@ReaderActivity.isNetworkAccessible(null)) {
|
||||
api.unstarrItem(allItems[pager.currentItem].id)
|
||||
.enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
afterUnsave()
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
Toast.makeText(
|
||||
baseContext,
|
||||
R.string.cant_unmark_favortie,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
|
||||
afterUnsave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
var allItems: ArrayList<Item> = ArrayList()
|
||||
}
|
||||
}
|
||||
|
@ -1,69 +1,105 @@
|
||||
package apps.amine.bou.readerforselfoss
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.Toolbar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.widget.Toast
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton
|
||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.android.synthetic.main.activity_sources.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
||||
import com.ftinc.scoop.Scoop
|
||||
|
||||
|
||||
class SourcesActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var appColors: AppColors
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@SourcesActivity)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
Scoop.getInstance().apply(this)
|
||||
|
||||
setContentView(R.layout.activity_sources)
|
||||
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
}
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
fab.rippleColor = appColors.colorAccentDark
|
||||
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
recyclerView.clearOnScrollListeners()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val mFab: FloatingActionButton = findViewById(R.id.fab)
|
||||
val mRecyclerView: RecyclerView = findViewById(R.id.activity_sources)
|
||||
val mLayoutManager = LinearLayoutManager(this)
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val api = SelfossApi(this, this@SourcesActivity, prefs.getBoolean("isSelfSignedCert", false))
|
||||
var items: ArrayList<Sources> = ArrayList()
|
||||
val api = SelfossApi(
|
||||
this,
|
||||
this@SourcesActivity,
|
||||
prefs.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1").toLong(),
|
||||
prefs.getBoolean("should_log_everything", false)
|
||||
)
|
||||
var items: ArrayList<Source> = ArrayList()
|
||||
|
||||
mFab.attachToRecyclerView(mRecyclerView)
|
||||
mRecyclerView.setHasFixedSize(true)
|
||||
mRecyclerView.layoutManager = mLayoutManager
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = mLayoutManager
|
||||
|
||||
api.sources.enqueue(object : Callback<List<Sources>> {
|
||||
override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
|
||||
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
||||
items = response.body() as ArrayList<Sources>
|
||||
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)
|
||||
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) {
|
||||
Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
override fun onFailure(call: Call<List<Source>>, t: Throwable) {
|
||||
Toast.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.cant_get_sources,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mFab.setOnClickListener {
|
||||
fab.setOnClickListener {
|
||||
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
@ -2,57 +2,64 @@ package apps.amine.bou.readerforselfoss.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.support.constraint.ConstraintLayout
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.widget.CardView
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.ImageView.ScaleType
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
|
||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||
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.isNetworkAccessible
|
||||
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
|
||||
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.util.ColorGenerator
|
||||
import com.bumptech.glide.Glide
|
||||
import com.like.LikeButton
|
||||
import com.like.OnLikeListener
|
||||
import kotlinx.android.synthetic.main.card_item.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
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.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.*
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
|
||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapFitCenter
|
||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class ItemCardAdapter(private val app: Activity,
|
||||
private val items: ArrayList<Item>,
|
||||
private val api: SelfossApi,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
private val articleViewer: Boolean,
|
||||
private val fullHeightCards: Boolean,
|
||||
private val appColors: AppColors,
|
||||
val debugReadingItems: Boolean,
|
||||
val userIdentifier: String) : RecyclerView.Adapter<ItemCardAdapter.ViewHolder>() {
|
||||
class ItemCardAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<Item>,
|
||||
override val api: SelfossApi,
|
||||
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 debugReadingItems: Boolean,
|
||||
override val userIdentifier: String,
|
||||
override val updateItems: (ArrayList<Item>) -> Unit
|
||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
|
||||
private val c: Context = app.baseContext
|
||||
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 {
|
||||
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as ConstraintLayout
|
||||
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
@ -60,23 +67,28 @@ class ItemCardAdapter(private val app: Activity,
|
||||
val itm = items[position]
|
||||
|
||||
|
||||
holder.saveBtn.isLiked = itm.starred
|
||||
holder.title.text = Html.fromHtml(itm.title)
|
||||
holder.mView.favButton.isLiked = itm.starred
|
||||
holder.mView.title.text = Html.fromHtml(itm.title)
|
||||
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
|
||||
|
||||
holder.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||
holder.mView.title.setLinkTextColor(appColors.colorAccent)
|
||||
|
||||
if (itm.getThumbnail(c).isEmpty()) {
|
||||
Glide.with(c).clear(holder.itemImage)
|
||||
holder.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
if (fullHeightCards) {
|
||||
c.bitmapFitCenter(itm.getThumbnail(c), holder.itemImage)
|
||||
} else {
|
||||
c.bitmapCenterCrop(itm.getThumbnail(c), holder.itemImage)
|
||||
}
|
||||
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||
|
||||
if (!fullHeightCards) {
|
||||
holder.mView.itemImage.maxHeight = imageMaxHeight
|
||||
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
if (itm.getThumbnail(c).isEmpty()) {
|
||||
holder.mView.itemImage.visibility = View.GONE
|
||||
Glide.with(c).clear(holder.mView.itemImage)
|
||||
holder.mView.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
holder.mView.itemImage.visibility = View.VISIBLE
|
||||
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
|
||||
}
|
||||
|
||||
val fHolder = holder
|
||||
if (itm.getIcon(c).isEmpty()) {
|
||||
val color = generator.getColor(itm.sourcetitle)
|
||||
|
||||
@ -84,146 +96,95 @@ class ItemCardAdapter(private val app: Activity,
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.sourcetitle.toTextDrawableString(), color)
|
||||
holder.sourceImage.setImageDrawable(drawable)
|
||||
.build(itm.sourcetitle.toTextDrawableString(c), color)
|
||||
holder.mView.sourceImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.sourceImage)
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
|
||||
}
|
||||
|
||||
holder.saveBtn.isLiked = itm.starred
|
||||
holder.mView.favButton.isLiked = itm.starred
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
private fun doUnmark(i: Item, position: Int) {
|
||||
val s = Snackbar
|
||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo_string) {
|
||||
items.add(position, i)
|
||||
notifyItemInserted(position)
|
||||
|
||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
items.remove(i)
|
||||
notifyItemRemoved(position)
|
||||
doUnmark(i, position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(android.support.design.R.id.snackbar_text)
|
||||
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>) {
|
||||
if (debugReadingItems) {
|
||||
val message =
|
||||
"message: ${response.message()} " +
|
||||
"response isSuccess: ${response.isSuccessful} " +
|
||||
"response code: ${response.code()} " +
|
||||
"response message: ${response.message()} " +
|
||||
"response errorBody: ${response.errorBody()?.string()} " +
|
||||
"body success: ${response.body()?.success} " +
|
||||
"body isSuccess: ${response.body()?.isSuccess}"
|
||||
Crashlytics.setUserIdentifier(userIdentifier)
|
||||
Crashlytics.log(100, "READ_DEBUG_SUCCESS", message)
|
||||
Crashlytics.logException(Exception("Was success, but did it work ?"))
|
||||
|
||||
Toast.makeText(c, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
doUnmark(i, position)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
if (debugReadingItems) {
|
||||
Crashlytics.setUserIdentifier(userIdentifier)
|
||||
Crashlytics.log(100, "READ_DEBUG_ERROR", t.message)
|
||||
Crashlytics.logException(t)
|
||||
Toast.makeText(c, t.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
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) {
|
||||
lateinit var saveBtn: LikeButton
|
||||
lateinit var browserBtn: ImageButton
|
||||
lateinit var shareBtn: ImageButton
|
||||
lateinit var itemImage: ImageView
|
||||
lateinit var sourceImage: ImageView
|
||||
lateinit var title: TextView
|
||||
lateinit var sourceTitleAndDate: TextView
|
||||
|
||||
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
|
||||
init {
|
||||
(mView.findViewById<CardView>(R.id.card)).setCardBackgroundColor(appColors.cardBackground)
|
||||
mView.setCardBackgroundColor(appColors.cardBackgroundColor)
|
||||
handleClickListeners()
|
||||
handleCustomTabActions()
|
||||
}
|
||||
|
||||
private fun handleClickListeners() {
|
||||
sourceImage = mView.findViewById(R.id.sourceImage)
|
||||
itemImage = mView.findViewById(R.id.itemImage)
|
||||
title = mView.findViewById(R.id.title)
|
||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate)
|
||||
saveBtn = mView.findViewById(R.id.favButton)
|
||||
shareBtn = mView.findViewById(R.id.shareBtn)
|
||||
browserBtn = mView.findViewById(R.id.browserBtn)
|
||||
|
||||
if (!fullHeightCards) {
|
||||
itemImage.maxHeight = c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
||||
itemImage.scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
saveBtn.setOnLikeListener(object : OnLikeListener {
|
||||
mView.favButton.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>) {}
|
||||
if (c.isNetworkAccessible(null)) {
|
||||
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 onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
mView.favButton.isLiked = false
|
||||
Toast.makeText(
|
||||
c,
|
||||
R.string.cant_mark_favortie,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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>) {}
|
||||
if (c.isNetworkAccessible(null)) {
|
||||
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()
|
||||
override fun onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
mView.favButton.isLiked = true
|
||||
Toast.makeText(
|
||||
c,
|
||||
R.string.cant_unmark_favortie,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
shareBtn.setOnClickListener {
|
||||
c.shareLink(items[adapterPosition].getLinkDecoded())
|
||||
mView.shareBtn.setOnClickListener {
|
||||
val item = items[adapterPosition]
|
||||
c.shareLink(item.getLinkDecoded(), item.title)
|
||||
}
|
||||
|
||||
browserBtn.setOnClickListener {
|
||||
c.openInBrowser(items[adapterPosition])
|
||||
mView.browserBtn.setOnClickListener {
|
||||
c.openInBrowserAsNewTask(items[adapterPosition])
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,11 +193,15 @@ class ItemCardAdapter(private val app: Activity,
|
||||
helper.bindCustomTabsService(app)
|
||||
|
||||
mView.setOnClickListener {
|
||||
c.openItemUrl(items[adapterPosition].getLinkDecoded(),
|
||||
c.openItemUrl(
|
||||
items,
|
||||
adapterPosition,
|
||||
items[adapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app)
|
||||
app
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,68 @@
|
||||
package apps.amine.bou.readerforselfoss.adapters
|
||||
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.support.constraint.ConstraintLayout
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import com.like.LikeButton
|
||||
import com.like.OnLikeListener
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.utils.*
|
||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
|
||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||
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 com.crashlytics.android.Crashlytics
|
||||
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
|
||||
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.util.ColorGenerator
|
||||
import com.like.LikeButton
|
||||
import com.like.OnLikeListener
|
||||
import kotlinx.android.synthetic.main.list_item.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class ItemListAdapter(private val app: Activity,
|
||||
private val items: ArrayList<Item>,
|
||||
private val api: SelfossApi,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val clickBehavior: Boolean,
|
||||
private val internalBrowser: Boolean,
|
||||
private val articleViewer: Boolean,
|
||||
val debugReadingItems: Boolean,
|
||||
val userIdentifier: String) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {
|
||||
class ItemListAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<Item>,
|
||||
override val api: SelfossApi,
|
||||
override val db: AppDatabase,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
private val articleViewer: Boolean,
|
||||
override val debugReadingItems: Boolean,
|
||||
override val userIdentifier: String,
|
||||
override val appColors: AppColors,
|
||||
override val updateItems: (ArrayList<Item>) -> Unit
|
||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
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 {
|
||||
val v = LayoutInflater.from(c).inflate(R.layout.list_item, parent, false) as ConstraintLayout
|
||||
val v = LayoutInflater.from(c).inflate(
|
||||
R.layout.list_item,
|
||||
parent,
|
||||
false
|
||||
) as ConstraintLayout
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
@ -57,214 +70,74 @@ class ItemListAdapter(private val app: Activity,
|
||||
val itm = items[position]
|
||||
|
||||
|
||||
holder.saveBtn.isLiked = itm.starred
|
||||
holder.title.text = Html.fromHtml(itm.title)
|
||||
holder.mView.title.text = Html.fromHtml(itm.title)
|
||||
|
||||
holder.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
|
||||
|
||||
holder.mView.title.setLinkTextColor(appColors.colorAccent)
|
||||
|
||||
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||
|
||||
if (itm.getThumbnail(c).isEmpty()) {
|
||||
val sizeInInt = 46
|
||||
val sizeInDp = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
|
||||
.displayMetrics).toInt()
|
||||
.displayMetrics
|
||||
).toInt()
|
||||
|
||||
val marginInInt = 16
|
||||
val marginInDp = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
||||
.displayMetrics).toInt()
|
||||
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
||||
.displayMetrics
|
||||
).toInt()
|
||||
|
||||
val params = holder.sourceImage.layoutParams as ViewGroup.MarginLayoutParams
|
||||
val params = holder.mView.itemImage.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.height = sizeInDp
|
||||
params.width = sizeInDp
|
||||
params.setMargins(marginInDp, 0, 0, 0)
|
||||
holder.sourceImage.layoutParams = params
|
||||
holder.mView.itemImage.layoutParams = params
|
||||
|
||||
if (itm.getIcon(c).isEmpty()) {
|
||||
val color = generator.getColor(itm.sourcetitle)
|
||||
val textDrawable = StringBuilder()
|
||||
for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
textDrawable.append(s[0])
|
||||
}
|
||||
|
||||
val builder = TextDrawable.builder().round()
|
||||
val drawable =
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.sourcetitle.toTextDrawableString(c), color)
|
||||
|
||||
val drawable = builder.build(textDrawable.toString(), color)
|
||||
holder.sourceImage.setImageDrawable(drawable)
|
||||
holder.mView.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.sourceImage)
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
|
||||
}
|
||||
} else {
|
||||
c.bitmapCenterCrop(itm.getThumbnail(c), holder.sourceImage)
|
||||
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
|
||||
}
|
||||
|
||||
if (bars[position]) holder.actionBar.visibility = View.VISIBLE else holder.actionBar.visibility = View.GONE
|
||||
|
||||
holder.saveBtn.isLiked = itm.starred
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
|
||||
private fun doUnmark(i: Item, position: Int) {
|
||||
val s = Snackbar
|
||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo_string) {
|
||||
items.add(position, i)
|
||||
notifyItemInserted(position)
|
||||
|
||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
items.remove(i)
|
||||
notifyItemRemoved(position)
|
||||
doUnmark(i, position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(android.support.design.R.id.snackbar_text)
|
||||
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>) {
|
||||
if (debugReadingItems) {
|
||||
val message =
|
||||
"message: ${response.message()} " +
|
||||
"response isSuccess: ${response.isSuccessful} " +
|
||||
"response code: ${response.code()} " +
|
||||
"response message: ${response.message()} " +
|
||||
"response errorBody: ${response.errorBody()?.string()} " +
|
||||
"body success: ${response.body()?.success} " +
|
||||
"body isSuccess: ${response.body()?.isSuccess}"
|
||||
Crashlytics.setUserIdentifier(userIdentifier)
|
||||
Crashlytics.log(100, "READ_DEBUG_SUCCESS", message)
|
||||
Crashlytics.logException(Exception("Was success, but did it work ?"))
|
||||
Toast.makeText(c, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
doUnmark(i, position)
|
||||
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
if (debugReadingItems) {
|
||||
Crashlytics.setUserIdentifier(userIdentifier)
|
||||
Crashlytics.log(100, "READ_DEBUG_ERROR", t.message)
|
||||
Crashlytics.logException(t)
|
||||
Toast.makeText(c, t.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
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) {
|
||||
lateinit var saveBtn: LikeButton
|
||||
lateinit var browserBtn: ImageButton
|
||||
lateinit var shareBtn: ImageButton
|
||||
lateinit var actionBar: RelativeLayout
|
||||
lateinit var sourceImage: ImageView
|
||||
lateinit var title: TextView
|
||||
lateinit var sourceTitleAndDate: TextView
|
||||
|
||||
init {
|
||||
handleClickListeners()
|
||||
handleCustomTabActions()
|
||||
}
|
||||
|
||||
private fun handleClickListeners() {
|
||||
actionBar = mView.findViewById(R.id.actionBar)
|
||||
sourceImage = mView.findViewById(R.id.itemImage)
|
||||
title = mView.findViewById(R.id.title)
|
||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate)
|
||||
saveBtn = mView.findViewById(R.id.favButton)
|
||||
shareBtn = mView.findViewById(R.id.shareBtn)
|
||||
browserBtn = mView.findViewById(R.id.browserBtn)
|
||||
|
||||
|
||||
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 {
|
||||
c.shareLink(items[adapterPosition].getLinkDecoded())
|
||||
}
|
||||
|
||||
browserBtn.setOnClickListener {
|
||||
c.openInBrowser(items[adapterPosition])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleCustomTabActions() {
|
||||
val customTabsIntent = c.buildCustomTabsIntent()
|
||||
helper.bindCustomTabsService(app)
|
||||
|
||||
|
||||
if (!clickBehavior) {
|
||||
mView.setOnClickListener {
|
||||
c.openItemUrl(items[adapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app)
|
||||
}
|
||||
mView.setOnLongClickListener {
|
||||
actionBarShowHide()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
mView.setOnClickListener { actionBarShowHide() }
|
||||
mView.setOnLongClickListener {
|
||||
c.openItemUrl(items[adapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app)
|
||||
true
|
||||
}
|
||||
mView.setOnClickListener {
|
||||
c.openItemUrl(
|
||||
items,
|
||||
adapterPosition,
|
||||
items[adapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionBarShowHide() {
|
||||
bars[adapterPosition] = true
|
||||
if (actionBar.visibility == View.GONE) actionBar.visibility = View.VISIBLE else actionBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,161 @@
|
||||
package apps.amine.bou.readerforselfoss.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
|
||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
|
||||
import apps.amine.bou.readerforselfoss.utils.succeeded
|
||||
import org.acra.ACRA
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
|
||||
abstract var items: ArrayList<Item>
|
||||
abstract val api: SelfossApi
|
||||
abstract val db: AppDatabase
|
||||
abstract val debugReadingItems: Boolean
|
||||
abstract val userIdentifier: String
|
||||
abstract val app: Activity
|
||||
abstract val appColors: AppColors
|
||||
abstract val updateItems: (ArrayList<Item>) -> Unit
|
||||
|
||||
fun updateAllItems(newItems: ArrayList<Item>) {
|
||||
items = newItems
|
||||
notifyDataSetChanged()
|
||||
updateItems(items)
|
||||
}
|
||||
|
||||
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)
|
||||
thread {
|
||||
db.itemsDao().insertAllItems(i.toEntity())
|
||||
}
|
||||
notifyItemInserted(position)
|
||||
updateItems(items)
|
||||
|
||||
if (app.isNetworkAccessible(null)) {
|
||||
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)
|
||||
thread {
|
||||
db.itemsDao().delete(i.toEntity())
|
||||
}
|
||||
notifyItemRemoved(position)
|
||||
updateItems(items)
|
||||
doUnmark(i, position)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().deleteReadActionForArticle(i.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
s.show()
|
||||
}
|
||||
|
||||
fun removeItemAtIndex(position: Int) {
|
||||
val i = items[position]
|
||||
items.remove(i)
|
||||
notifyItemRemoved(position)
|
||||
updateItems(items)
|
||||
|
||||
thread {
|
||||
db.itemsDao().delete(i.toEntity())
|
||||
}
|
||||
|
||||
if (app.isNetworkAccessible(null)) {
|
||||
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (!response.succeeded() && debugReadingItems) {
|
||||
val message =
|
||||
"message: ${response.message()} " +
|
||||
"response isSuccess: ${response.isSuccessful} " +
|
||||
"response code: ${response.code()} " +
|
||||
"response message: ${response.message()} " +
|
||||
"response errorBody: ${response.errorBody()?.string()} " +
|
||||
"body success: ${response.body()?.success} " +
|
||||
"body isSuccess: ${response.body()?.isSuccess}"
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
|
||||
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
doUnmark(i, position)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
if (debugReadingItems) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
|
||||
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
Toast.makeText(
|
||||
app,
|
||||
app.getString(R.string.cant_mark_read),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
items.add(i)
|
||||
notifyItemInserted(position)
|
||||
updateItems(items)
|
||||
|
||||
thread {
|
||||
db.itemsDao().insertAllItems(i.toEntity())
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
|
||||
doUnmark(i, 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,37 +2,40 @@ package apps.amine.bou.readerforselfoss.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.support.constraint.ConstraintLayout
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
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.util.ColorGenerator
|
||||
import kotlinx.android.synthetic.main.source_list_item.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||
|
||||
|
||||
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 generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val v = LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false) as ConstraintLayout
|
||||
val v = LayoutInflater.from(c).inflate(
|
||||
R.layout.source_list_item,
|
||||
parent,
|
||||
false
|
||||
) as ConstraintLayout
|
||||
return ViewHolder(v)
|
||||
}
|
||||
|
||||
@ -46,54 +49,58 @@ class SourcesListAdapter(private val app: Activity,
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.title.toTextDrawableString(), color)
|
||||
holder.sourceImage.setImageDrawable(drawable)
|
||||
.build(itm.title.toTextDrawableString(c), color)
|
||||
holder.mView.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.sourceImage)
|
||||
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
|
||||
}
|
||||
|
||||
holder.sourceTitle.text = itm.title
|
||||
holder.mView.sourceTitle.text = itm.title
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||
lateinit var sourceImage: ImageView
|
||||
lateinit var sourceTitle: TextView
|
||||
|
||||
init {
|
||||
|
||||
handleClickListeners()
|
||||
}
|
||||
|
||||
private fun handleClickListeners() {
|
||||
sourceImage = mView.findViewById(R.id.itemImage)
|
||||
sourceTitle = mView.findViewById(R.id.sourceTitle)
|
||||
|
||||
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
val (id) = items[adapterPosition]
|
||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
items.removeAt(adapterPosition)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||
} else {
|
||||
Toast.makeText(app, R.string.can_delete_source, Toast.LENGTH_SHORT).show()
|
||||
if (c.isNetworkAccessible(null)) {
|
||||
val (id) = items[adapterPosition]
|
||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
items.removeAt(adapterPosition)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
Toast.makeText(app, R.string.can_delete_source, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,24 +7,26 @@ import retrofit2.Call
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
|
||||
|
||||
class MercuryApi(private val key: String) {
|
||||
class MercuryApi(shouldLog: Boolean) {
|
||||
private val service: MercuryService
|
||||
|
||||
init {
|
||||
|
||||
val interceptor = HttpLoggingInterceptor()
|
||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
interceptor.level = if (shouldLog) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
.setLenient()
|
||||
.create()
|
||||
val retrofit =
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl("https://mercury.postlight.com")
|
||||
.baseUrl("https://www.amine-bou.fr")
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
@ -32,6 +34,6 @@ class MercuryApi(private val key: String) {
|
||||
}
|
||||
|
||||
fun parseUrl(url: String): Call<ParsedContent> {
|
||||
return service.parseUrl(url, this.key)
|
||||
return service.parseUrl(url)
|
||||
}
|
||||
}
|
||||
|
@ -2,26 +2,29 @@ package apps.amine.bou.readerforselfoss.api.mercury
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
|
||||
class ParsedContent(val title: String,
|
||||
val content: String,
|
||||
val date_published: String,
|
||||
val lead_image_url: String,
|
||||
val dek: String,
|
||||
val url: String,
|
||||
val domain: String,
|
||||
val excerpt: String,
|
||||
val total_pages: Int,
|
||||
val rendered_pages: Int,
|
||||
val next_page_url: String) : Parcelable {
|
||||
class ParsedContent(
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("content") val content: String?,
|
||||
@SerializedName("date_published") val date_published: String,
|
||||
@SerializedName("lead_image_url") val lead_image_url: String?,
|
||||
@SerializedName("dek") val dek: String,
|
||||
@SerializedName("url") val url: String,
|
||||
@SerializedName("domain") val domain: String,
|
||||
@SerializedName("excerpt") val excerpt: String,
|
||||
@SerializedName("total_pages") val total_pages: Int,
|
||||
@SerializedName("rendered_pages") val rendered_pages: Int,
|
||||
@SerializedName("next_page_url") val next_page_url: String
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
@JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
|
||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
||||
}
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<ParsedContent> =
|
||||
object : Parcelable.Creator<ParsedContent> {
|
||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(source: Parcel) : this(
|
||||
|
@ -1,14 +1,11 @@
|
||||
package apps.amine.bou.readerforselfoss.api.mercury
|
||||
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
|
||||
|
||||
|
||||
interface MercuryService {
|
||||
@GET("parser")
|
||||
fun parseUrl(@Query("url") url: String, @Header("x-api-key") key: String): Call<ParsedContent>
|
||||
@GET("parser.php")
|
||||
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||
|
||||
import java.lang.reflect.Type
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonDeserializer
|
||||
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext
|
||||
): Boolean? =
|
||||
try {
|
||||
json.asInt == 1
|
||||
} catch (e: Exception) {
|
||||
|
@ -2,8 +2,8 @@ package apps.amine.bou.readerforselfoss.api.selfoss
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
||||
import com.burgstaller.okhttp.DispatchingAuthenticator
|
||||
@ -13,16 +13,20 @@ import com.burgstaller.okhttp.digest.Credentials
|
||||
import com.burgstaller.okhttp.digest.DigestAuthenticator
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Call
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||
|
||||
|
||||
// codebeat:disable[ARITY,TOO_MANY_FUNCTIONS]
|
||||
class SelfossApi(c: Context, callingActivity: Activity, isWithSelfSignedCert: Boolean) {
|
||||
class SelfossApi(
|
||||
c: Context,
|
||||
callingActivity: Activity?,
|
||||
isWithSelfSignedCert: Boolean,
|
||||
timeout: Long,
|
||||
shouldLog: Boolean
|
||||
) {
|
||||
|
||||
private lateinit var service: SelfossService
|
||||
private val config: Config = Config(c)
|
||||
@ -36,23 +40,30 @@ class SelfossApi(c: Context, callingActivity: Activity, isWithSelfSignedCert: Bo
|
||||
this
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
|
||||
if (timeout != -1L) {
|
||||
this.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun Credentials.createAuthenticator(): DispatchingAuthenticator =
|
||||
DispatchingAuthenticator.Builder()
|
||||
.with("digest", DigestAuthenticator(this))
|
||||
.with("basic", BasicAuthenticator(this))
|
||||
.build()
|
||||
|
||||
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean): OkHttpClient {
|
||||
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
|
||||
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
||||
return OkHttpClient
|
||||
.Builder()
|
||||
.maybeWithSettingsTimeout(timeout)
|
||||
.maybeWithSelfSigned(isWithSelfSignedCert)
|
||||
.authenticator(CachingAuthenticatorDecorator(this, authCache))
|
||||
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
userName = config.userLogin
|
||||
password = config.userPassword
|
||||
@ -66,38 +77,80 @@ class SelfossApi(c: Context, callingActivity: Activity, isWithSelfSignedCert: Bo
|
||||
val gson =
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
||||
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
val logging = HttpLoggingInterceptor()
|
||||
|
||||
logging.level = if (shouldLog) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
|
||||
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
|
||||
|
||||
httpClient.addInterceptor(logging)
|
||||
|
||||
try {
|
||||
val retrofit =
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl(config.baseUrl)
|
||||
.client(authenticator.getHttpClien(isWithSelfSignedCert))
|
||||
.client(httpClient.build())
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
service = retrofit.create(SelfossService::class.java)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
|
||||
if (callingActivity != null) {
|
||||
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(): Call<SuccessResponse> =
|
||||
service.loginToSelfoss(config.userLogin, config.userPassword)
|
||||
|
||||
fun readItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
||||
getItems("read", tag, sourceId, search, 200)
|
||||
fun readItems(
|
||||
tag: String?,
|
||||
sourceId: Long?,
|
||||
search: String?,
|
||||
itemsNumber: Int,
|
||||
offset: Int
|
||||
): Call<List<Item>> =
|
||||
getItems("read", tag, sourceId, search, itemsNumber, offset)
|
||||
|
||||
fun newItems(tag: String?, sourceId: Long?, search: String?, itemsNumber: Int): Call<List<Item>> =
|
||||
getItems("unread", tag, sourceId, search, itemsNumber)
|
||||
fun newItems(
|
||||
tag: String?,
|
||||
sourceId: Long?,
|
||||
search: String?,
|
||||
itemsNumber: Int,
|
||||
offset: Int
|
||||
): Call<List<Item>> =
|
||||
getItems("unread", tag, sourceId, search, itemsNumber, offset)
|
||||
|
||||
fun starredItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
|
||||
getItems("starred", tag, sourceId, search, 200)
|
||||
fun starredItems(
|
||||
tag: String?,
|
||||
sourceId: Long?,
|
||||
search: String?,
|
||||
itemsNumber: Int,
|
||||
offset: Int
|
||||
): Call<List<Item>> =
|
||||
getItems("starred", tag, sourceId, search, itemsNumber, offset)
|
||||
|
||||
private fun getItems(type: String, tag: String?, sourceId: Long?, search: String?, items: Int): Call<List<Item>> =
|
||||
service.getItems(type, tag, sourceId, search, userName, password, items)
|
||||
fun allItems(): Call<List<Item>> =
|
||||
service.allItems(userName, password)
|
||||
|
||||
private fun getItems(
|
||||
type: String,
|
||||
tag: String?,
|
||||
sourceId: Long?,
|
||||
search: String?,
|
||||
items: Int,
|
||||
offset: Int
|
||||
): Call<List<Item>> =
|
||||
service.getItems(type, tag, sourceId, search, userName, password, items, offset)
|
||||
|
||||
fun markItem(itemId: String): Call<SuccessResponse> =
|
||||
service.markAsRead(itemId, userName, password)
|
||||
@ -123,7 +176,7 @@ class SelfossApi(c: Context, callingActivity: Activity, isWithSelfSignedCert: Bo
|
||||
fun update(): Call<String> =
|
||||
service.update(userName, password)
|
||||
|
||||
val sources: Call<List<Sources>>
|
||||
val sources: Call<List<Source>>
|
||||
get() = service.sources(userName, password)
|
||||
|
||||
fun deleteSource(id: String): Call<SuccessResponse> =
|
||||
@ -132,9 +185,12 @@ class SelfossApi(c: Context, callingActivity: Activity, isWithSelfSignedCert: Bo
|
||||
fun spouts(): Call<Map<String, Spout>> =
|
||||
service.spouts(userName, password)
|
||||
|
||||
fun createSource(title: String, url: String, spout: String, tags: String, filter: String): Call<SuccessResponse> =
|
||||
fun createSource(
|
||||
title: String,
|
||||
url: String,
|
||||
spout: String,
|
||||
tags: String,
|
||||
filter: String
|
||||
): Call<SuccessResponse> =
|
||||
service.createSource(title, url, spout, tags, filter, userName, password)
|
||||
|
||||
}
|
||||
|
||||
// codebeat:enable[ARITY,TOO_MANY_FUNCTIONS]
|
@ -7,55 +7,72 @@ import android.os.Parcelable
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
||||
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 {
|
||||
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
||||
baseUriBuilder.appendPath(path).appendPath(file)
|
||||
|
||||
return if (file.isEmptyOrNullOrNullString()) ""
|
||||
else baseUriBuilder.toString()
|
||||
baseUriBuilder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class Tag(
|
||||
@SerializedName("tag") val tag: String,
|
||||
@SerializedName("color") val color: String,
|
||||
@SerializedName("unread") val unread: Int
|
||||
)
|
||||
|
||||
data class Tag(val tag: String, val color: String, val unread: Int)
|
||||
|
||||
class SuccessResponse(val success: Boolean) {
|
||||
class SuccessResponse(@SerializedName("success") val success: Boolean) {
|
||||
val isSuccess: Boolean
|
||||
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,
|
||||
val title: String,
|
||||
val tags: String,
|
||||
val spout: String,
|
||||
val error: String,
|
||||
val icon: String) {
|
||||
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
|
||||
|
||||
fun getIcon(app: Context): String {
|
||||
if (config == null) {
|
||||
config = Config(app)
|
||||
}
|
||||
return constructUrl(config,"favicons", icon)
|
||||
return constructUrl(config, "favicons", icon)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Item(val id: String,
|
||||
val datetime: String,
|
||||
val title: String,
|
||||
val unread: Boolean,
|
||||
val starred: Boolean,
|
||||
val thumbnail: String,
|
||||
val icon: String,
|
||||
val link: String,
|
||||
val sourcetitle: String) : Parcelable {
|
||||
data class Item(
|
||||
@SerializedName("id") val id: String,
|
||||
@SerializedName("datetime") val datetime: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("unread") val unread: Boolean,
|
||||
@SerializedName("starred") var starred: Boolean,
|
||||
@SerializedName("thumbnail") val thumbnail: String,
|
||||
@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
|
||||
|
||||
@ -70,12 +87,14 @@ data class Item(val id: String,
|
||||
id = source.readString(),
|
||||
datetime = source.readString(),
|
||||
title = source.readString(),
|
||||
content = source.readString(),
|
||||
unread = 0.toByte() != source.readByte(),
|
||||
starred = 0.toByte() != source.readByte(),
|
||||
thumbnail = source.readString(),
|
||||
icon = source.readString(),
|
||||
link = source.readString(),
|
||||
sourcetitle = source.readString()
|
||||
sourcetitle = source.readString(),
|
||||
tags = source.readParcelable(ClassLoader.getSystemClassLoader())
|
||||
)
|
||||
|
||||
override fun describeContents() = 0
|
||||
@ -84,12 +103,14 @@ data class Item(val id: String,
|
||||
dest.writeString(id)
|
||||
dest.writeString(datetime)
|
||||
dest.writeString(title)
|
||||
dest.writeString(content)
|
||||
dest.writeByte((if (unread) 1 else 0))
|
||||
dest.writeByte((if (starred) 1 else 0))
|
||||
dest.writeString(thumbnail)
|
||||
dest.writeString(icon)
|
||||
dest.writeString(link)
|
||||
dest.writeString(sourcetitle)
|
||||
dest.writeParcelable(tags, flags)
|
||||
}
|
||||
|
||||
fun getIcon(app: Context): String {
|
||||
@ -109,21 +130,50 @@ data class Item(val id: String,
|
||||
// TODO: maybe find a better way to handle these kind of urls
|
||||
fun getLinkDecoded(): String {
|
||||
var stringUrl: String
|
||||
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
||||
if (link.contains("&url=")) {
|
||||
stringUrl = link.substringAfter("&url=")
|
||||
} else {
|
||||
stringUrl = this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
stringUrl = this.link.replace("&", "&")
|
||||
}
|
||||
stringUrl =
|
||||
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
||||
if (link.contains("&url=")) {
|
||||
link.substringAfter("&url=")
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
|
||||
// handle :443 => https
|
||||
if (stringUrl.contains(":443")) {
|
||||
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
||||
}
|
||||
|
||||
// handle url not starting with http
|
||||
if (stringUrl.startsWith("//")) {
|
||||
stringUrl = "http:$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()
|
||||
)
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeString(tags)
|
||||
}
|
||||
}
|
@ -5,92 +5,120 @@ import retrofit2.http.DELETE
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
internal interface SelfossService {
|
||||
|
||||
@GET("login")
|
||||
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
||||
|
||||
@GET("items")
|
||||
fun getItems(@Query("type") type: String,
|
||||
@Query("tag") tag: String?,
|
||||
@Query("source") source: Long?,
|
||||
@Query("search") search: String?,
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String,
|
||||
@Query("items") items: Int): Call<List<Item>>
|
||||
fun getItems(
|
||||
@Query("type") type: String,
|
||||
@Query("tag") tag: String?,
|
||||
@Query("source") source: Long?,
|
||||
@Query("search") search: String?,
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String,
|
||||
@Query("items") items: Int,
|
||||
@Query("offset") offset: Int
|
||||
): Call<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}")
|
||||
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}")
|
||||
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
|
||||
@POST("mark")
|
||||
fun markAllAsRead(@Field("ids[]") ids: List<String>,
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String): Call<SuccessResponse>
|
||||
|
||||
fun markAllAsRead(
|
||||
@Field("ids[]") ids: List<String>,
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<SuccessResponse>
|
||||
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@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}")
|
||||
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")
|
||||
fun stats(@Query("username") username: String,
|
||||
@Query("password") password: String): Call<Stats>
|
||||
|
||||
fun stats(
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<Stats>
|
||||
|
||||
@GET("tags")
|
||||
fun tags(@Query("username") username: String,
|
||||
@Query("password") password: String): Call<List<Tag>>
|
||||
|
||||
fun tags(
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<List<Tag>>
|
||||
|
||||
@GET("update")
|
||||
fun update(@Query("username") username: String,
|
||||
@Query("password") password: String): Call<String>
|
||||
fun update(
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<String>
|
||||
|
||||
@GET("sources/spouts")
|
||||
fun spouts(@Query("username") username: String,
|
||||
@Query("password") password: String): Call<Map<String, Spout>>
|
||||
fun spouts(
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<Map<String, Spout>>
|
||||
|
||||
@GET("sources/list")
|
||||
fun sources(@Query("username") username: String,
|
||||
@Query("password") password: String): Call<List<Sources>>
|
||||
|
||||
fun sources(
|
||||
@Query("username") username: String,
|
||||
@Query("password") password: String
|
||||
): Call<List<Source>>
|
||||
|
||||
@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
|
||||
@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>
|
||||
}
|
||||
// codebeat:disable[ARITY]
|
@ -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,152 @@
|
||||
package apps.amine.bou.readerforselfoss.background
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.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.Item
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||
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.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
|
||||
import org.acra.ACRA
|
||||
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 {
|
||||
if (context.isNetworkAccessible(null)) {
|
||||
|
||||
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_cloud_download)
|
||||
|
||||
notificationManager.notify(1, notification.build())
|
||||
|
||||
val settings =
|
||||
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
|
||||
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).build()
|
||||
|
||||
val api = SelfossApi(
|
||||
this.context,
|
||||
null,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
sharedPref.getString("api_timeout", "-1").toLong(),
|
||||
sharedPref.getBoolean("should_log_everything", false)
|
||||
)
|
||||
|
||||
api.allItems().enqueue(object : Callback<List<Item>> {
|
||||
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.cancel(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(
|
||||
call: Call<List<Item>>,
|
||||
response: Response<List<Item>>
|
||||
) {
|
||||
thread {
|
||||
if (response.body() != null) {
|
||||
val apiItems = (response.body() as ArrayList<Item>)
|
||||
db.itemsDao().deleteAllItems()
|
||||
db.itemsDao()
|
||||
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
|
||||
|
||||
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 pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||
|
||||
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_fiber_new_black_24dp)
|
||||
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.notify(2, newItemsNotification.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.cancel(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
thread {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.SUCCESS
|
||||
}
|
||||
|
||||
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) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(t, context)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,464 @@
|
||||
package apps.amine.bou.readerforselfoss.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
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 android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebSettings
|
||||
import androidx.room.Room
|
||||
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.api.selfoss.SuccessResponse
|
||||
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.themes.AppColors
|
||||
import apps.amine.bou.readerforselfoss.utils.Config
|
||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
|
||||
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.succeeded
|
||||
import apps.amine.bou.readerforselfoss.utils.toPx
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||
import kotlinx.android.synthetic.main.fragment_article.view.*
|
||||
import org.acra.ACRA
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ArticleFragment : Fragment() {
|
||||
private lateinit var pageNumber: Number
|
||||
private var fontSize: Int = 14
|
||||
private lateinit var allItems: ArrayList<Item>
|
||||
private lateinit var mCustomTabActivityHelper: CustomTabActivityHelper
|
||||
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 editor: SharedPreferences.Editor
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var appColors: AppColors
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
mCustomTabActivityHelper.unbindCustomTabsService(activity)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(activity!!)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
pageNumber = arguments!!.getInt(ARG_POSITION)
|
||||
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
|
||||
|
||||
db = Room.databaseBuilder(
|
||||
context!!,
|
||||
AppDatabase::class.java, "selfoss-database"
|
||||
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
|
||||
}
|
||||
|
||||
private lateinit var rootView: ViewGroup
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
rootView = inflater
|
||||
.inflate(R.layout.fragment_article, container, false) as ViewGroup
|
||||
|
||||
url = allItems[pageNumber.toInt()].getLinkDecoded()
|
||||
contentText = allItems[pageNumber.toInt()].content
|
||||
contentTitle = allItems[pageNumber.toInt()].title
|
||||
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
|
||||
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
editor = prefs.edit()
|
||||
fontSize = prefs.getString("reader_font_size", "14").toInt()
|
||||
|
||||
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
val debugReadingItems = prefs.getBoolean("read_debug", false)
|
||||
|
||||
val api = SelfossApi(
|
||||
context!!,
|
||||
activity!!,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1").toLong(),
|
||||
prefs.getBoolean("should_log_everything", false)
|
||||
)
|
||||
|
||||
fab = rootView.fab
|
||||
|
||||
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||
|
||||
fab.rippleColor = appColors.colorAccentDark
|
||||
|
||||
val floatingToolbar: FloatingToolbar = rootView.floatingToolbar
|
||||
floatingToolbar.attachFab(fab)
|
||||
|
||||
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
|
||||
|
||||
val customTabsIntent = activity!!.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 -> activity!!.shareLink(url, contentTitle)
|
||||
R.id.open_action -> activity!!.openItemUrl(
|
||||
allItems,
|
||||
pageNumber.toInt(),
|
||||
url,
|
||||
customTabsIntent,
|
||||
false,
|
||||
false,
|
||||
activity!!
|
||||
)
|
||||
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
|
||||
api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue(
|
||||
object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (!response.succeeded() && debugReadingItems) {
|
||||
val message =
|
||||
"message: ${response.message()} " +
|
||||
"response isSuccess: ${response.isSuccessful} " +
|
||||
"response code: ${response.code()} " +
|
||||
"response message: ${response.message()} " +
|
||||
"response errorBody: ${response.errorBody()?.string()} " +
|
||||
"body success: ${response.body()?.success} " +
|
||||
"body isSuccess: ${response.body()?.isSuccess}"
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), activity!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<SuccessResponse>,
|
||||
t: Throwable
|
||||
) {
|
||||
if (debugReadingItems) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
thread {
|
||||
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MenuItem?) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
rootView.source.text = contentSource
|
||||
|
||||
if (contentText.isEmptyOrNullOrNullString()) {
|
||||
getContentFromMercury(customTabsIntent, prefs)
|
||||
} else {
|
||||
rootView.titleView.text = contentTitle
|
||||
|
||||
htmlToWebview(contentText, prefs)
|
||||
|
||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||
rootView.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(context!!)
|
||||
.asBitmap()
|
||||
.load(contentImage)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(rootView.imageView)
|
||||
} else {
|
||||
rootView.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
rootView.nestedScrollView.setOnScrollChangeListener(
|
||||
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
if (scrollY > oldScrollY) {
|
||||
fab.hide()
|
||||
} else {
|
||||
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
private fun getContentFromMercury(
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
prefs: SharedPreferences
|
||||
) {
|
||||
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
|
||||
rootView.progressBar.visibility = View.VISIBLE
|
||||
val parser = MercuryApi(
|
||||
prefs.getBoolean("should_log_everything", false)
|
||||
)
|
||||
|
||||
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 {
|
||||
rootView.titleView.text = response.body()!!.title
|
||||
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) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
htmlToWebview(response.body()!!.content.orEmpty(), prefs)
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
|
||||
rootView.imageView.visibility = View.VISIBLE
|
||||
try {
|
||||
Glide
|
||||
.with(context!!)
|
||||
.asBitmap()
|
||||
.load(response.body()!!.lead_image_url)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(rootView.imageView)
|
||||
} catch (e: Exception) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
} else {
|
||||
rootView.imageView.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
rootView.nestedScrollView.scrollTo(0, 0)
|
||||
|
||||
rootView.progressBar.visibility = View.GONE
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
openInBrowserAfterFailing(customTabsIntent)
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<ParsedContent>,
|
||||
t: Throwable
|
||||
) = openInBrowserAfterFailing(customTabsIntent)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun htmlToWebview(c: String, prefs: SharedPreferences) {
|
||||
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
|
||||
|
||||
rootView.webcontent.visibility = View.VISIBLE
|
||||
val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
|
||||
if (context != null) {
|
||||
rootView.webcontent.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context!!,
|
||||
R.color.dark_webview
|
||||
)
|
||||
)
|
||||
Pair(ContextCompat.getColor(context!!, R.color.dark_webview_text), ContextCompat.getColor(context!!, R.color.light_webview_text))
|
||||
} else {
|
||||
Pair(null, null)
|
||||
}
|
||||
} else {
|
||||
if (context != null) {
|
||||
rootView.webcontent.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
context!!,
|
||||
R.color.light_webview
|
||||
)
|
||||
)
|
||||
Pair(ContextCompat.getColor(context!!, R.color.light_webview_text), ContextCompat.getColor(context!!, R.color.dark_webview_text))
|
||||
} 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"
|
||||
}
|
||||
|
||||
rootView.webcontent.settings.useWideViewPort = true
|
||||
rootView.webcontent.settings.loadWithOverviewMode = true
|
||||
rootView.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
rootView.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
} else {
|
||||
rootView.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
|
||||
}
|
||||
|
||||
var baseUrl: String? = null
|
||||
|
||||
try {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
|
||||
}
|
||||
|
||||
rootView.webcontent.loadDataWithBaseURL(
|
||||
baseUrl,
|
||||
"""<html>
|
||||
|<head>
|
||||
| <style>
|
||||
| img {
|
||||
| display: inline-block;
|
||||
| height: auto;
|
||||
| width: 100%;
|
||||
| max-width: 100%;
|
||||
| }
|
||||
| a {
|
||||
| color: $stringColor !important;
|
||||
| }
|
||||
| *:not(a) {
|
||||
| color: $stringTextColor;
|
||||
| }
|
||||
| * {
|
||||
| font-size: ${fontSize.toPx}px;
|
||||
| text-align: justify;
|
||||
| word-break: break-word;
|
||||
| overflow:hidden;
|
||||
| }
|
||||
| a, pre, code {
|
||||
| text-align: left;
|
||||
| }
|
||||
| pre, code {
|
||||
| white-space: pre-wrap;
|
||||
| width:100%;
|
||||
| background-color: $stringBackgroundColor;
|
||||
| }
|
||||
| </style>
|
||||
|</head>
|
||||
|<body>
|
||||
| $c
|
||||
|</body>""".trimMargin(),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
|
||||
rootView.progressBar.visibility = View.GONE
|
||||
activity!!.openItemUrl(
|
||||
allItems,
|
||||
pageNumber.toInt(),
|
||||
url,
|
||||
customTabsIntent,
|
||||
true,
|
||||
false,
|
||||
activity!!
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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")
|
||||
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")
|
||||
fun items(): List<ItemEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAllItems(vararg items: ItemEntity)
|
||||
|
||||
@Query("DELETE FROM items")
|
||||
fun deleteAllItems()
|
||||
|
||||
@Delete
|
||||
fun delete(item: ItemEntity)
|
||||
|
||||
@Update
|
||||
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 = 3)
|
||||
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,16 @@
|
||||
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`))")
|
||||
}
|
||||
}
|
@ -1,24 +1,28 @@
|
||||
package apps.amine.bou.readerforselfoss.settings;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
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.design.widget.AppBarLayout;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R;
|
||||
import com.ftinc.scoop.Scoop;
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R;
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors;
|
||||
import apps.amine.bou.readerforselfoss.themes.Toppings;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link PreferenceActivity} which implements and proxies the necessary calls
|
||||
@ -30,9 +34,10 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
new AppColors(this);
|
||||
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
Scoop.getInstance().apply(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@ -40,9 +45,16 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
LinearLayout root = (LinearLayout)findViewById(android.R.id.list).getParent().getParent().getParent();
|
||||
LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
|
||||
AppBarLayout bar = (AppBarLayout) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
|
||||
Toolbar toolbar = bar.findViewById(R.id.toolbar);
|
||||
|
||||
Scoop scoop = Scoop.getInstance();
|
||||
scoop.bind(this, Toppings.PRIMARY.getValue(), toolbar);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.getValue());
|
||||
}
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
@ -116,6 +128,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
@ -16,20 +16,24 @@ import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceChangeListener;
|
||||
import android.preference.Preference.OnPreferenceClickListener;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.SwitchPreference;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R;
|
||||
import apps.amine.bou.readerforselfoss.themes.AppColors;
|
||||
import apps.amine.bou.readerforselfoss.utils.Config;
|
||||
import com.ftinc.scoop.ui.ScoopSettingsActivity;
|
||||
|
||||
|
||||
/**
|
||||
@ -89,6 +93,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
new AppColors(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setupActionBar();
|
||||
}
|
||||
@ -125,11 +130,16 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
* This method stops fragment injection in malicious applications.
|
||||
* Make sure to deny any unknown fragments here.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isValidFragment(String fragmentName) {
|
||||
return PreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| OfflinePreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| DebugPreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| LinksPreferenceFragment.class.getName().equals(fragmentName);
|
||||
|| LinksPreferenceFragment.class.getName().equals(fragmentName)
|
||||
|| ThemePreferenceFragment.class.getName().equals(fragmentName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,32 +154,74 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
|
||||
itemsNumber.getEditText().setFilters(new InputFilter[]{
|
||||
new InputFilter (){
|
||||
new InputFilter() {
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
try {
|
||||
int input = Integer.parseInt(dest.toString() + source.toString());
|
||||
if (input <= 200 && input >0)
|
||||
return null;
|
||||
} catch (NumberFormatException nfe) { }
|
||||
return "";
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
try {
|
||||
int input = Integer.parseInt(dest.toString() + source.toString());
|
||||
if (input <= 200 && input > 0)
|
||||
return null;
|
||||
} catch (NumberFormatException nfe) {
|
||||
Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
getActivity().finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public static class ArticleViewerPreferenceFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.pref_viewer);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
final EditTextPreference fontSize = (EditTextPreference) findPreference("reader_font_size");
|
||||
fontSize.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
try {
|
||||
fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
|
||||
} catch (NumberFormatException e) {}
|
||||
}
|
||||
});
|
||||
fontSize.getEditText().setFilters(new InputFilter[]{
|
||||
new InputFilter() {
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
try {
|
||||
int input = Integer.parseInt(dest.toString() + source.toString());
|
||||
if (input > 0)
|
||||
return null;
|
||||
} catch (NumberFormatException nfe) {}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -191,7 +243,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
addPreferencesFromResource(R.xml.pref_debug);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
SharedPreferences pref = getActivity().getSharedPreferences(Config.Companion.getSettingsName(), Context.MODE_PRIVATE);
|
||||
SharedPreferences pref = getActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE);
|
||||
final String id = pref.getString("unique_id", "...");
|
||||
|
||||
final Preference identifier = findPreference("debug_identifier");
|
||||
@ -201,11 +253,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
identifier.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
ClipData clip = ClipData.newPlainText("Selfoss unique id", id);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
if (clipboard != null) {
|
||||
ClipData clip = ClipData.newPlainText("Selfoss unique id", id);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
|
||||
Toast.makeText(getActivity(), R.string.unique_id_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
Toast.makeText(getActivity(), R.string.unique_id_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
identifier.setTitle(id);
|
||||
@ -239,10 +294,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
addPreferencesFromResource(R.xml.pref_links);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
findPreference( "trackerLink" ).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
openUrl(Uri.parse(getString(R.string.tracker_url)));
|
||||
openUrl(Uri.parse(Config.trackerUrl));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -250,7 +305,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
openUrl(Uri.parse(getString(R.string.source_url)));
|
||||
openUrl(Uri.parse(Config.sourceUrl));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
openUrl(Uri.parse(Config.translationUrl));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -267,17 +330,82 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaderClick(Header header, int position) {
|
||||
super.onHeaderClick(header, position);
|
||||
if (header.id == R.id.theme_change) {
|
||||
Intent intent = ScoopSettingsActivity.createIntent(getApplicationContext());
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getApplicationContext().startActivity(intent);
|
||||
finish();
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public static class ThemePreferenceFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.pref_theme);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
getActivity().finish();
|
||||
return true;
|
||||
} else if (id == R.id.clear) {
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
SharedPreferences.Editor 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();
|
||||
getActivity().finish();
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.settings_theme, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public static class OfflinePreferenceFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.pref_offline);
|
||||
setHasOptionsMenu(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);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public static class ExperimentalPreferenceFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.pref_experimental);
|
||||
setHasOptionsMenu(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
@ -2,52 +2,68 @@ package apps.amine.bou.readerforselfoss.themes
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.support.annotation.ColorInt
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import android.util.TypedValue
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import java.lang.reflect.AccessibleObject.setAccessible
|
||||
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
|
||||
class AppColors(a: Activity) {
|
||||
@ColorInt val accent: Int
|
||||
@ColorInt val dark: Int
|
||||
@ColorInt val primary: Int
|
||||
@ColorInt val cardBackground: Int
|
||||
@ColorInt val windowBackground: Int
|
||||
|
||||
@ColorInt val colorPrimary: Int
|
||||
@ColorInt val colorPrimaryDark: Int
|
||||
@ColorInt val colorAccent: Int
|
||||
@ColorInt val colorAccentDark: Int
|
||||
@ColorInt val cardBackgroundColor: Int
|
||||
@ColorInt val colorBackground: 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
|
||||
}
|
||||
|
||||
val wrapper = Context::class.java
|
||||
val method = wrapper!!.getMethod("getThemeResId")
|
||||
method.isAccessible = true
|
||||
|
||||
isDarkTheme = when(method.invoke(a.baseContext)) {
|
||||
R.style.NoBarTealOrangeDark,
|
||||
R.style.NoBarDark,
|
||||
R.style.NoBarBlueAmberDark,
|
||||
R.style.NoBarGreyOrangeDark,
|
||||
R.style.NoBarIndigoPinkDark,
|
||||
R.style.NoBarRedTealDark,
|
||||
R.style.NoBarCyanPinkDark -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val typedAccent = TypedValue()
|
||||
val typedAccentDark = TypedValue()
|
||||
val typedPrimary = TypedValue()
|
||||
val typedCardBackground = TypedValue()
|
||||
val typedWindowBackground = TypedValue()
|
||||
|
||||
a.theme.resolveAttribute(R.attr.colorAccent, typedAccent, true)
|
||||
a.theme.resolveAttribute(R.attr.colorAccent, typedAccent, true)
|
||||
a.theme.resolveAttribute(R.attr.colorPrimary, typedPrimary, true)
|
||||
a.theme.resolveAttribute(R.attr.cardBackgroundColor, typedCardBackground, true)
|
||||
a.theme.resolveAttribute(android.R.attr.colorBackground, typedWindowBackground, true)
|
||||
accent = typedAccent.data
|
||||
dark = typedAccentDark.data
|
||||
primary = typedPrimary.data
|
||||
cardBackground = typedCardBackground.data
|
||||
windowBackground = typedWindowBackground.data
|
||||
|
||||
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,16 @@
|
||||
package apps.amine.bou.readerforselfoss.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import org.acra.ErrorReporter
|
||||
|
||||
fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) {
|
||||
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
if (sharedPref.getBoolean("acra_should_log", false)) {
|
||||
this.handleSilentException(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
fun ErrorReporter.doHandleSilentException(throwable: Throwable) {
|
||||
this.handleSilentException(throwable)
|
||||
}
|
@ -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,125 +2,40 @@ package apps.amine.bou.readerforselfoss.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.text.TextUtils
|
||||
import android.util.Patterns
|
||||
|
||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
import apps.amine.bou.readerforselfoss.BuildConfig
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
|
||||
|
||||
fun Context.checkAndDisplayStoreApk() = {
|
||||
fun isStoreVersion(): Boolean =
|
||||
try {
|
||||
val installer = this.packageManager
|
||||
.getInstallerPackageName(this.packageName)
|
||||
!TextUtils.isEmpty(installer)
|
||||
} catch (e: Throwable) {
|
||||
false
|
||||
}
|
||||
|
||||
if (!isStoreVersion() && !BuildConfig.GITHUB_VERSION) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.warning_version))
|
||||
alertDialog.setMessage(getString(R.string.text_version))
|
||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
||||
{ dialog, _ -> dialog.dismiss() })
|
||||
alertDialog.show()
|
||||
} else Unit
|
||||
}
|
||||
|
||||
fun String.isUrlValid(): Boolean =
|
||||
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
|
||||
fun String.isBaseUrlValid(): Boolean {
|
||||
val baseUrl = HttpUrl.parse(this)
|
||||
var existsAndEndsWithSlash = false
|
||||
if (baseUrl != null) {
|
||||
val pathSegments = baseUrl.pathSegments()
|
||||
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
||||
}
|
||||
|
||||
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
|
||||
}
|
||||
|
||||
fun String?.isEmptyOrNullOrNullString(): Boolean =
|
||||
this == null || this == "null" || this.isEmpty()
|
||||
|
||||
fun Context.checkApkVersion(settings: SharedPreferences,
|
||||
editor: SharedPreferences.Editor,
|
||||
mFirebaseRemoteConfig: FirebaseRemoteConfig) = {
|
||||
fun isThereAnUpdate() {
|
||||
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(this).create()
|
||||
alertDialog.setTitle(getString(R.string.new_apk_available_title))
|
||||
alertDialog.setMessage(getString(R.string.new_apk_available_message))
|
||||
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.new_apk_available_get)) { _, _ ->
|
||||
editor.putString(APK_LINK, apkLink)
|
||||
editor.apply()
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink))
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.new_apk_available_no),
|
||||
{ dialog, _ ->
|
||||
editor.putString(APK_LINK, apkLink)
|
||||
editor.apply()
|
||||
dialog.dismiss()
|
||||
})
|
||||
alertDialog.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mFirebaseRemoteConfig.fetch(43200)
|
||||
.addOnCompleteListener { task ->
|
||||
if (task.isSuccessful) {
|
||||
mFirebaseRemoteConfig.activateFetched()
|
||||
}
|
||||
|
||||
isThereAnUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.longHash(): Long {
|
||||
var h = 98764321261L
|
||||
val l = this.length
|
||||
val chars = this.toCharArray()
|
||||
|
||||
for (i in 0..l - 1) {
|
||||
for (i in 0 until l) {
|
||||
h = 31 * h + chars[i].toLong()
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
fun String.toStringUriWithHttp() =
|
||||
if (!this.startsWith("https://") && !this.startsWith("http://"))
|
||||
fun String.toStringUriWithHttp(): String =
|
||||
if (!this.startsWith("https://") && !this.startsWith("http://")) {
|
||||
"http://" + this
|
||||
else
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun Context.shareLink(itemUrl: String) {
|
||||
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))
|
||||
}
|
||||
|
||||
fun Context.openInBrowser(i: Item) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
|
||||
startActivity(intent)
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
getString(R.string.share)
|
||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
@ -6,7 +6,6 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import apps.amine.bou.readerforselfoss.LoginActivity
|
||||
|
||||
|
||||
class Config(c: Context) {
|
||||
|
||||
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
||||
@ -26,21 +25,35 @@ class Config(c: Context) {
|
||||
val httpUserPassword: String
|
||||
get() = settings.getString("httpPassword", "")
|
||||
|
||||
|
||||
companion object {
|
||||
val settingsName = "paramsselfoss"
|
||||
const val settingsName = "paramsselfoss"
|
||||
|
||||
fun logoutAndRedirect(c: Context,
|
||||
callingActivity: Activity,
|
||||
editor: SharedPreferences.Editor,
|
||||
baseUrlFail: Boolean = false): Boolean {
|
||||
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"
|
||||
|
||||
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)
|
||||
if (baseUrlFail) {
|
||||
intent.putExtra("baseUrlFail", baseUrlFail)
|
||||
}
|
||||
c.startActivity(intent)
|
||||
callingActivity.finish()
|
||||
return true
|
||||
|
@ -7,7 +7,7 @@ import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
fun getUnsafeHttpClient() =
|
||||
fun getUnsafeHttpClient(): OkHttpClient.Builder =
|
||||
try {
|
||||
// Create a trust manager that does not validate certificate chains
|
||||
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
||||
@ -15,13 +15,18 @@ fun getUnsafeHttpClient() =
|
||||
arrayOf()
|
||||
|
||||
@Throws(CertificateException::class)
|
||||
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>, authType: String) {
|
||||
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) {
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<java.security.cert.X509Certificate>,
|
||||
authType: String
|
||||
) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
@ -33,7 +38,6 @@ fun getUnsafeHttpClient() =
|
||||
OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||
.hostnameVerifier { _, _ -> true }
|
||||
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
@ -1,22 +1,28 @@
|
||||
package apps.amine.bou.readerforselfoss.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
|
||||
import org.acra.ACRA
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
fun String.toTextDrawableString(): String {
|
||||
fun String.toTextDrawableString(c: Context): String {
|
||||
val textDrawable = StringBuilder()
|
||||
for (s in this.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
textDrawable.append(s[0])
|
||||
for (s in this.split(" ".toRegex()).filter { !it.isEmpty() }.toTypedArray()) {
|
||||
try {
|
||||
textDrawable.append(s[0])
|
||||
} catch (e: StringIndexOutOfBoundsException) {
|
||||
ACRA.getErrorReporter().maybeHandleSilentException(e, c)
|
||||
}
|
||||
}
|
||||
return textDrawable.toString()
|
||||
}
|
||||
|
||||
fun Item.sourceAndDateText(): String {
|
||||
var formattedDate: String = try {
|
||||
val formattedDate: String = try {
|
||||
" " + DateUtils.getRelativeTimeSpanString(
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
|
||||
Date().time,
|
||||
@ -29,4 +35,18 @@ fun Item.sourceAndDateText(): String {
|
||||
}
|
||||
|
||||
return this.sourcetitle + 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()))
|
||||
}
|
||||
}
|
@ -6,23 +6,31 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.support.customtabs.CustomTabsIntent
|
||||
|
||||
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
|
||||
|
||||
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.ReaderActivity
|
||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import org.acra.ACRA
|
||||
|
||||
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||
|
||||
val actionIntent = Intent(Intent.ACTION_SEND)
|
||||
actionIntent.type = "text/plain"
|
||||
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(this, 0, actionIntent, 0)
|
||||
|
||||
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
actionIntent,
|
||||
0
|
||||
)
|
||||
|
||||
val intentBuilder = CustomTabsIntent.Builder()
|
||||
|
||||
@ -32,53 +40,88 @@ fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||
intentBuilder.setShowTitle(true)
|
||||
|
||||
|
||||
intentBuilder.setStartAnimations(this,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left)
|
||||
intentBuilder.setExitAnimations(this,
|
||||
android.R.anim.slide_in_left,
|
||||
android.R.anim.slide_out_right)
|
||||
intentBuilder.setStartAnimations(
|
||||
this,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left
|
||||
)
|
||||
intentBuilder.setExitAnimations(
|
||||
this,
|
||||
android.R.anim.slide_in_left,
|
||||
android.R.anim.slide_out_right
|
||||
)
|
||||
|
||||
val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
|
||||
intentBuilder.setCloseButtonIcon(closeicon)
|
||||
|
||||
val shareLabel = this.getString(R.string.label_share)
|
||||
val icon = BitmapFactory.decodeResource(resources,
|
||||
R.drawable.ic_share_white_24dp)
|
||||
val icon = BitmapFactory.decodeResource(
|
||||
resources,
|
||||
R.drawable.ic_share_white_24dp
|
||||
)
|
||||
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
|
||||
|
||||
return intentBuilder.build()
|
||||
}
|
||||
|
||||
fun Context.openItemUrl(linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
internalBrowser: Boolean,
|
||||
articleViewer: Boolean,
|
||||
app: Activity) {
|
||||
if (!internalBrowser || !linkDecoded.isUrlValid()) {
|
||||
openInBrowser(linkDecoded, app)
|
||||
fun Context.openItemUrlInternally(
|
||||
allItems: ArrayList<Item>,
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
articleViewer: Boolean,
|
||||
app: Activity
|
||||
) {
|
||||
if (articleViewer) {
|
||||
ReaderActivity.allItems = allItems
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
} else {
|
||||
if (articleViewer) {
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
|
||||
DragDismissIntentBuilder(this)
|
||||
.setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side
|
||||
.setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL) // Larger elasticities will make it easier to dismiss.
|
||||
.build(intent)
|
||||
|
||||
intent.putExtra("url", linkDecoded)
|
||||
app.startActivity(intent)
|
||||
} else {
|
||||
try {
|
||||
CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(linkDecoded)
|
||||
) { _, uri ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
openInBrowser(linkDecoded, app)
|
||||
try {
|
||||
CustomTabActivityHelper.openCustomTab(
|
||||
app,
|
||||
customTabsIntent,
|
||||
Uri.parse(linkDecoded)
|
||||
) { _, uri ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -88,3 +131,65 @@ private fun openInBrowser(linkDecoded: String, app: Activity) {
|
||||
intent.data = Uri.parse(linkDecoded)
|
||||
app.startActivity(intent)
|
||||
}
|
||||
|
||||
fun String.isUrlValid(): Boolean =
|
||||
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
|
||||
fun String.isBaseUrlValid(logErrors: Boolean): Boolean {
|
||||
val baseUrl = HttpUrl.parse(this)
|
||||
var existsAndEndsWithSlash = false
|
||||
if (baseUrl != null) {
|
||||
val pathSegments = baseUrl.pathSegments()
|
||||
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
||||
}
|
||||
|
||||
val isValid = Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
|
||||
if (!isValid && logErrors) {
|
||||
ACRA.getErrorReporter().doHandleSilentException(java.lang.Exception("Patterns.WEB_URL.matcher(this).matches() == ${Patterns.WEB_URL.matcher(this).matches()} && existsAndEndsWithSlash == $existsAndEndsWithSlash && baseUrl.pathSegments() == ${baseUrl?.pathSegments()}"))
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
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,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()
|
@ -2,7 +2,6 @@ package apps.amine.bou.readerforselfoss.utils.bottombar
|
||||
|
||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||
|
||||
|
||||
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||
this.setText("")
|
||||
this.hide()
|
||||
@ -10,7 +9,4 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||
}
|
||||
|
||||
fun TextBadgeItem.maybeShow(): TextBadgeItem =
|
||||
if (this.isHidden)
|
||||
this.show()
|
||||
else
|
||||
this
|
||||
if (this.isHidden) this.show() else this
|
||||
|
@ -1,12 +1,13 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.customtabs.CustomTabsClient;
|
||||
import android.support.customtabs.CustomTabsIntent;
|
||||
import android.support.customtabs.CustomTabsServiceConnection;
|
||||
import android.support.customtabs.CustomTabsSession;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
import androidx.browser.customtabs.CustomTabsSession;
|
||||
|
||||
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.
|
||||
*
|
||||
* @param activity The host activity.
|
||||
* @param activity The host activity.
|
||||
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
||||
* @param uri the Uri to be opened.
|
||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
||||
* @param uri the Uri to be opened.
|
||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
||||
*/
|
||||
public static void openCustomTab(Activity activity,
|
||||
CustomTabsIntent customTabsIntent,
|
||||
Uri uri,
|
||||
CustomTabFallback fallback) {
|
||||
CustomTabsIntent customTabsIntent,
|
||||
Uri uri,
|
||||
CustomTabFallback fallback) {
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||
|
||||
//If we cant find a package name, it means theres no browser that supports
|
||||
@ -47,6 +48,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||
|
||||
/**
|
||||
* Unbinds the Activity from the Custom Tabs Service.
|
||||
*
|
||||
* @param activity the activity that is connected to the service.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param connectionCallback
|
||||
*/
|
||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
||||
@ -81,6 +84,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||
|
||||
/**
|
||||
* Binds the Activity to the Custom Tabs Service.
|
||||
*
|
||||
* @param activity the activity to be binded to the service.
|
||||
*/
|
||||
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.
|
||||
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
||||
*/
|
||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
||||
if (mClient == null) return false;
|
||||
|
||||
CustomTabsSession session = getSession();
|
||||
if (session == null) return false;
|
||||
return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
||||
|
||||
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -141,9 +144,8 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||
*/
|
||||
public interface CustomTabFallback {
|
||||
/**
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.support.customtabs.CustomTabsService;
|
||||
import androidx.browser.customtabs.CustomTabsService;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
class CustomTabsHelper {
|
||||
private static final String TAG = "CustomTabsHelper";
|
||||
@ -26,7 +28,8 @@ class CustomTabsHelper {
|
||||
|
||||
private static String sPackageNameToUse;
|
||||
|
||||
private CustomTabsHelper() {}
|
||||
private CustomTabsHelper() {
|
||||
}
|
||||
|
||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
||||
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
|
||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||
* valid package name.
|
||||
*
|
||||
* <p>
|
||||
* This is <strong>not</strong> threadsafe.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param intent The intent to check with.
|
||||
* @return Whether there is a specialized handler for the given intent.
|
||||
*/
|
||||
@ -101,7 +105,7 @@ class CustomTabsHelper {
|
||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.GET_RESOLVED_FILTER);
|
||||
if (handlers == null || handlers.size() == 0) {
|
||||
if (handlers == null || handlers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (ResolveInfo resolveInfo : handlers) {
|
||||
|
@ -1,8 +1,9 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.support.customtabs.CustomTabsClient;
|
||||
import android.support.customtabs.CustomTabsServiceConnection;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||
|
||||
|
||||
import android.support.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
|
||||
|
||||
public interface ServiceConnectionCallback {
|
||||
/**
|
||||
* Called when the service is connected.
|
||||
*
|
||||
* @param client a CustomTabsClient
|
||||
*/
|
||||
void onServiceConnected(CustomTabsClient client);
|
||||
|
@ -1,15 +1,13 @@
|
||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
|
||||
package apps.amine.bou.readerforselfoss.utils.drawer
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
|
||||
|
||||
|
||||
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
||||
var icon: ImageView = view.findViewById(R.id.material_drawer_icon)
|
||||
var name: TextView = view.findViewById(R.id.material_drawer_name)
|
||||
|
@ -2,10 +2,10 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.drawer
|
||||
|
||||
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 androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||
import com.mikepenz.materialdrawer.holder.ImageHolder
|
||||
@ -15,9 +15,8 @@ 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>() {
|
||||
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
|
||||
BaseDrawerItem<T, VH>() {
|
||||
fun withIcon(url: String): T {
|
||||
this.icon = ImageHolder(url)
|
||||
return this as T
|
||||
@ -77,7 +76,10 @@ abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
|
||||
val selectedIconColor = getSelectedIconColor(ctx)
|
||||
|
||||
//set the background for the item
|
||||
UIUtils.setBackground(viewHolder.view, UIUtils.getSelectableBackground(ctx, selectedColor, true))
|
||||
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
|
||||
@ -86,8 +88,11 @@ abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
|
||||
//set the colors for textViews
|
||||
viewHolder.name.setTextColor(getTextColorStateList(color, selectedTextColor))
|
||||
//set the description text color
|
||||
ColorHolder.applyToOr(descriptionTextColor,
|
||||
viewHolder.description, getTextColorStateList(color, selectedTextColor))
|
||||
ColorHolder.applyToOr(
|
||||
descriptionTextColor,
|
||||
viewHolder.description,
|
||||
getTextColorStateList(color, selectedTextColor)
|
||||
)
|
||||
|
||||
//define the typeface for our textViews
|
||||
if (getTypeface() != null) {
|
||||
|
@ -1,20 +1,18 @@
|
||||
/* 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.support.annotation.LayoutRes
|
||||
import android.support.annotation.StringRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.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
|
||||
|
||||
import apps.amine.bou.readerforselfoss.R
|
||||
|
||||
|
||||
|
||||
class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(), ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
|
||||
class CustomUrlPrimaryDrawerItem :
|
||||
CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(),
|
||||
ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
|
||||
protected var mBadge: StringHolder = StringHolder("")
|
||||
protected var mBadgeStyle = BadgeStyle()
|
||||
|
||||
@ -67,7 +65,10 @@ class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrima
|
||||
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)))
|
||||
mBadgeStyle.style(
|
||||
viewHolder.badge,
|
||||
getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx))
|
||||
)
|
||||
viewHolder.badgeContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
viewHolder.badgeContainer.visibility = View.GONE
|
||||
@ -89,6 +90,5 @@ class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrima
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
@ -2,27 +2,37 @@ package apps.amine.bou.readerforselfoss.utils.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
||||
|
||||
|
||||
fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
|
||||
Glide.with(this).asBitmap().load(url).apply(RequestOptions.centerCropTransform()).into(iv)
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.bitmapFitCenter(url: String, iv: ImageView) =
|
||||
Glide.with(this).asBitmap().load(url).apply(RequestOptions.fitCenterTransform()).into(iv)
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(RequestOptions.centerCropTransform()).
|
||||
into(object : BitmapImageViewTarget(iv) {
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(object : BitmapImageViewTarget(iv) {
|
||||
override fun setResource(resource: Bitmap?) {
|
||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, resource)
|
||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
|
||||
resources,
|
||||
resource
|
||||
)
|
||||
circularBitmapDrawable.isCircular = true
|
||||
iv.setImageDrawable(circularBitmapDrawable)
|
||||
}
|
||||
|
@ -6,14 +6,14 @@ 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.module.GlideModule
|
||||
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 applyOptions(context: Context?, builder: GlideBuilder?) {
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
|
||||
|
||||
@ -22,11 +22,12 @@ class SelfSignedGlideModule : GlideModule {
|
||||
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))
|
||||
registry?.append(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package apps.amine.bou.readerforselfoss.utils.network
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo
|
||||
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 cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
|
||||
val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
|
||||
|
||||
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
|
||||
}
|
@ -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.title,
|
||||
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.title,
|
||||
this.content,
|
||||
this.unread,
|
||||
this.starred,
|
||||
this.thumbnail,
|
||||
this.icon,
|
||||
this.link,
|
||||
this.sourcetitle,
|
||||
this.tags.tags
|
||||
)
|
BIN
app/src/main/res/drawable-hdpi/ic_action_lab.png
Normal file
After Width: | Height: | Size: 683 B |
BIN
app/src/main/res/drawable-hdpi/ic_add.png
Normal file
After Width: | Height: | Size: 134 B |
Before Width: | Height: | Size: 124 B |
BIN
app/src/main/res/drawable-hdpi/ic_chrome_reader_mode.png
Normal file
After Width: | Height: | Size: 216 B |
After Width: | Height: | Size: 206 B |
BIN
app/src/main/res/drawable-hdpi/ic_cloud_download.png
Normal file
After Width: | Height: | Size: 334 B |
BIN
app/src/main/res/drawable-hdpi/ic_fiber_new.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
app/src/main/res/drawable-hdpi/ic_history.png
Normal file
After Width: | Height: | Size: 551 B |
BIN
app/src/main/res/drawable-hdpi/ic_open_in_browser.png
Normal file
After Width: | Height: | Size: 204 B |
After Width: | Height: | Size: 523 B |
Before Width: | Height: | Size: 434 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_lab.png
Normal file
After Width: | Height: | Size: 409 B |
BIN
app/src/main/res/drawable-mdpi/ic_add.png
Normal file
After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 86 B |
BIN
app/src/main/res/drawable-mdpi/ic_chrome_reader_mode.png
Normal file
After Width: | Height: | Size: 136 B |
After Width: | Height: | Size: 134 B |
BIN
app/src/main/res/drawable-mdpi/ic_cloud_download.png
Normal file
After Width: | Height: | Size: 228 B |
BIN
app/src/main/res/drawable-mdpi/ic_fiber_new.png
Normal file
After Width: | Height: | Size: 215 B |
BIN
app/src/main/res/drawable-mdpi/ic_history.png
Normal file
After Width: | Height: | Size: 352 B |
BIN
app/src/main/res/drawable-mdpi/ic_open_in_browser.png
Normal file
After Width: | Height: | Size: 157 B |
After Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 307 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_lab.png
Normal file
After Width: | Height: | Size: 871 B |
BIN
app/src/main/res/drawable-xhdpi/ic_add.png
Normal file
After Width: | Height: | Size: 168 B |
Before Width: | Height: | Size: 108 B |
BIN
app/src/main/res/drawable-xhdpi/ic_chrome_reader_mode.png
Normal file
After Width: | Height: | Size: 171 B |
After Width: | Height: | Size: 174 B |
BIN
app/src/main/res/drawable-xhdpi/ic_cloud_download.png
Normal file
After Width: | Height: | Size: 380 B |