Compare commits
378 Commits
v1.5.0.1
...
v161808215
Author | SHA1 | Date | |
---|---|---|---|
9606d36670 | |||
869cf64c54 | |||
f57ec1f6c0 | |||
361eea9a06 | |||
838b4056ac | |||
0c0a98510b | |||
be642ed06f | |||
fd77f38e95 | |||
c9baab7267 | |||
86985cfd5b | |||
1327a4e069 | |||
c46acbc579 | |||
4c6a403fae | |||
78920022bd | |||
7b16c41e82 | |||
3389f8bd09 | |||
8dc25c527d | |||
46d6bd57c1 | |||
db014fe13d | |||
6c293f4cac | |||
91e5d3736f | |||
e11dee220f | |||
fcebf916d2 | |||
73cc1a7297 | |||
798f112498 | |||
38b5e7dc65 | |||
2799a48f2b | |||
ad5edae6cd | |||
9cb02f0272 | |||
6d24fd9336 | |||
a3a7b78c96 | |||
e995286068 | |||
65fb6d9b7e | |||
eb02d1efad | |||
f8d3e1eefb | |||
218b8fa843 | |||
9f94af6239 | |||
d3584ac40e | |||
90bdb289d0 | |||
78a08750a2 | |||
baba851e97 | |||
2a03783623 | |||
9f2a4438b1 | |||
5ee5287ffa | |||
29547c2c94 | |||
4846c870fa | |||
c17980a032 | |||
a929e419d9 | |||
487d484bae | |||
0ca4c04c61 | |||
c857cf2d67 | |||
acb502028b | |||
533636f3a1 | |||
eb5672901b | |||
53a8716b51 | |||
3aaff612af | |||
fdcd8c6c6a | |||
bafd478604 | |||
987513a88b | |||
a450ab2a3b | |||
db89fe5aad | |||
67a30b92f6 | |||
c397de8c3e | |||
b4db532c45 | |||
ebecc9c80a | |||
4f8556fca8 | |||
68892fb41b | |||
6d6f6c72ac | |||
df5556b945 | |||
d6c74049c3 | |||
18946464a2 | |||
edb5eabee7 | |||
99a305f3e2 | |||
68dc5a6acf | |||
6816461502 | |||
15b93bbd9e | |||
cd61e140f6 | |||
4d861a84e6 | |||
f24de68618 | |||
3bcffff444 | |||
75e9031fa5 | |||
3b77e24399 | |||
0a738e895f | |||
242e5ba035 | |||
c94612106c | |||
320924b4ed | |||
403ecc4521 | |||
6a50b37364 | |||
d9d341ac5d | |||
e9805b731e | |||
c6d4337cd1 | |||
173f4b2ff7 | |||
3b9436264c | |||
35fe87d79d | |||
f1bb7ba9ad | |||
279f229166 | |||
be1794e27b | |||
4d4a2039c8 | |||
3013ae4f35 | |||
bb3f7d3786 | |||
f7cc305e44 | |||
da17f89148 | |||
ec71ab3c6f | |||
0d007f1492 | |||
96f8663b8f | |||
1a4bc1b301 | |||
b51ae58a97 | |||
b126fc32da | |||
b8d234c415 | |||
2c8902d404 | |||
80ad65b196 | |||
744d9ba72b | |||
0c1d708588 | |||
95e79e7c5d | |||
3ce3260d20 | |||
641f4f34d3 | |||
99620cb1c5 | |||
8f5f33f5d2 | |||
78e9230b82 | |||
78aa44c007 | |||
53fd944f00 | |||
9e6cb4ee3d | |||
87ad6f2826 | |||
9050f5a56f | |||
3437004082 | |||
dcf620af87 | |||
128085a02e | |||
302040ec25 | |||
e177c22032 | |||
a11007113a | |||
5e7897bcf4 | |||
9559af3637 | |||
4c499abcdb | |||
0055a503b3 | |||
3a189ee4b6 | |||
e25dc49271 | |||
4208a80db8 | |||
ddb75e0d93 | |||
8b37e992a2 | |||
bac59036cd | |||
6c89a3b77c | |||
dc2ef39fc6 | |||
a4806da2c5 | |||
ee30edb214 | |||
e4ed663fb3 | |||
01629309b0 | |||
059c2991fb | |||
686ec5dd90 | |||
eab9df8ed9 | |||
0107c3d7e2 | |||
2def2f2e2c | |||
44c79892a0 | |||
bc96b314c2 | |||
8dcf749b4e | |||
6a56ec6442 | |||
30e46d7eae | |||
9458b1834b | |||
297f797b97 | |||
c70e80758c | |||
3bf1d7c4f9 | |||
173247041a | |||
3a28772096 | |||
bd08b8aba3 | |||
2ceb0f988b | |||
4ef3b155b8 | |||
350e24cded | |||
1bf8a578bc | |||
4818a101cc | |||
baebf938ef | |||
fea57c7b1e | |||
113dfa68be | |||
60c6514fa1 | |||
114485afc3 | |||
d6a51381b9 | |||
620f13fd7c | |||
6577b2c3d7 | |||
caef522c8b | |||
40ea07de2e | |||
7905e4aa12 | |||
64f4fd708a | |||
b46e4a018f | |||
3e999a9be2 | |||
f656d621e6 | |||
951cc1e6bd | |||
d8d4264f1b | |||
014eeec2b9 | |||
83837bddc3 | |||
f97666db92 | |||
ee08ea41a1 | |||
4ca64610cb | |||
4980145e46 | |||
10cbc19a0c | |||
15fba2b29b | |||
096952f88c | |||
0ea70c1922 | |||
69ac2e2b44 | |||
68098f4d84 | |||
080d52893e | |||
b02334a8d4 | |||
27118add22 | |||
2a6f98a1e8 | |||
1f67f2fdee | |||
ebf4d294a8 | |||
4a4dbacc95 | |||
687839b5f8 | |||
8fb339034f | |||
8e9fd9c985 | |||
72400f71c0 | |||
1151587951 | |||
abcd500045 | |||
beda24e736 | |||
37b2c5c2df | |||
7b5246ebf1 | |||
d6d5e72f48 | |||
fa8e88d489 | |||
1ef2da9f76 | |||
1ebd894be7 | |||
a9c493d105 | |||
f833d73fab | |||
9e6602f114 | |||
3bdfef9f8b | |||
6f7f475a6b | |||
8fc5fab67b | |||
6927e92396 | |||
c7470396d7 | |||
f21570e2e4 | |||
51f406e20c | |||
9e3fde744e | |||
ccf406ae68 | |||
bc78d1e079 | |||
d151eb261e | |||
0856598cd9 | |||
f0563efc62 | |||
84dfa9a8a5 | |||
8e25489cca | |||
198f95e1ca | |||
7e02fe89ea | |||
819356412c | |||
deb789bc1b | |||
133ba74548 | |||
1461e32643 | |||
f400c3d9ac | |||
7e595a4f74 | |||
18c9c499b2 | |||
24ae115ed4 | |||
7f345558cd | |||
57177cc910 | |||
cea258bc21 | |||
ed9b1c8ba7 | |||
5a79fd89e9 | |||
42a130db08 | |||
320a8d19de | |||
5721506007 | |||
803e8cb2f4 | |||
98492fd0c0 | |||
0b07178577 | |||
07e545079c | |||
95d64dc5e8 | |||
abe546dcda | |||
e6f367acaf | |||
a9b61853b9 | |||
5afc04a630 | |||
1da4cc2782 | |||
c5ebc89e4f | |||
dfc1719cce | |||
0812259470 | |||
e1476c5840 | |||
e30ea28e3f | |||
4a6d3aab7f | |||
8157146498 | |||
94d23888b1 | |||
737fe9bb4a | |||
0051ed2e73 | |||
e0595957e2 | |||
8d09ff7fdb | |||
04feb66b07 | |||
54b2ac7f24 | |||
12356a35fa | |||
12262304ac | |||
c58f97452e | |||
eb3872f7a6 | |||
9fa178d513 | |||
043b184065 | |||
10559bb894 | |||
d0000d66b2 | |||
b447ac738a | |||
faebfc238c | |||
c28fbd37cc | |||
4b8396959d | |||
b39d510e07 | |||
286dda7f80 | |||
7bda896e2d | |||
ba4feeea87 | |||
6f52eae3c6 | |||
40ea8d56e6 | |||
72e562e8a8 | |||
6fa01bfe19 | |||
0ef59c9b91 | |||
d768d2232b | |||
b44a200731 | |||
016815e0d1 | |||
590534e4a6 | |||
7ea9d4e519 | |||
e0ab09f533 | |||
fbe98f1b16 | |||
d0675b8443 | |||
3ea1ed02ae | |||
ba120b1e0b | |||
acf6995c2d | |||
8306860f90 | |||
65974166be | |||
ee8924f986 | |||
170e575465 | |||
b7d5317b10 | |||
f12e7748c5 | |||
69a2418afc | |||
4924ddd172 | |||
1889b43786 | |||
f2e38a4203 | |||
90a8fac8d4 | |||
04402c5ab9 | |||
f8f710df99 | |||
b8105bb6fb | |||
1d18c898b2 | |||
95e208000f | |||
ecdddef81d | |||
c9b1d329e6 | |||
e68c16c7a4 | |||
585c57fe3a | |||
d04cbac79c | |||
044585ee9b | |||
299478e840 | |||
b2d69be5f8 | |||
dc970bbf3c | |||
8717bd5d5d | |||
5b307a8407 | |||
daef66087d | |||
1ad1cf4460 | |||
c0b9718368 | |||
d684f323b8 | |||
24a1c56fe6 | |||
cdeba4f84e | |||
cafba196cf | |||
493b1b12b3 | |||
5320f88230 | |||
246ec2c3ac | |||
9c9b45aeab | |||
8c5dc43735 | |||
b1e812314f | |||
c14f47a74b | |||
58a5b4a5e5 | |||
1cfc2bf36f | |||
5a56d826d9 | |||
8ad8b55424 | |||
3da1d431db | |||
4565079f29 | |||
3482092cb2 | |||
2df5e52de0 | |||
0ef4fc67fa | |||
e2bfd549d3 | |||
7071af5fa5 | |||
95f267f701 | |||
f363bbcc0c | |||
65a912f271 | |||
758661f5fb | |||
2d3c297726 | |||
ca85e3d3ed | |||
2190ad0387 | |||
0d067e05af | |||
c3305b7523 | |||
fb84b31122 | |||
9d40026ef7 | |||
041a225992 | |||
b42dc7f87c | |||
0283e49c27 | |||
e5e1b2f5a5 | |||
813a0ae475 | |||
f0e036cdd8 |
67
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
### Hey you !
|
||||||
|
|
||||||
|
Thank you for wanting to help. Even the smallest things can help this project become better.
|
||||||
|
|
||||||
|
Please read the guidelines before contributing, and follow them (or try to) when contributing.
|
||||||
|
|
||||||
|
### What you can do to help.
|
||||||
|
|
||||||
|
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
|
||||||
|
|
||||||
|
You can fork the repository, and [help me solve some issues](https://github.com/aminecmi/ReaderforSelfoss/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||||
|
|
||||||
|
### What I can't help you with.
|
||||||
|
|
||||||
|
Please, don't use the issue tracker for anything related to [Selfoss itself](https://github.com/SSilence/selfoss). The app calls the api provided by Selfoss, and can't help with solving issues with your Selfoss instance.
|
||||||
|
|
||||||
|
Always check if the web version of your instance is working.
|
||||||
|
|
||||||
|
# Some rules
|
||||||
|
### Bug reports/Feature request
|
||||||
|
|
||||||
|
* Always search before reporting an issue or asking for a feature to avoid duplicates.
|
||||||
|
* Include your unique user id. It's displayed on the debug settings page. (You can tap it, it'll be copied to your clipboard)
|
||||||
|
* Include every other useful details (app version, phone model, Android version and screenshots when possible).
|
||||||
|
* Avoid bumping non-fatal issues, or feature requests. I'll try to fix them as soon as possible, and try to prioritize the requests. (You may wan to use the [reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) for that)
|
||||||
|
|
||||||
|
### Pull requests
|
||||||
|
|
||||||
|
* Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why.
|
||||||
|
* Please ask before starting to work on an issue. I may be working on it, or someone else could be doing so.
|
||||||
|
* Each pull request should implement **ONE** feature or bugfix. Keep in mind that you can submit as many PR as you want.
|
||||||
|
* Your code must be simple and clear enough to avoid using comments to explain what it does.
|
||||||
|
* Follow the used coding style [the android koding style](https://android.github.io/kotlin-guides/style.html) ([some idoms for reference](http://kotlinlang.org/docs/reference/idioms.html)) with more to come.
|
||||||
|
* Try as much as possible to write a test for your feature, and if you do so, run it, and make it work.
|
||||||
|
* Always check your changes and discard the ones that are irrelevant to your feature or bugfix.
|
||||||
|
* Have meaningful commit messages.
|
||||||
|
* Always reference the issue you are working on in your PR description.
|
||||||
|
* Be willing to accept criticism on your PRs (as I am on mine).
|
||||||
|
* Remember that PR review can take time.
|
||||||
|
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
|
||||||
|
You can directly import this project into IntellIJ/Android Studio.
|
||||||
|
|
||||||
|
You'll have to:
|
||||||
|
|
||||||
|
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
|
||||||
|
|
||||||
|
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
#### Inside ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
```
|
||||||
|
appLoginUrl="URL" # It can be empty.
|
||||||
|
appLoginUsername="LOGIN" # It can be empty.
|
||||||
|
appLoginPassword="PASS" # It can be empty.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### As gradle parameters
|
||||||
|
|
||||||
|
```
|
||||||
|
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
|
||||||
|
```
|
32
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* [ ] Are you running the latest version?
|
||||||
|
* [ ] Did you check for an existing issue ?
|
||||||
|
* [ ] Are you reporting to the correct repository?
|
||||||
|
* [ ] Did you perform a cursory search?
|
||||||
|
* [ ] Did you read the `CONTRIBUTING` guide ?
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
[Description of the bug or feature]
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. [First Step]
|
||||||
|
2. [Second Step]
|
||||||
|
3. [and so on...]
|
||||||
|
|
||||||
|
**Expected behavior:** [What you expected to happen]
|
||||||
|
|
||||||
|
**Actual behavior:** [What actually happened]
|
||||||
|
|
||||||
|
|
||||||
|
### Screenshots (optional)
|
||||||
|
|
||||||
|
`...`
|
||||||
|
|
||||||
|
### Device
|
||||||
|
|
||||||
|
- Device (manufacturer, model ...)
|
||||||
|
- OS (Android Version, ROM/Stock, Rooted/not, mods...)
|
||||||
|
- App version _(See Prerequisites)_
|
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
## Types of changes
|
||||||
|
|
||||||
|
- [ ] I have read the **CONTRIBUTING** document.
|
||||||
|
- [ ] My code follows the code style of this project.
|
||||||
|
- [ ] I have updated the documentation accordingly.
|
||||||
|
- [ ] I have added tests to cover my changes.
|
||||||
|
- [ ] All new and existing tests passed.
|
||||||
|
- [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654))
|
||||||
|
|
||||||
|
This closes issue #XXX
|
||||||
|
|
||||||
|
This is implements feature #YYY
|
||||||
|
|
||||||
|
This finishes chore #ZZZ
|
7
.gitignore
vendored
@ -214,7 +214,8 @@ gradle-app.setting
|
|||||||
|
|
||||||
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
|
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
|
||||||
|
|
||||||
secrets.xml
|
release/
|
||||||
|
|
||||||
mipmap-*
|
crowdin.properties
|
||||||
release/
|
|
||||||
|
publish-version.sh
|
360
CHANGELOG.md
@ -1,3 +1,361 @@
|
|||||||
|
**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.
|
||||||
|
|
||||||
|
**1.5.7.x**
|
||||||
|
|
||||||
|
- Added confirmation to the mark as read and update menues.
|
||||||
|
|
||||||
|
- Add to favorites from article viewer.
|
||||||
|
|
||||||
|
- Added an option to use a webview in the article viewer (see #149)
|
||||||
|
|
||||||
|
- Fixes (#151 #152 #155 #157 #160 #174) and more.
|
||||||
|
|
||||||
|
- New year fixes !!!
|
||||||
|
|
||||||
|
- Changed page indicator position as it was overlaping content.
|
||||||
|
|
||||||
|
- Now using slack instead of gitter.
|
||||||
|
|
||||||
|
- Moved completely to a webview to fix #161.
|
||||||
|
|
||||||
|
- Fixed typos in French ( Thanks @aancel )
|
||||||
|
|
||||||
|
- Updated the Contribution guide about translations.
|
||||||
|
|
||||||
|
- Better handling for articles update. (See #169)
|
||||||
|
|
||||||
|
- Ability to change the article viewer content font size (see #153)
|
||||||
|
|
||||||
|
- Versions updates * 2.
|
||||||
|
|
||||||
|
- Added padding to the recyclerview.
|
||||||
|
|
||||||
|
**1.5.5.x (didn't last long) AND 1.5.6.x**
|
||||||
|
|
||||||
|
- Toolbar in reader activity.
|
||||||
|
|
||||||
|
- Marking items as read on scroll (with settings to enable/disable).
|
||||||
|
|
||||||
|
- Swapped the title and subtitle in the article viewer.
|
||||||
|
|
||||||
|
- Added an animation to the viewpager.
|
||||||
|
|
||||||
|
- Completed Dutch, Indonesian and Portuguese translations !
|
||||||
|
|
||||||
|
- Fixed #142, #144, #147.
|
||||||
|
|
||||||
|
- Changed versions handling.
|
||||||
|
|
||||||
|
- Removed indonesian english as it was causing issues with the english version of the app.
|
||||||
|
|
||||||
|
**1.5.4.22**
|
||||||
|
|
||||||
|
- You can now scroll through the loaded articles !
|
||||||
|
|
||||||
|
**1.5.4.21**
|
||||||
|
|
||||||
|
- Spanish translation and some Indonesian !
|
||||||
|
|
||||||
|
**1.5.4.20**
|
||||||
|
|
||||||
|
- Turkish translation !
|
||||||
|
|
||||||
|
**1.5.4.19**
|
||||||
|
|
||||||
|
- Fixed an issue with crowdin configuration (and its translations)
|
||||||
|
|
||||||
|
**1.5.4.18**
|
||||||
|
|
||||||
|
- Typo fix.
|
||||||
|
|
||||||
|
- The real last infinite scroll bug fix.
|
||||||
|
|
||||||
|
- Simplified Chinese translation !
|
||||||
|
|
||||||
|
**1.5.4.17**
|
||||||
|
|
||||||
|
- Fixed the last bug with infinite scroll.
|
||||||
|
|
||||||
|
**1.5.4.16**
|
||||||
|
|
||||||
|
- Fixing list view displaying issues.
|
||||||
|
|
||||||
|
- Endless scroll is not in beta anymore.
|
||||||
|
|
||||||
|
**1.5.4.15**
|
||||||
|
|
||||||
|
- Fixed an issue with the sources list.
|
||||||
|
|
||||||
|
**1.5.4.14**
|
||||||
|
|
||||||
|
- Fixing infinite scroll trying to load more items when there are no more.
|
||||||
|
|
||||||
|
**1.5.4.13**
|
||||||
|
|
||||||
|
- Displaying the right number of items.
|
||||||
|
|
||||||
|
- Fixing infinite scroll remaining issues. Should be stable enough.
|
||||||
|
|
||||||
|
**1.5.4.12**
|
||||||
|
|
||||||
|
- Fixed fab and toolbar issue (#113)
|
||||||
|
|
||||||
|
- Fixed links clickable (#114)
|
||||||
|
|
||||||
|
- Changed the link colors in the article viewer
|
||||||
|
|
||||||
|
**1.5.4.11**
|
||||||
|
|
||||||
|
- Hiding FABs on scroll.
|
||||||
|
|
||||||
|
- Closing #109 (code cleaning)
|
||||||
|
|
||||||
|
- Hiding fabs on scroll (#101)
|
||||||
|
|
||||||
|
**1.5.4.10**
|
||||||
|
|
||||||
|
- Displaying a loader when "reading more" in the article viewer.
|
||||||
|
|
||||||
|
- Displaying the thumbnail instead of icon on the article viewer.
|
||||||
|
|
||||||
|
- Scrolling to top when loading content with the "read more" button.
|
||||||
|
|
||||||
|
**1.5.4.09**
|
||||||
|
|
||||||
|
- Using the kotlin wrapper for the material drawer (see #98 for more details).
|
||||||
|
|
||||||
|
- Updated support libraries
|
||||||
|
|
||||||
|
- Changed the Floating Action Button to the support library version.
|
||||||
|
|
||||||
|
- New reader activity action bar #103.
|
||||||
|
|
||||||
|
**1.5.4.08**
|
||||||
|
|
||||||
|
- Thanks @jrafaelsantana for translating the whole app in Brazilian Portuguese.
|
||||||
|
|
||||||
|
**1.5.4.07**
|
||||||
|
|
||||||
|
- Loading more items on swipe too.
|
||||||
|
|
||||||
|
- Fixed popup menu style. User may need to reselect the theme.
|
||||||
|
|
||||||
|
- Disabled reporting marking items as read if there isn't an issue.
|
||||||
|
|
||||||
|
**1.5.4.05/06**
|
||||||
|
|
||||||
|
- Translation fix.
|
||||||
|
|
||||||
|
**1.5.4.04**
|
||||||
|
|
||||||
|
- Fixing an issue with marking items as read (something related to an old version of selfoss).
|
||||||
|
|
||||||
|
**1.5.4.03**
|
||||||
|
|
||||||
|
- Trying to fix some issue with pre-launch reports. Reverted because it seems to be related to the dev console side.
|
||||||
|
|
||||||
|
**1.5.4.02**
|
||||||
|
|
||||||
|
- Fixing full height cards issue.
|
||||||
|
|
||||||
|
**1.5.4.01**
|
||||||
|
|
||||||
|
- Removed the "apk downloaded from outside of playstore" message.
|
||||||
|
|
||||||
|
- Versions update.
|
||||||
|
|
||||||
|
- HTML viewer version update. It should fix an issue with images.
|
||||||
|
|
||||||
|
- Some code cleaning.
|
||||||
|
|
||||||
|
**1.5.4.00**
|
||||||
|
|
||||||
|
- Added issue reporting from within the app.
|
||||||
|
|
||||||
|
**1.5.3.06**
|
||||||
|
|
||||||
|
- Fixed infinite scroll not working.
|
||||||
|
|
||||||
|
- Fixed logs not working.
|
||||||
|
|
||||||
|
- Temporary workaround handling opening invalid urls. Waiting to solve #83.
|
||||||
|
|
||||||
|
**1.5.3.05**
|
||||||
|
|
||||||
|
- Fixed an issue on older versions of Android.
|
||||||
|
|
||||||
|
- Libs update.
|
||||||
|
|
||||||
|
**1.5.3.04**
|
||||||
|
|
||||||
|
- Crowdin translations
|
||||||
|
|
||||||
|
**1.5.3.03**
|
||||||
|
|
||||||
|
- Libs updates.
|
||||||
|
|
||||||
|
- Translation fix.
|
||||||
|
|
||||||
|
**1.5.3.01/02**
|
||||||
|
|
||||||
|
- Added translation link to the settings page.
|
||||||
|
|
||||||
|
- Added the translation link to the README.
|
||||||
|
|
||||||
|
**1.5.3.00**
|
||||||
|
|
||||||
|
- (BETA) Added pull from bottom to load more pages of results. May be buggy.
|
||||||
|
|
||||||
|
**1.5.2.18/19**
|
||||||
|
|
||||||
|
- APK minification finally working. That means less space taken !
|
||||||
|
- Added an option to log every API call.
|
||||||
|
|
||||||
|
**1.5.2.17**
|
||||||
|
|
||||||
|
- Source code and tracker links weren't being set, and updated the contributing doc.
|
||||||
|
|
||||||
|
**1.5.2.15/16**
|
||||||
|
|
||||||
|
- Adding an account header on the lateral drawer.
|
||||||
|
|
||||||
|
- The account header is only displayed when the setting is enabled.
|
||||||
|
|
||||||
|
**1.5.2.13/14**
|
||||||
|
|
||||||
|
- Updated glide.
|
||||||
|
|
||||||
|
- Loading images from self signed certificate now working.
|
||||||
|
|
||||||
|
**1.5.2.12**
|
||||||
|
|
||||||
|
- Self signed certificates are now working for loading data. Image are not loading yet.
|
||||||
|
|
||||||
|
**1.5.2.11**
|
||||||
|
|
||||||
|
- Added a random unique identifier to be used in the logs.
|
||||||
|
|
||||||
|
**1.5.2.08/09/10**
|
||||||
|
|
||||||
|
- Added settable logs for reading articles problems.
|
||||||
|
|
||||||
|
**1.5.2.07**
|
||||||
|
|
||||||
|
- Added the ability to choose the number of items loaded (the maximum value is 200 and is imposed by the selfoss api)
|
||||||
|
|
||||||
|
**1.5.2.06**
|
||||||
|
|
||||||
|
- Fix problem introduced in 1.5.2.04. SVG file not working on older versions of android.
|
||||||
|
|
||||||
|
**1.5.2.05**
|
||||||
|
|
||||||
|
- Versions updates
|
||||||
|
|
||||||
|
**1.5.2.04**
|
||||||
|
|
||||||
|
- Reverted to the old icon.
|
||||||
|
|
||||||
|
- Better icon for the intro activity.
|
||||||
|
|
||||||
|
- Updated gradle version.
|
||||||
|
|
||||||
|
**1.5.2.03**
|
||||||
|
|
||||||
|
- Added the ability to accept self signed certificates. (Needs more testing)
|
||||||
|
|
||||||
|
**1.5.2.02**
|
||||||
|
|
||||||
|
- Added optional login option.
|
||||||
|
|
||||||
|
**1.5.2.01**
|
||||||
|
|
||||||
|
- New (Better) Icon !
|
||||||
|
|
||||||
|
**1.5.2.0**
|
||||||
|
|
||||||
|
- New Icon !
|
||||||
|
|
||||||
|
**1.5.1.9/10/11**
|
||||||
|
|
||||||
|
- Hiding the unread badge when marking all items as read.
|
||||||
|
|
||||||
|
**1.5.1.8**
|
||||||
|
|
||||||
|
- Fixes and libs updates.
|
||||||
|
|
||||||
|
**1.5.1.7**
|
||||||
|
|
||||||
|
- Bug fixes.
|
||||||
|
|
||||||
|
- Code cleaning
|
||||||
|
|
||||||
|
**1.5.1.6**
|
||||||
|
|
||||||
|
- Added back the badges after it was fixed on the library side.
|
||||||
|
|
||||||
|
**1.5.1.5**
|
||||||
|
|
||||||
|
- THEMES !!!! For now, the app has predefined themes. You can ask for new ones until I make them dynamic.
|
||||||
|
|
||||||
|
**1.5.1.3/4**
|
||||||
|
|
||||||
|
- Fixes introduces by the previous alpha (1.5.1.2)
|
||||||
|
|
||||||
|
**1.5.1.2**
|
||||||
|
|
||||||
|
- Added testing to the CI.
|
||||||
|
|
||||||
|
- Code cleaning
|
||||||
|
|
||||||
|
- Display the pull to refresh loader on api call
|
||||||
|
|
||||||
|
- Fixes :
|
||||||
|
|
||||||
|
- Can't pull down to refresh on first launch
|
||||||
|
|
||||||
|
- Recurring crash because of the url
|
||||||
|
|
||||||
|
- Couldn't open some urls because of missing "http"
|
||||||
|
|
||||||
|
- Adding a source with invalid url would crash
|
||||||
|
|
||||||
|
|
||||||
|
**1.5.1.1**
|
||||||
|
|
||||||
|
- Fixed an issue when trying to add a source without being logged in.
|
||||||
|
|
||||||
|
- Reloading drawer tags badges on slide to refresh.
|
||||||
|
|
||||||
|
**1.5.1**
|
||||||
|
|
||||||
|
- Added a drawer for filtering sources and tags.
|
||||||
|
|
||||||
|
- You can now search for items from the toolbar.
|
||||||
|
|
||||||
|
**1.5.0.2**
|
||||||
|
|
||||||
|
- If the content in the article viewer is empty, the article will open in a custom tab.
|
||||||
|
|
||||||
|
- Added a share button, and an "open in browser" button to the bottom of the article viewer.
|
||||||
|
|
||||||
|
- Updated custom tab code.
|
||||||
|
|
||||||
**1.5.0.1**
|
**1.5.0.1**
|
||||||
|
|
||||||
- The release APK wasn't working at all.
|
- The release APK wasn't working at all.
|
||||||
@ -145,4 +503,4 @@ _Updates_
|
|||||||
|
|
||||||
**1.3.3.4**
|
**1.3.3.4**
|
||||||
|
|
||||||
...
|
...
|
||||||
|
24
README.md
@ -1,29 +1,29 @@
|
|||||||
# ReaderForSelfoss
|
# 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/)
|
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>
|
||||||
|
|
||||||
## Build
|
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).
|
||||||
|
|
||||||
You can directly import this project into IntellIJ/Android Studio.
|
## Join the alpha channel
|
||||||
|
|
||||||
You'll have to:
|
**Keep in mind, it could be instable, but you'll have the new updates faster**
|
||||||
|
|
||||||
- [Create your own launcher icon](https://developer.android.com/studio/write/image-asset-studio.html#creating-launcher)
|
- 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.
|
||||||
|
|
||||||
- Configure Fabric, or [remove it](https://docs.fabric.io/android/fabric/settings/removing.html#).
|
## Want to help ?
|
||||||
- Define the following in `res/values/strings.xml` or create `res/values/secrets.xml`
|
|
||||||
|
|
||||||
- mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser
|
Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md)
|
||||||
- feedback_email: An email to receive users feedback.
|
|
||||||
- source_url: an url to the source code, used in the settings
|
|
||||||
- tracker_url: an url to the tracker, used in the settings
|
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
|
||||||
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
|
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
|
||||||
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
|
||||||
- [Help me translate the app](https://poeditor.com/join/project/viHr8ujJ7S)
|
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
|
||||||
|
- [Ask for help](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ)
|
||||||
|
162
app/build.gradle
@ -1,32 +1,48 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
}
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
ext {
|
||||||
classpath 'io.fabric.tools:gradle:1.+'
|
configuration = [
|
||||||
}
|
buildDate: new Date()
|
||||||
|
]
|
||||||
|
// This will make me able to build multiple times a day. May break thinks. I may forget it.
|
||||||
|
todaysBuilds = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
def gitVersion() {
|
||||||
|
def process = "git describe --abbrev=0 --tags".execute()
|
||||||
|
return process.text.substring(1).replaceAll("\\.", "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
def versionCodeFromGit() {
|
||||||
|
println "version code " + gitVersion()
|
||||||
|
return gitVersion().toInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
def versionNameFromGit() {
|
||||||
|
println "version name " + gitVersion()
|
||||||
|
return gitVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
apply plugin: 'io.fabric'
|
|
||||||
|
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
repositories {
|
apply plugin: 'kotlin-android-extensions'
|
||||||
maven { url 'https://maven.fabric.io/public' }
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileOptions {
|
||||||
buildToolsVersion "25.0.3"
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
compileSdkVersion 28
|
||||||
|
buildToolsVersion '28.0.1'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "apps.amine.bou.readerforselfoss"
|
applicationId "apps.amine.bou.readerforselfoss"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 27
|
||||||
versionCode 1501
|
versionCode versionCodeFromGit()
|
||||||
versionName "1.5.0.1"
|
versionName versionNameFromGit()
|
||||||
|
|
||||||
// Enabling multidex support.
|
// Enabling multidex support.
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@ -35,90 +51,118 @@ android {
|
|||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
}
|
}
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
// tests
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||||
'proguard-rules.pro'
|
'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
debug {
|
||||||
|
buildConfigField "String", "LOGIN_URL", appLoginUrl
|
||||||
|
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
|
||||||
|
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions "build"
|
flavorDimensions "build"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
githubConfig {
|
githubConfig {
|
||||||
versionNameSuffix '-github'
|
versionNameSuffix '-github'
|
||||||
dimension "build"
|
dimension "build"
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "true"
|
|
||||||
}
|
}
|
||||||
storeConfig {
|
storeConfig {
|
||||||
|
// As jenkins publishes to alpha first, this is the default suffix now.
|
||||||
versionNameSuffix '-store'
|
versionNameSuffix '-store'
|
||||||
dimension "build"
|
dimension "build"
|
||||||
buildConfigField "boolean", "GITHUB_VERSION", "false"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
// Testing
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.1'
|
||||||
|
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.1'
|
||||||
|
// Espresso-intents for validation and stubbing of Intents
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.1'
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
// Android Support
|
// Android Support
|
||||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||||
compile 'com.android.support:design:25.3.1'
|
implementation 'com.android.support:design:27.1.1'
|
||||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
implementation 'com.android.support:recyclerview-v7:27.1.1'
|
||||||
compile 'com.android.support:support-v4:25.3.1'
|
implementation 'com.android.support:support-v4:27.1.1'
|
||||||
compile 'com.android.support:support-vector-drawable:25.3.1'
|
implementation 'com.android.support:support-vector-drawable:27.1.1'
|
||||||
compile 'com.android.support:customtabs:25.3.1'
|
implementation 'com.android.support:customtabs:27.1.1'
|
||||||
compile 'com.android.support:cardview-v7:25.3.1'
|
implementation 'com.android.support:cardview-v7:27.1.1'
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
|
||||||
|
|
||||||
// Firebase + crashlytics
|
|
||||||
compile 'com.google.firebase:firebase-core:10.2.6'
|
|
||||||
compile 'com.google.firebase:firebase-config:10.2.6'
|
|
||||||
compile 'com.google.firebase:firebase-invites:10.2.6'
|
|
||||||
compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
|
|
||||||
transitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//multidex
|
//multidex
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
// Intro
|
// Intro
|
||||||
compile 'agency.tango.android:material-intro-screen:0.0.5'
|
implementation 'agency.tango.android:material-intro-screen:0.0.5'
|
||||||
|
|
||||||
// About
|
// About
|
||||||
compile('com.mikepenz:aboutlibraries:5.9.6@aar') {
|
implementation('com.mikepenz:aboutlibraries:6.0.0@aar') {
|
||||||
transitive = true
|
transitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrofit + http logging + okhttp
|
// Retrofit + http logging + okhttp
|
||||||
compile 'com.squareup.retrofit2:retrofit:2.3.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||||
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
|
||||||
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
||||||
compile 'com.burgstaller:okhttp-digest:1.12'
|
implementation 'com.burgstaller:okhttp-digest:1.12'
|
||||||
|
|
||||||
// Material-ish things
|
// Material-ish things
|
||||||
compile 'com.roughike:bottom-bar:2.2.0'
|
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.3'
|
||||||
compile 'com.melnykov:floatingactionbutton:1.3.0'
|
implementation 'com.github.jd-alexander:LikeButton:0.2.1'
|
||||||
compile 'com.github.jd-alexander:LikeButton:0.2.1'
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
|
||||||
compile 'org.sufficientlysecure:html-textview:3.3'
|
|
||||||
|
|
||||||
// glide
|
// glide
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
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
|
// Asking politely users to rate the app
|
||||||
compile 'com.github.stkent:amplify:1.5.0'
|
implementation 'com.github.stkent:amplify:2.1.0'
|
||||||
|
|
||||||
// For the article reader
|
// Drawer
|
||||||
compile 'com.klinkerapps:drag-dismiss-activity:1.4.0'
|
implementation 'co.zsmb:materialdrawer-kt:1.3.5'
|
||||||
|
implementation 'com.anupcowkur:reservoir:3.1.0'
|
||||||
|
|
||||||
|
// Themes
|
||||||
|
implementation 'com.52inc:scoops:1.0.0'
|
||||||
|
implementation 'com.jrummyapps:colorpicker:2.1.7'
|
||||||
|
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
|
||||||
|
|
||||||
|
// Pager
|
||||||
|
implementation 'me.relex:circleindicator:1.2.2@aar'
|
||||||
|
|
||||||
|
implementation 'androidx.core:core-ktx:0.3'
|
||||||
|
|
||||||
|
// Crash
|
||||||
|
implementation 'ch.acra:acra-http:5.1.3'
|
||||||
|
implementation 'ch.acra:acra-dialog:5.1.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
initAppLoginPropertiesIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
def initAppLoginPropertiesIfNeeded() {
|
||||||
|
def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties')
|
||||||
|
if (!propertiesFile.exists()) {
|
||||||
|
def commentMessage = "This is autogenerated local property from system environment to prevent key to be committed to source control."
|
||||||
apply plugin: 'com.google.gms.google-services'
|
ant.propertyfile(file: System.getProperty("user.home") + "/.gradle/gradle.properties", comment: commentMessage) {
|
||||||
|
entry(key: "appLoginUrl", value: System.getProperty("appLoginUrl"))
|
||||||
|
entry(key: "appLoginUsername", value: System.getProperty("appLoginUsername"))
|
||||||
|
entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
app/proguard-rules.pro
vendored
@ -48,7 +48,11 @@
|
|||||||
#}
|
#}
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn retrofit2.Platform$Java8
|
-dontwarn retrofit2.Platform$Java8
|
||||||
-keepattributes Signature
|
-keep class retrofit.** { *; }
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit.http.* <methods>;
|
||||||
|
}
|
||||||
|
-keepattributes *Annotation*,Signature
|
||||||
-keepattributes Exceptions
|
-keepattributes Exceptions
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn javax.annotation.Nullable
|
-dontwarn javax.annotation.Nullable
|
||||||
@ -56,4 +60,19 @@
|
|||||||
|
|
||||||
|
|
||||||
#Bottom bar lib
|
#Bottom bar lib
|
||||||
-dontwarn com.roughike.bottombar.**
|
-dontwarn com.roughike.bottombar.**
|
||||||
|
|
||||||
|
|
||||||
|
# self signed glidemodule
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn com.anupcowkur.reservoir.**
|
||||||
|
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
|
||||||
|
-keep class android.support.v7.widget.SearchView { *; }
|
@ -0,0 +1,3 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
// TODO: test source adding
|
@ -0,0 +1,100 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import 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.click
|
||||||
|
import android.support.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import android.support.test.espresso.action.ViewActions.pressBack
|
||||||
|
import android.support.test.espresso.action.ViewActions.pressKey
|
||||||
|
import android.support.test.espresso.action.ViewActions.typeText
|
||||||
|
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.isDisplayed
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.isRoot
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import android.support.test.rule.ActivityTestRule
|
||||||
|
import android.support.test.runner.AndroidJUnit4
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class HomeActivityEspressoTest {
|
||||||
|
lateinit var context: Context
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(HomeActivity::class.java, true, false)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun clearData() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val editor =
|
||||||
|
context
|
||||||
|
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
editor.clear()
|
||||||
|
|
||||||
|
editor.putString("url", BuildConfig.LOGIN_URL)
|
||||||
|
editor.putString("login", BuildConfig.LOGIN_USERNAME)
|
||||||
|
editor.putString("password", BuildConfig.LOGIN_PASSWORD)
|
||||||
|
|
||||||
|
editor.commit()
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun menuItems() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(
|
||||||
|
withMenu(
|
||||||
|
id = R.id.action_search,
|
||||||
|
titleId = R.string.menu_home_search
|
||||||
|
)
|
||||||
|
).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.search_bar)).check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
onView(withId(R.id.search_src_text)).perform(
|
||||||
|
typeText("android"),
|
||||||
|
pressKey(KeyEvent.KEYCODE_SEARCH),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withText(R.string.action_disconnect)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name), times(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test articles opening and actions for cards and lists
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
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.hasComponent
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import android.support.test.rule.ActivityTestRule
|
||||||
|
import android.support.test.runner.AndroidJUnit4
|
||||||
|
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 java.util.*
|
||||||
|
|
||||||
|
@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))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
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.click
|
||||||
|
import android.support.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import android.support.test.espresso.action.ViewActions.pressBack
|
||||||
|
import android.support.test.espresso.action.ViewActions.typeText
|
||||||
|
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.isRoot
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import android.support.test.rule.ActivityTestRule
|
||||||
|
import android.support.test.runner.AndroidJUnit4
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import com.mikepenz.aboutlibraries.ui.LibsActivity
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class LoginActivityEspressoTest {
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(LoginActivity::class.java, true, false)
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var url: String
|
||||||
|
private lateinit var username: String
|
||||||
|
private lateinit var password: String
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val editor =
|
||||||
|
context
|
||||||
|
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
editor.clear()
|
||||||
|
editor.commit()
|
||||||
|
|
||||||
|
|
||||||
|
url = BuildConfig.LOGIN_URL
|
||||||
|
username = BuildConfig.LOGIN_USERNAME
|
||||||
|
password = BuildConfig.LOGIN_PASSWORD
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun menuItems() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
openActionBarOverflowOrOptionsMenu(context)
|
||||||
|
|
||||||
|
onView(withText(R.string.action_about)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(LibsActivity::class.java.name), times(1))
|
||||||
|
|
||||||
|
onView(isRoot()).perform(pressBack())
|
||||||
|
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongLoginUrl() {
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginProgress))
|
||||||
|
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests for multiple false urls with dialog
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyAuthData() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordLayout)).check(
|
||||||
|
matches(
|
||||||
|
isHintOrErrorEnabled()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun wrongAuthData() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||||
|
typeText("WRONGPASS"),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun workingAuth() {
|
||||||
|
|
||||||
|
rule.launchActivity(Intent())
|
||||||
|
|
||||||
|
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withId(R.id.withLogin)).perform(click())
|
||||||
|
|
||||||
|
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||||
|
typeText(username),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||||
|
typeText(password),
|
||||||
|
closeSoftKeyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withId(R.id.signInButton)).perform(click())
|
||||||
|
|
||||||
|
intended(hasComponent(HomeActivity::class.java.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
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 org.junit.After
|
||||||
|
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MainActivityEspressoTest {
|
||||||
|
|
||||||
|
lateinit var intent: Intent
|
||||||
|
lateinit var preferencesEditor: SharedPreferences.Editor
|
||||||
|
|
||||||
|
@Rule @JvmField
|
||||||
|
val rule = ActivityTestRule(MainActivity::class.java, true, false)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
intent = Intent()
|
||||||
|
val context = getInstrumentation().targetContext
|
||||||
|
|
||||||
|
// create a SharedPreferences editor
|
||||||
|
preferencesEditor = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||||
|
|
||||||
|
Intents.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkFirstOpenLaunchesIntro() {
|
||||||
|
preferencesEditor.putBoolean("firstStart", true)
|
||||||
|
preferencesEditor.commit()
|
||||||
|
|
||||||
|
rule.launchActivity(intent)
|
||||||
|
|
||||||
|
intended(hasComponent(MainActivity::class.java.name))
|
||||||
|
intended(hasComponent(IntroActivity::class.java.name))
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name), times(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkNotFirstOpenLaunchesLogin() {
|
||||||
|
preferencesEditor.putBoolean("firstStart", false)
|
||||||
|
preferencesEditor.commit()
|
||||||
|
|
||||||
|
rule.launchActivity(intent)
|
||||||
|
|
||||||
|
intended(hasComponent(MainActivity::class.java.name))
|
||||||
|
intended(hasComponent(LoginActivity::class.java.name))
|
||||||
|
intended(hasComponent(IntroActivity::class.java.name), times(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun releaseIntents() {
|
||||||
|
Intents.release()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.support.design.widget.TextInputLayout
|
||||||
|
import android.support.test.espresso.matcher.ViewMatchers
|
||||||
|
import android.view.View
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.hamcrest.TypeSafeMatcher
|
||||||
|
|
||||||
|
fun isHintOrErrorEnabled(): Matcher<View> =
|
||||||
|
object : TypeSafeMatcher<View>() {
|
||||||
|
override fun describeTo(description: Description?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: View?): Boolean {
|
||||||
|
if (item !is TextInputLayout) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.isHintEnabled || item.isErrorEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withMenu(id: Int, titleId: Int): Matcher<View> =
|
||||||
|
Matchers.anyOf(
|
||||||
|
ViewMatchers.withId(id),
|
||||||
|
ViewMatchers.withText(titleId)
|
||||||
|
)
|
@ -1,20 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="apps.amine.bou.readerforselfoss">
|
package="apps.amine.bou.readerforselfoss"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<!-- For firebase only -->
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MyApp"
|
android:name=".MyApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/NoBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme">
|
||||||
@ -28,7 +25,8 @@
|
|||||||
android:name=".IntroActivity"
|
android:name=".IntroActivity"
|
||||||
android:theme="@style/Theme.Intro">
|
android:theme="@style/Theme.Intro">
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".LoginActivity"
|
<activity
|
||||||
|
android:name=".LoginActivity"
|
||||||
android:label="@string/title_activity_login">
|
android:label="@string/title_activity_login">
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".HomeActivity">
|
<activity android:name=".HomeActivity">
|
||||||
@ -41,13 +39,15 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
|
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".SourcesActivity"
|
<activity
|
||||||
|
android:name=".SourcesActivity"
|
||||||
android:parentActivityName=".HomeActivity">
|
android:parentActivityName=".HomeActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value=".HomeActivity" />
|
android:value=".HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".AddSourceActivity"
|
<activity
|
||||||
|
android:name=".AddSourceActivity"
|
||||||
android:parentActivityName=".SourcesActivity">
|
android:parentActivityName=".SourcesActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
@ -61,9 +61,21 @@
|
|||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ReaderActivity"
|
<activity
|
||||||
android:theme="@style/DragDismissTheme">
|
android:name=".ReaderActivity">
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
BIN
app/src/main/ic_launcher-web.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,53 +1,133 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.support.constraint.ConstraintLayout
|
import android.support.constraint.ConstraintLayout
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
|
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
|
import kotlinx.android.synthetic.main.activity_add_source.*
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AddSourceActivity : AppCompatActivity() {
|
class AddSourceActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var mSpoutsValue: String? = null
|
private var mSpoutsValue: String? = null
|
||||||
|
private lateinit var api: SelfossApi
|
||||||
|
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@AddSourceActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_source)
|
setContentView(R.layout.activity_add_source)
|
||||||
|
|
||||||
val mProgress = findViewById(R.id.progress) as ProgressBar
|
val scoop = Scoop.getInstance()
|
||||||
val mForm = findViewById(R.id.formContainer) as ConstraintLayout
|
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
|
||||||
val mNameInput = findViewById(R.id.nameInput) as EditText
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val mSourceUri = findViewById(R.id.sourceUri) as EditText
|
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||||
val mTags = findViewById(R.id.tags) as EditText
|
|
||||||
val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner
|
|
||||||
val mSaveBtn = findViewById(R.id.saveBtn) as Button
|
|
||||||
val api = SelfossApi(this)
|
|
||||||
|
|
||||||
|
|
||||||
val intent = intent
|
|
||||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
|
||||||
mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
|
||||||
mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api) }
|
val 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)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@AddSourceActivity,
|
||||||
|
prefs.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getBoolean("should_log_everything", false)
|
||||||
|
)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
mustLoginToAddSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput)
|
||||||
|
|
||||||
|
saveBtn.setTextColor(appColors.colorAccent)
|
||||||
|
|
||||||
|
saveBtn.setOnClickListener {
|
||||||
|
handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val config = Config(this)
|
||||||
|
|
||||||
|
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid()) {
|
||||||
|
mustLoginToAddSource()
|
||||||
|
} else {
|
||||||
|
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSpoutsSpinner(
|
||||||
|
spoutsSpinner: Spinner,
|
||||||
|
api: SelfossApi?,
|
||||||
|
mProgress: ProgressBar,
|
||||||
|
formContainer: ConstraintLayout
|
||||||
|
) {
|
||||||
val spoutsKV = HashMap<String, String>()
|
val spoutsKV = HashMap<String, String>()
|
||||||
mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
|
||||||
val spoutName = (view as TextView).text.toString()
|
if (view != null) {
|
||||||
mSpoutsValue = spoutsKV[spoutName]
|
val spoutName = (view as TextView).text.toString()
|
||||||
|
mSpoutsValue = spoutsKV[spoutName]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||||
@ -55,66 +135,105 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = Config(this)
|
var items: Map<String, Spout>
|
||||||
|
api!!.spouts().enqueue(object : Callback<Map<String, Spout>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<Map<String, Spout>>,
|
||||||
|
response: Response<Map<String, Spout>>
|
||||||
|
) {
|
||||||
|
if (response.body() != null) {
|
||||||
|
items = response.body()!!
|
||||||
|
|
||||||
if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) {
|
val itemsStrings = items.map { it.value.name }
|
||||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
for ((key, value) in items) {
|
||||||
val i = Intent(this, LoginActivity::class.java)
|
spoutsKV[value.name] = key
|
||||||
startActivity(i)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
|
|
||||||
var items: Map<String, Spout>
|
|
||||||
api.spouts().enqueue(object : Callback<Map<String, Spout>> {
|
|
||||||
override fun onResponse(call: Call<Map<String, Spout>>, response: Response<Map<String, Spout>>) {
|
|
||||||
if (response.body() != null) {
|
|
||||||
items = response.body()!!
|
|
||||||
|
|
||||||
val itemsStrings = items.map { it.value.name }
|
|
||||||
for ((key, value) in items) {
|
|
||||||
spoutsKV.put(value.name, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgress.visibility = View.GONE
|
|
||||||
mForm.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val spinnerArrayAdapter = ArrayAdapter(this@AddSourceActivity, android.R.layout.simple_spinner_item, itemsStrings)
|
|
||||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
mSpoutsSpinner.adapter = spinnerArrayAdapter
|
|
||||||
|
|
||||||
} else {
|
|
||||||
handleProblemWithSpouts()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
mProgress.visibility = View.GONE
|
||||||
|
formContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val spinnerArrayAdapter =
|
||||||
|
ArrayAdapter(
|
||||||
|
this@AddSourceActivity,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
itemsStrings
|
||||||
|
)
|
||||||
|
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
spoutsSpinner.adapter = spinnerArrayAdapter
|
||||||
|
} else {
|
||||||
handleProblemWithSpouts()
|
handleProblemWithSpouts()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleProblemWithSpouts() {
|
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
||||||
Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
|
handleProblemWithSpouts()
|
||||||
mProgress.visibility = View.GONE
|
}
|
||||||
}
|
|
||||||
})
|
private fun handleProblemWithSpouts() {
|
||||||
|
Toast.makeText(
|
||||||
|
this@AddSourceActivity,
|
||||||
|
R.string.cant_get_spouts,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
mProgress.visibility = View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeGetDetailsFromIntentSharing(
|
||||||
|
intent: Intent,
|
||||||
|
sourceUri: EditText,
|
||||||
|
nameInput: EditText
|
||||||
|
) {
|
||||||
|
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
||||||
|
sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||||
|
nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
|
private fun mustLoginToAddSource() {
|
||||||
|
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
||||||
|
val i = Intent(this, LoginActivity::class.java)
|
||||||
|
startActivity(i)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
|
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
|
||||||
|
|
||||||
|
val sourceDetailsAvailable =
|
||||||
|
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
|
||||||
|
|
||||||
|
if (sourceDetailsAvailable) {
|
||||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
api.createSource(title, url, mSpoutsValue!!, mTags.text.toString(), "").enqueue(object : Callback<SuccessResponse> {
|
api.createSource(
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
title,
|
||||||
|
url,
|
||||||
|
mSpoutsValue!!,
|
||||||
|
tags.text.toString(),
|
||||||
|
""
|
||||||
|
).enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
if (response.body() != null && response.body()!!.isSuccess) {
|
if (response.body() != null && response.body()!!.isSuccess) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} 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) {
|
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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -7,41 +7,54 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.v7.app.AppCompatDelegate
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
|
|
||||||
class IntroActivity : MaterialIntroActivity() {
|
class IntroActivity : MaterialIntroActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
|
|
||||||
|
addSlide(
|
||||||
|
SlideFragmentBuilder()
|
||||||
.backgroundColor(R.color.colorPrimary)
|
.backgroundColor(R.color.colorPrimary)
|
||||||
.buttonsColor(R.color.colorAccent)
|
.buttonsColor(R.color.colorAccent)
|
||||||
.image(R.mipmap.ic_launcher)
|
.image(R.drawable.web_hi_res_512)
|
||||||
.title(getString(R.string.intro_hello_title))
|
.title(getString(R.string.intro_hello_title))
|
||||||
.description(getString(R.string.intro_hello_message))
|
.description(getString(R.string.intro_hello_message))
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
addSlide(
|
||||||
|
SlideFragmentBuilder()
|
||||||
.backgroundColor(R.color.colorAccent)
|
.backgroundColor(R.color.colorAccent)
|
||||||
.buttonsColor(R.color.colorPrimary)
|
.buttonsColor(R.color.colorPrimary)
|
||||||
.image(R.drawable.ic_info_outline_white_48dp)
|
.image(R.drawable.ic_info_outline_white_48px)
|
||||||
.title(getString(R.string.intro_needs_selfoss_title))
|
.title(getString(R.string.intro_needs_selfoss_title))
|
||||||
.description(getString(R.string.intro_needs_selfoss_message))
|
.description(getString(R.string.intro_needs_selfoss_message))
|
||||||
.build(),
|
.build(),
|
||||||
MessageButtonBehaviour(View.OnClickListener {
|
MessageButtonBehaviour(
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://selfoss.aditu.de"))
|
View.OnClickListener {
|
||||||
|
val browserIntent = Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://selfoss.aditu.de")
|
||||||
|
)
|
||||||
startActivity(browserIntent)
|
startActivity(browserIntent)
|
||||||
}, getString(R.string.intro_needs_selfoss_link)))
|
}, getString(R.string.intro_needs_selfoss_link)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
addSlide(SlideFragmentBuilder()
|
addSlide(
|
||||||
|
SlideFragmentBuilder()
|
||||||
.backgroundColor(R.color.colorPrimaryDark)
|
.backgroundColor(R.color.colorPrimaryDark)
|
||||||
.buttonsColor(R.color.colorAccentDark)
|
.buttonsColor(R.color.colorAccentDark)
|
||||||
.image(R.drawable.ic_thumb_up_white_48dp)
|
.image(R.drawable.ic_thumb_up_white_48px)
|
||||||
.title(getString(R.string.intro_all_set_title))
|
.title(getString(R.string.intro_all_set_title))
|
||||||
.description(getString(R.string.intro_all_set_message))
|
.description(getString(R.string.intro_all_set_message))
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
|
@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.TextInputLayout
|
|
||||||
import android.support.v7.app.AlertDialog
|
import android.support.v7.app.AlertDialog
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@ -14,106 +13,109 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Switch
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
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.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
|
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
|
||||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
|
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
|
||||||
import com.mikepenz.aboutlibraries.Libs
|
import com.mikepenz.aboutlibraries.Libs
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||||
|
import kotlinx.android.synthetic.main.activity_login.*
|
||||||
|
import org.acra.ACRA
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var settings: SharedPreferences? = null
|
|
||||||
private var mProgressView: View? = null
|
|
||||||
private var mUrlView: EditText? = null
|
|
||||||
private var mLoginView: TextView? = null
|
|
||||||
private var mHTTPLoginView: TextView? = null
|
|
||||||
private var mPasswordView: EditText? = null
|
|
||||||
private var mHTTPPasswordView: EditText? = null
|
|
||||||
private var inValidCount: Int = 0
|
private var inValidCount: Int = 0
|
||||||
|
private var isWithSelfSignedCert = false
|
||||||
private var isWithLogin = false
|
private var isWithLogin = false
|
||||||
private var isWithHTTPLogin = false
|
private var isWithHTTPLogin = false
|
||||||
private var mLoginFormView: View? = null
|
|
||||||
private var mFirebaseAnalytics: FirebaseAnalytics? = null
|
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var settings: SharedPreferences
|
||||||
|
private lateinit var editor: SharedPreferences.Editor
|
||||||
|
private lateinit var userIdentifier: String
|
||||||
|
private var logErrors: Boolean = false
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@LoginActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_login)
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
handleBaseUrlFail()
|
||||||
|
|
||||||
|
|
||||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
if (settings!!.getString("url", "").isNotEmpty()) {
|
userIdentifier = settings.getString("unique_id", "")
|
||||||
|
logErrors = settings.getBoolean("login_debug", false)
|
||||||
|
|
||||||
|
editor = settings.edit()
|
||||||
|
|
||||||
|
if (settings.getString("url", "").isNotEmpty()) {
|
||||||
goToMain()
|
goToMain()
|
||||||
} else {
|
|
||||||
checkAndDisplayStoreApk(this@LoginActivity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isWithLogin = false
|
handleActions()
|
||||||
isWithHTTPLogin = false
|
}
|
||||||
inValidCount = 0
|
|
||||||
|
|
||||||
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
|
private fun handleActions() {
|
||||||
mUrlView = findViewById(R.id.url) as EditText
|
|
||||||
mLoginView = findViewById(R.id.login) as TextView
|
|
||||||
mHTTPLoginView = findViewById(R.id.httpLogin) as TextView
|
|
||||||
mPasswordView = findViewById(R.id.password) as EditText
|
|
||||||
mHTTPPasswordView = findViewById(R.id.httpPassword) as EditText
|
|
||||||
mLoginFormView = findViewById(R.id.login_form)
|
|
||||||
mProgressView = findViewById(R.id.login_progress)
|
|
||||||
|
|
||||||
val mSwitch = findViewById(R.id.withLogin) as Switch
|
withSelfhostedCert.setOnCheckedChangeListener { _, b ->
|
||||||
val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch
|
isWithSelfSignedCert = !isWithSelfSignedCert
|
||||||
val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
val mHTTPLoginLayout = findViewById(R.id.httpLoginInput) as TextInputLayout
|
|
||||||
val mPasswordLayout = findViewById(R.id.passwordLayout) as TextInputLayout
|
|
||||||
val mHTTPPasswordLayout = findViewById(R.id.httpPasswordInput) as TextInputLayout
|
|
||||||
val mEmailSignInButton = findViewById(R.id.email_sign_in_button) as Button
|
|
||||||
|
|
||||||
mPasswordView!!.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
|
warningText.visibility = visi
|
||||||
if (id == R.id.login || id == EditorInfo.IME_NULL) {
|
}
|
||||||
attemptLogin()
|
|
||||||
return@OnEditorActionListener true
|
passwordView.setOnEditorActionListener(
|
||||||
|
TextView.OnEditorActionListener { _, id, _ ->
|
||||||
|
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
|
||||||
|
attemptLogin()
|
||||||
|
return@OnEditorActionListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
false
|
)
|
||||||
})
|
|
||||||
|
|
||||||
mEmailSignInButton.setOnClickListener { attemptLogin() }
|
signInButton.setOnClickListener { attemptLogin() }
|
||||||
|
|
||||||
mSwitch.setOnCheckedChangeListener { _, b ->
|
withLogin.setOnCheckedChangeListener { _, b ->
|
||||||
isWithLogin = !isWithLogin
|
isWithLogin = !isWithLogin
|
||||||
val visi: Int
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
loginLayout.visibility = visi
|
||||||
visi = View.GONE
|
passwordLayout.visibility = visi
|
||||||
}
|
|
||||||
mLoginLayout.visibility = visi
|
|
||||||
mPasswordLayout.visibility = visi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mHTTPSwitch.setOnCheckedChangeListener { _, b ->
|
withHttpLogin.setOnCheckedChangeListener { _, b ->
|
||||||
isWithHTTPLogin = !isWithHTTPLogin
|
isWithHTTPLogin = !isWithHTTPLogin
|
||||||
val visi: Int
|
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||||
if (b) {
|
|
||||||
visi = View.VISIBLE
|
|
||||||
|
|
||||||
} else {
|
httpLoginInput.visibility = visi
|
||||||
visi = View.GONE
|
httpPasswordInput.visibility = visi
|
||||||
}
|
}
|
||||||
mHTTPLoginLayout.visibility = visi
|
}
|
||||||
mHTTPPasswordLayout.visibility = visi
|
|
||||||
|
private fun handleBaseUrlFail() {
|
||||||
|
if (intent.getBooleanExtra("baseUrlFail", false)) {
|
||||||
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
|
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||||
|
alertDialog.setMessage(getString(R.string.base_url_error))
|
||||||
|
alertDialog.setButton(
|
||||||
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
"OK",
|
||||||
|
{ dialog, _ -> dialog.dismiss() }
|
||||||
|
)
|
||||||
|
alertDialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,33 +128,36 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
private fun attemptLogin() {
|
private fun attemptLogin() {
|
||||||
|
|
||||||
// Reset errors.
|
// Reset errors.
|
||||||
mUrlView!!.error = null
|
urlView.error = null
|
||||||
mLoginView!!.error = null
|
loginView.error = null
|
||||||
mHTTPLoginView!!.error = null
|
httpLoginView.error = null
|
||||||
mPasswordView!!.error = null
|
passwordView.error = null
|
||||||
mHTTPPasswordView!!.error = null
|
httpPasswordView.error = null
|
||||||
|
|
||||||
// Store values at the time of the login attempt.
|
// Store values at the time of the login attempt.
|
||||||
val url = mUrlView!!.text.toString()
|
val url = urlView.text.toString()
|
||||||
val login = mLoginView!!.text.toString()
|
val login = loginView.text.toString()
|
||||||
val httpLogin = mHTTPLoginView!!.text.toString()
|
val httpLogin = httpLoginView.text.toString()
|
||||||
val password = mPasswordView!!.text.toString()
|
val password = passwordView.text.toString()
|
||||||
val httpPassword = mHTTPPasswordView!!.text.toString()
|
val httpPassword = httpPasswordView.text.toString()
|
||||||
|
|
||||||
var cancel = false
|
var cancel = false
|
||||||
var focusView: View? = null
|
var focusView: View? = null
|
||||||
|
|
||||||
if (!isUrlValid(url)) {
|
if (!url.isBaseUrlValid()) {
|
||||||
mUrlView!!.error = getString(R.string.login_url_problem)
|
urlView.error = getString(R.string.login_url_problem)
|
||||||
focusView = mUrlView
|
focusView = urlView
|
||||||
cancel = true
|
cancel = true
|
||||||
inValidCount++
|
inValidCount++
|
||||||
if (inValidCount == 3) {
|
if (inValidCount == 3) {
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
alertDialog.setButton(
|
||||||
{ dialog, _ -> dialog.dismiss() })
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
"OK",
|
||||||
|
{ dialog, _ -> dialog.dismiss() }
|
||||||
|
)
|
||||||
alertDialog.show()
|
alertDialog.show()
|
||||||
inValidCount = 0
|
inValidCount = 0
|
||||||
}
|
}
|
||||||
@ -160,90 +165,112 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (isWithLogin || isWithHTTPLogin) {
|
if (isWithLogin || isWithHTTPLogin) {
|
||||||
if (TextUtils.isEmpty(password)) {
|
if (TextUtils.isEmpty(password)) {
|
||||||
mPasswordView!!.error = getString(R.string.error_invalid_password)
|
passwordView.error = getString(R.string.error_invalid_password)
|
||||||
focusView = mPasswordView
|
focusView = passwordView
|
||||||
cancel = true
|
cancel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(login)) {
|
if (TextUtils.isEmpty(login)) {
|
||||||
mLoginView!!.error = getString(R.string.error_field_required)
|
loginView.error = getString(R.string.error_field_required)
|
||||||
focusView = mLoginView
|
focusView = loginView
|
||||||
cancel = true
|
cancel = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancel) {
|
if (cancel) {
|
||||||
focusView!!.requestFocus()
|
focusView?.requestFocus()
|
||||||
} else {
|
} else {
|
||||||
showProgress(true)
|
showProgress(true)
|
||||||
|
|
||||||
val editor = settings!!.edit()
|
|
||||||
editor.putString("url", url)
|
editor.putString("url", url)
|
||||||
editor.putString("login", login)
|
editor.putString("login", login)
|
||||||
editor.putString("httpUserName", httpLogin)
|
editor.putString("httpUserName", httpLogin)
|
||||||
editor.putString("password", password)
|
editor.putString("password", password)
|
||||||
editor.putString("httpPassword", httpPassword)
|
editor.putString("httpPassword", httpPassword)
|
||||||
|
editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
|
||||||
editor.apply()
|
editor.apply()
|
||||||
|
|
||||||
val api = SelfossApi(this@LoginActivity)
|
val api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@LoginActivity,
|
||||||
|
isWithSelfSignedCert,
|
||||||
|
isWithSelfSignedCert
|
||||||
|
)
|
||||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
api.login().enqueue(object : Callback<SuccessResponse> {
|
||||||
private fun preferenceError() {
|
private fun preferenceError(t: Throwable) {
|
||||||
editor.remove("url")
|
editor.remove("url")
|
||||||
editor.remove("login")
|
editor.remove("login")
|
||||||
editor.remove("httpUserName")
|
editor.remove("httpUserName")
|
||||||
editor.remove("password")
|
editor.remove("password")
|
||||||
editor.remove("httpPassword")
|
editor.remove("httpPassword")
|
||||||
editor.apply()
|
editor.apply()
|
||||||
mUrlView!!.error = getString(R.string.wrong_infos)
|
urlView.error = getString(R.string.wrong_infos)
|
||||||
mLoginView!!.error = getString(R.string.wrong_infos)
|
loginView.error = getString(R.string.wrong_infos)
|
||||||
mPasswordView!!.error = getString(R.string.wrong_infos)
|
passwordView.error = getString(R.string.wrong_infos)
|
||||||
mHTTPLoginView!!.error = getString(R.string.wrong_infos)
|
httpLoginView.error = getString(R.string.wrong_infos)
|
||||||
mHTTPPasswordView!!.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)
|
showProgress(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (response.body() != null && response.body()!!.isSuccess) {
|
||||||
mFirebaseAnalytics!!.logEvent(FirebaseAnalytics.Event.LOGIN, Bundle())
|
|
||||||
goToMain()
|
goToMain()
|
||||||
} else {
|
} else {
|
||||||
preferenceError()
|
preferenceError(Exception("No response body..."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
preferenceError()
|
preferenceError(t)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress UI and hides the login form.
|
|
||||||
*/
|
|
||||||
private fun showProgress(show: Boolean) {
|
private fun showProgress(show: Boolean) {
|
||||||
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||||
|
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
loginForm
|
||||||
if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() {
|
.animate()
|
||||||
|
.setDuration(shortAnimTime.toLong())
|
||||||
|
.alpha(
|
||||||
|
if (show) 0F else 1F
|
||||||
|
).setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
|
loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
|
loginProgress
|
||||||
if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() {
|
.animate()
|
||||||
|
.setDuration(shortAnimTime.toLong())
|
||||||
|
.alpha(
|
||||||
|
if (show) 1F else 0F
|
||||||
|
).setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
|
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
val inflater = menuInflater
|
menuInflater.inflate(R.menu.login_menu, menu)
|
||||||
inflater.inflate(R.menu.login_menu, menu)
|
menu.findItem(R.id.login_debug).isChecked = logErrors
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,10 +278,18 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.about -> {
|
R.id.about -> {
|
||||||
LibsBuilder()
|
LibsBuilder()
|
||||||
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
|
||||||
.withAboutIconShown(true)
|
.withAboutIconShown(true)
|
||||||
.withAboutVersionShown(true)
|
.withAboutVersionShown(true)
|
||||||
.start(this)
|
.start(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.login_debug -> {
|
||||||
|
val newState = !item.isChecked
|
||||||
|
item.isChecked = newState
|
||||||
|
logErrors = newState
|
||||||
|
editor.putBoolean("login_debug", newState)
|
||||||
|
editor.apply()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
@ -5,14 +5,16 @@ import android.os.Bundle
|
|||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
|
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean(
|
||||||
|
"firstStart",
|
||||||
|
true
|
||||||
|
)) {
|
||||||
val i = Intent(this@MainActivity, IntroActivity::class.java)
|
val i = Intent(this@MainActivity, IntroActivity::class.java)
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
} else {
|
} else {
|
||||||
@ -21,6 +23,5 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,136 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.support.multidex.MultiDexApplication
|
import android.support.multidex.MultiDexApplication
|
||||||
import com.crashlytics.android.Crashlytics
|
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.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.github.stkent.amplify.tracking.Amplify
|
||||||
import io.fabric.sdk.android.Fabric
|
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||||
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
|
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() {
|
class MyApp : MultiDexApplication() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (!BuildConfig.DEBUG)
|
|
||||||
Fabric.with(this, Crashlytics())
|
|
||||||
|
|
||||||
|
initAmplify()
|
||||||
|
|
||||||
|
initCache()
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
if (prefs.getString("unique_id", "").isEmpty()) {
|
||||||
|
val editor = prefs.edit()
|
||||||
|
editor.putString("unique_id", randomUUID().toString())
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
initDrawerImageLoader()
|
||||||
|
|
||||||
|
initTheme()
|
||||||
|
|
||||||
|
tryToHandleBug()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
Amplify.initSharedInstance(this)
|
||||||
.setFeedbackEmailAddress(getString(R.string.feedback_email))
|
.setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
|
||||||
.setAlwaysShow(BuildConfig.DEBUG)
|
.setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
|
||||||
.applyAllDefaultRules()
|
.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?
|
||||||
|
) {
|
||||||
|
Glide.with(imageView?.context)
|
||||||
|
.load(uri)
|
||||||
|
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel(imageView: ImageView?) {
|
||||||
|
Glide.with(imageView?.context).clear(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun placeholder(ctx: Context?, tag: String?): Drawable {
|
||||||
|
return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTheme() {
|
||||||
|
Scoop.waffleCone()
|
||||||
|
.addFlavor(getString(R.string.default_theme), R.style.NoBar, true)
|
||||||
|
.addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
|
||||||
|
.setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
|
||||||
|
.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryToHandleBug() {
|
||||||
|
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||||
|
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
|
||||||
|
it.toString().contains("android.view.ViewDebug")
|
||||||
|
}) {
|
||||||
|
Unit
|
||||||
|
} else {
|
||||||
|
oldHandler.uncaughtException(thread, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,84 +1,263 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.content.res.Resources
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.preference.PreferenceManager
|
||||||
import android.view.View
|
import android.support.v4.app.FragmentManager
|
||||||
|
import android.support.v4.app.FragmentStatePagerAdapter
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v4.view.ViewPager
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.Toast
|
||||||
import android.widget.TextView
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
import com.bumptech.glide.Glide
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
|
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
|
||||||
import org.sufficientlysecure.htmltextview.HtmlTextView
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||||
|
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.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
|
|
||||||
|
|
||||||
|
class ReaderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
class ReaderActivity : DragDismissActivity() {
|
private var markOnScroll: Boolean = false
|
||||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
private var debugReadingItems: Boolean = false
|
||||||
|
private var currentItem: Int = 0
|
||||||
|
private lateinit var userIdentifier: String
|
||||||
|
|
||||||
override fun onStart() {
|
private lateinit var api: SelfossApi
|
||||||
super.onStart()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
private lateinit var toolbarMenu: Menu
|
||||||
|
|
||||||
|
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||||
|
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
|
||||||
|
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
private fun canFavorite() {
|
||||||
super.onStop()
|
showMenuItem(true)
|
||||||
mCustomTabActivityHelper!!.unbindCustomTabsService(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
|
private fun canRemoveFromFavorite() {
|
||||||
val v = inflater.inflate(R.layout.activity_reader, parent, false)
|
showMenuItem(false)
|
||||||
showProgressBar()
|
}
|
||||||
|
|
||||||
val image = v.findViewById(R.id.imageView) as ImageView
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val source = v.findViewById(R.id.source) as TextView
|
super.onCreate(savedInstanceState)
|
||||||
val title = v.findViewById(R.id.title) as TextView
|
|
||||||
val content = v.findViewById(R.id.content) as HtmlTextView
|
|
||||||
val url = intent.getStringExtra("url")
|
|
||||||
val parser = MercuryApi(getString(R.string.mercury))
|
|
||||||
|
|
||||||
val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity)
|
setContentView(R.layout.activity_reader)
|
||||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
|
||||||
mCustomTabActivityHelper!!.bindCustomTabsService(this)
|
|
||||||
|
|
||||||
|
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> {
|
setSupportActionBar(toolBar)
|
||||||
override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
if (response.body() != null) {
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
source.text = response.body()!!.domain
|
|
||||||
title.text = response.body()!!.title
|
val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
if (response.body()!!.content != null && !response.body()!!.content.isEmpty())
|
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
content.setHtml(response.body()!!.content, HtmlHttpImageGetter(content, null, true))
|
|
||||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isEmpty())
|
debugReadingItems = sharedPref.getBoolean("read_debug", false)
|
||||||
Glide.with(applicationContext).load(response.body()!!.lead_image_url).asBitmap().fitCenter().into(image)
|
userIdentifier = sharedPref.getString("unique_id", "")
|
||||||
hideProgressBar()
|
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
|
||||||
} else {
|
|
||||||
errorAfterMercuryCall()
|
if (allItems.isEmpty()) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@ReaderActivity,
|
||||||
|
settings.getBoolean("isSelfSignedCert", false),
|
||||||
|
sharedPref.getBoolean("should_log_everything", false)
|
||||||
|
)
|
||||||
|
|
||||||
|
currentItem = intent.getIntExtra("currentItem", 0)
|
||||||
|
|
||||||
|
pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
|
||||||
|
pager.currentItem = currentItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
|
||||||
|
|
||||||
|
pager.setPageTransformer(true, DepthPageTransformer())
|
||||||
|
(indicator as CircleIndicator).setViewPager(pager)
|
||||||
|
|
||||||
|
pager.addOnPageChangeListener(
|
||||||
|
object : ViewPager.SimpleOnPageChangeListener() {
|
||||||
|
var isLastItem = false
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
isLastItem = (position === (allItems.size - 1))
|
||||||
|
|
||||||
|
if (allItems[position].starred) {
|
||||||
|
canRemoveFromFavorite()
|
||||||
|
} else {
|
||||||
|
canFavorite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
|
if (markOnScroll && (state === ViewPager.SCROLL_STATE_DRAGGING || (state === ViewPager.SCROLL_STATE_IDLE && isLastItem))) {
|
||||||
|
api.markItem(allItems[pager.currentItem].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
|
||||||
|
) {
|
||||||
|
if (debugReadingItems) {
|
||||||
|
ACRA.getErrorReporter().maybeHandleSilentException(t, this@ReaderActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<ParsedContent>, t: Throwable) {
|
override fun onPause() {
|
||||||
errorAfterMercuryCall()
|
super.onPause()
|
||||||
}
|
if (markOnScroll) {
|
||||||
|
pager.clearOnPageChangeListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun errorAfterMercuryCall() {
|
override fun onSaveInstanceState(oldInstanceState: Bundle?) {
|
||||||
CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url)
|
super.onSaveInstanceState(oldInstanceState)
|
||||||
) { _, uri ->
|
oldInstanceState!!.clear()
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
}
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
startActivity(intent)
|
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
|
||||||
}
|
FragmentStatePagerAdapter(fm) {
|
||||||
finish()
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
})
|
R.id.save -> {
|
||||||
return v
|
api.starrItem(allItems[pager.currentItem].id)
|
||||||
|
.enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
|
||||||
|
canRemoveFromFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
Toast.makeText(
|
||||||
|
baseContext,
|
||||||
|
R.string.cant_mark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
R.id.unsave -> {
|
||||||
|
api.unstarrItem(allItems[pager.currentItem].id)
|
||||||
|
.enqueue(object : Callback<SuccessResponse> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
|
||||||
|
canFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
t: Throwable
|
||||||
|
) {
|
||||||
|
Toast.makeText(
|
||||||
|
baseContext,
|
||||||
|
R.string.cant_unmark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var allItems: ArrayList<Item> = ArrayList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,101 @@
|
|||||||
package apps.amine.bou.readerforselfoss
|
package apps.amine.bou.readerforselfoss
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
||||||
import com.melnykov.fab.FloatingActionButton
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.Toppings
|
||||||
|
import com.ftinc.scoop.Scoop
|
||||||
|
import kotlinx.android.synthetic.main.activity_sources.*
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
|
|
||||||
class SourcesActivity : AppCompatActivity() {
|
class SourcesActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
appColors = AppColors(this@SourcesActivity)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_sources)
|
setContentView(R.layout.activity_sources)
|
||||||
|
|
||||||
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
val mFab = findViewById(R.id.fab) as FloatingActionButton
|
|
||||||
val mRecyclerView = findViewById(R.id.activity_sources) as RecyclerView
|
|
||||||
val mLayoutManager = LinearLayoutManager(this)
|
val mLayoutManager = LinearLayoutManager(this)
|
||||||
val api = SelfossApi(this)
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
val api = SelfossApi(
|
||||||
|
this,
|
||||||
|
this@SourcesActivity,
|
||||||
|
prefs.getBoolean("isSelfSignedCert", false),
|
||||||
|
prefs.getBoolean("should_log_everything", false)
|
||||||
|
)
|
||||||
var items: ArrayList<Sources> = ArrayList()
|
var items: ArrayList<Sources> = ArrayList()
|
||||||
|
|
||||||
mFab.attachToRecyclerView(mRecyclerView)
|
recyclerView.setHasFixedSize(true)
|
||||||
mRecyclerView.setHasFixedSize(true)
|
recyclerView.layoutManager = mLayoutManager
|
||||||
mRecyclerView.layoutManager = mLayoutManager
|
|
||||||
|
|
||||||
api.sources.enqueue(object : Callback<List<Sources>> {
|
api.sources.enqueue(object : Callback<List<Sources>> {
|
||||||
override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
|
override fun onResponse(
|
||||||
|
call: Call<List<Sources>>,
|
||||||
|
response: Response<List<Sources>>
|
||||||
|
) {
|
||||||
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
||||||
items = response.body() as ArrayList<Sources>
|
items = response.body() as ArrayList<Sources>
|
||||||
}
|
}
|
||||||
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
|
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
|
||||||
mRecyclerView.adapter = mAdapter
|
recyclerView.adapter = mAdapter
|
||||||
mAdapter.notifyDataSetChanged()
|
mAdapter.notifyDataSetChanged()
|
||||||
if (items.isEmpty()) Toast.makeText(this@SourcesActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
|
if (items.isEmpty()) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@SourcesActivity,
|
||||||
|
R.string.nothing_here,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
|
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
|
||||||
Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
this@SourcesActivity,
|
||||||
|
R.string.cant_get_sources,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mFab.setOnClickListener {
|
fab.setOnClickListener {
|
||||||
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,59 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.support.v7.widget.CardView
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.constraint.ConstraintLayout
|
|
||||||
import android.support.design.widget.Snackbar
|
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ImageView.ScaleType
|
import android.widget.ImageView.ScaleType
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.shareLink
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import com.like.LikeButton
|
import com.like.LikeButton
|
||||||
import com.like.OnLikeListener
|
import com.like.OnLikeListener
|
||||||
|
import kotlinx.android.synthetic.main.card_item.view.*
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
class ItemCardAdapter(
|
||||||
class ItemCardAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
override val app: Activity,
|
||||||
private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean,
|
override var items: ArrayList<Item>,
|
||||||
private val articleViewer: Boolean, private val fullHeightCards: Boolean) : RecyclerView.Adapter<ItemCardAdapter.ViewHolder>() {
|
override val api: SelfossApi,
|
||||||
private val c: Context = app.applicationContext
|
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 generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
|
private val imageMaxHeight: Int =
|
||||||
|
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as ConstraintLayout
|
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
|
||||||
return ViewHolder(v)
|
return ViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,149 +61,76 @@ class ItemCardAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
val itm = items[position]
|
val itm = items[position]
|
||||||
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
holder.mView.favButton.isLiked = itm.starred
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
holder.mView.title.text = Html.fromHtml(itm.title)
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
holder.mView.title.setLinkTextColor(appColors.colorAccent)
|
||||||
val d: Long
|
|
||||||
try {
|
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
|
|
||||||
sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
|
if (!fullHeightCards) {
|
||||||
d,
|
holder.mView.itemImage.maxHeight = imageMaxHeight
|
||||||
Date().time,
|
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
|
||||||
DateUtils.MINUTE_IN_MILLIS,
|
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
|
||||||
)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.sourceTitleAndDate!!.text = sourceAndDate
|
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
if (itm.getThumbnail(c).isEmpty()) {
|
||||||
Glide.clear(holder.itemImage)
|
holder.mView.itemImage.visibility = View.GONE
|
||||||
holder.itemImage!!.setImageDrawable(null)
|
Glide.with(c).clear(holder.mView.itemImage)
|
||||||
|
holder.mView.itemImage.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
if (fullHeightCards) {
|
holder.mView.itemImage.visibility = View.VISIBLE
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage)
|
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
|
||||||
} else {
|
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
val color = generator.getColor(itm.sourcetitle)
|
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
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
.builder()
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
.round()
|
||||||
|
.build(itm.sourcetitle.toTextDrawableString(c), color)
|
||||||
|
holder.mView.sourceImage.setImageDrawable(drawable)
|
||||||
} else {
|
} else {
|
||||||
|
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
holder.mView.favButton.isLiked = itm.starred
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return items.size
|
return items.size
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doUnmark(i: Item, position: Int) {
|
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
|
||||||
val s = Snackbar
|
|
||||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.undo_string) {
|
|
||||||
items.add(position, i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
|
|
||||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val view = s.view
|
|
||||||
val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
|
||||||
tv.setTextColor(Color.WHITE)
|
|
||||||
s.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeItemAtIndex(position: Int) {
|
|
||||||
|
|
||||||
val i = items[position]
|
|
||||||
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
|
|
||||||
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
|
|
||||||
items.add(i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
|
||||||
var saveBtn: LikeButton? = null
|
|
||||||
var browserBtn: ImageButton? = null
|
|
||||||
var shareBtn: ImageButton? = null
|
|
||||||
var itemImage: ImageView? = null
|
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var title: TextView? = null
|
|
||||||
var sourceTitleAndDate: TextView? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
mView.setCardBackgroundColor(appColors.cardBackgroundColor)
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
handleCustomTabActions()
|
handleCustomTabActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
private fun handleClickListeners() {
|
||||||
sourceImage = mView.findViewById(R.id.sourceImage) as ImageView
|
|
||||||
itemImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
title = mView.findViewById(R.id.title) as TextView
|
|
||||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
|
|
||||||
saveBtn = mView.findViewById(R.id.favButton) as LikeButton
|
|
||||||
shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
|
|
||||||
browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
|
|
||||||
|
|
||||||
if (!fullHeightCards) {
|
mView.favButton.setOnLikeListener(object : OnLikeListener {
|
||||||
itemImage!!.maxHeight = c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
|
||||||
itemImage!!.scaleType = ScaleType.CENTER_CROP
|
|
||||||
}
|
|
||||||
|
|
||||||
saveBtn!!.setOnLikeListener(object : OnLikeListener {
|
|
||||||
override fun liked(likeButton: LikeButton) {
|
override fun liked(likeButton: LikeButton) {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[adapterPosition]
|
||||||
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(
|
||||||
saveBtn!!.isLiked = false
|
call: Call<SuccessResponse>,
|
||||||
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
|
t: Throwable
|
||||||
|
) {
|
||||||
|
mView.favButton.isLiked = false
|
||||||
|
Toast.makeText(
|
||||||
|
c,
|
||||||
|
R.string.cant_mark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -206,46 +138,50 @@ class ItemCardAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
override fun unLiked(likeButton: LikeButton) {
|
override fun unLiked(likeButton: LikeButton) {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[adapterPosition]
|
||||||
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(
|
||||||
saveBtn!!.isLiked = true
|
call: Call<SuccessResponse>,
|
||||||
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
|
t: Throwable
|
||||||
|
) {
|
||||||
|
mView.favButton.isLiked = true
|
||||||
|
Toast.makeText(
|
||||||
|
c,
|
||||||
|
R.string.cant_unmark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
shareBtn!!.setOnClickListener {
|
mView.shareBtn.setOnClickListener {
|
||||||
val i = items[adapterPosition]
|
c.shareLink(items[adapterPosition].getLinkDecoded())
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browserBtn!!.setOnClickListener {
|
mView.browserBtn.setOnClickListener {
|
||||||
val i = items[adapterPosition]
|
c.openInBrowserAsNewTask(items[adapterPosition])
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
|
||||||
c.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCustomTabActions() {
|
private fun handleCustomTabActions() {
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
val customTabsIntent = c.buildCustomTabsIntent()
|
||||||
helper.bindCustomTabsService(app)
|
helper.bindCustomTabsService(app)
|
||||||
|
|
||||||
mView.setOnClickListener {
|
mView.setOnClickListener {
|
||||||
openItemUrl(items[adapterPosition],
|
c.openItemUrl(
|
||||||
customTabsIntent,
|
items,
|
||||||
internalBrowser,
|
adapterPosition,
|
||||||
articleViewer,
|
items[adapterPosition].getLinkDecoded(),
|
||||||
app,
|
customTabsIntent,
|
||||||
c)
|
internalBrowser,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,63 @@
|
|||||||
package apps.amine.bou.readerforselfoss.adapters
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.constraint.ConstraintLayout
|
import android.support.constraint.ConstraintLayout
|
||||||
import android.support.design.widget.Snackbar
|
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors
|
||||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
|
||||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
import apps.amine.bou.readerforselfoss.utils.openItemUrl
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.shareLink
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import com.like.LikeButton
|
import com.like.LikeButton
|
||||||
import com.like.OnLikeListener
|
import com.like.OnLikeListener
|
||||||
|
import kotlinx.android.synthetic.main.list_item.view.*
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class ItemListAdapter(
|
||||||
class ItemListAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
|
override val app: Activity,
|
||||||
private val helper: CustomTabActivityHelper, private val clickBehavior: Boolean,
|
override var items: ArrayList<Item>,
|
||||||
private val internalBrowser: Boolean, private val articleViewer: Boolean) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {
|
override val api: SelfossApi,
|
||||||
|
private val helper: CustomTabActivityHelper,
|
||||||
|
private val clickBehavior: Boolean,
|
||||||
|
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 generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
private val c: Context = app.applicationContext
|
private val c: Context = app.baseContext
|
||||||
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
|
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.list_item, parent, false) as ConstraintLayout
|
val v = LayoutInflater.from(c).inflate(
|
||||||
|
R.layout.list_item,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
) as ConstraintLayout
|
||||||
return ViewHolder(v)
|
return ViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,136 +65,65 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
val itm = items[position]
|
val itm = items[position]
|
||||||
|
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
holder.mView.title.text = Html.fromHtml(itm.title)
|
||||||
holder.title!!.text = Html.fromHtml(itm.title)
|
|
||||||
|
|
||||||
var sourceAndDate = itm.sourcetitle
|
holder.mView.title.setLinkTextColor(appColors.colorAccent)
|
||||||
val d: Long
|
|
||||||
try {
|
|
||||||
d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
|
|
||||||
sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
|
|
||||||
d,
|
|
||||||
Date().time,
|
|
||||||
DateUtils.MINUTE_IN_MILLIS,
|
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
|
||||||
)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.sourceTitleAndDate!!.text = sourceAndDate
|
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
|
|
||||||
if (itm.getThumbnail(c).isEmpty()) {
|
if (itm.getThumbnail(c).isEmpty()) {
|
||||||
val sizeInInt = 46
|
val sizeInInt = 46
|
||||||
val sizeInDp = TypedValue.applyDimension(
|
val sizeInDp = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
|
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
|
||||||
.displayMetrics).toInt()
|
.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
val marginInInt = 16
|
val marginInInt = 16
|
||||||
val marginInDp = TypedValue.applyDimension(
|
val marginInDp = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
|
||||||
.displayMetrics).toInt()
|
.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
val params = holder.sourceImage!!.layoutParams as ViewGroup.MarginLayoutParams
|
val params = holder.mView.itemImage.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
params.height = sizeInDp
|
params.height = sizeInDp
|
||||||
params.width = sizeInDp
|
params.width = sizeInDp
|
||||||
params.setMargins(marginInDp, 0, 0, 0)
|
params.setMargins(marginInDp, 0, 0, 0)
|
||||||
holder.sourceImage!!.layoutParams = params
|
holder.mView.itemImage.layoutParams = params
|
||||||
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
val color = generator.getColor(itm.sourcetitle)
|
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.mView.itemImage.setImageDrawable(drawable)
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
|
||||||
} else {
|
} else {
|
||||||
|
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
|
||||||
val fHolder = holder
|
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.sourceImage)
|
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bars[position]) {
|
// TODO: maybe handle this differently. It crashes when changing tab
|
||||||
holder.actionBar!!.visibility = View.VISIBLE
|
try {
|
||||||
} else {
|
if (bars[position]) {
|
||||||
holder.actionBar!!.visibility = View.GONE
|
holder.mView.actionBar.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
holder.mView.actionBar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
holder.mView.actionBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.saveBtn!!.isLiked = itm.starred
|
holder.mView.favButton.isLiked = itm.starred
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = items.size
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun doUnmark(i: Item, position: Int) {
|
|
||||||
val s = Snackbar
|
|
||||||
.make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.undo_string) {
|
|
||||||
items.add(position, i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
|
|
||||||
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
doUnmark(i, position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val view = s.view
|
|
||||||
val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
|
||||||
tv.setTextColor(Color.WHITE)
|
|
||||||
s.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeItemAtIndex(position: Int) {
|
|
||||||
|
|
||||||
val i = items[position]
|
|
||||||
|
|
||||||
items.remove(i)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
|
|
||||||
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
|
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
|
|
||||||
doUnmark(i, position)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
|
||||||
Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
|
|
||||||
items.add(i)
|
|
||||||
notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||||
var saveBtn: LikeButton? = null
|
|
||||||
var browserBtn: ImageButton? = null
|
|
||||||
var shareBtn: ImageButton? = null
|
|
||||||
var actionBar: RelativeLayout? = null
|
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var title: TextView? = null
|
|
||||||
var sourceTitleAndDate: TextView? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
@ -192,24 +131,27 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
private fun handleClickListeners() {
|
||||||
actionBar = mView.findViewById(R.id.actionBar) as RelativeLayout
|
|
||||||
sourceImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
title = mView.findViewById(R.id.title) as TextView
|
|
||||||
sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
|
|
||||||
saveBtn = mView.findViewById(R.id.favButton) as LikeButton
|
|
||||||
shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
|
|
||||||
browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
|
|
||||||
|
|
||||||
|
mView.favButton.setOnLikeListener(object : OnLikeListener {
|
||||||
saveBtn!!.setOnLikeListener(object : OnLikeListener {
|
|
||||||
override fun liked(likeButton: LikeButton) {
|
override fun liked(likeButton: LikeButton) {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[adapterPosition]
|
||||||
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(
|
||||||
saveBtn!!.isLiked = false
|
call: Call<SuccessResponse>,
|
||||||
Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
|
t: Throwable
|
||||||
|
) {
|
||||||
|
mView.favButton.isLiked = false
|
||||||
|
Toast.makeText(
|
||||||
|
c,
|
||||||
|
R.string.cant_mark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -217,49 +159,53 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
override fun unLiked(likeButton: LikeButton) {
|
override fun unLiked(likeButton: LikeButton) {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[adapterPosition]
|
||||||
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
|
||||||
override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
|
override fun onResponse(
|
||||||
|
call: Call<SuccessResponse>,
|
||||||
|
response: Response<SuccessResponse>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(
|
||||||
saveBtn!!.isLiked = true
|
call: Call<SuccessResponse>,
|
||||||
Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
|
t: Throwable
|
||||||
|
) {
|
||||||
|
mView.favButton.isLiked = true
|
||||||
|
Toast.makeText(
|
||||||
|
c,
|
||||||
|
R.string.cant_unmark_favortie,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
shareBtn!!.setOnClickListener {
|
mView.shareBtn.setOnClickListener {
|
||||||
val i = items[adapterPosition]
|
c.shareLink(items[adapterPosition].getLinkDecoded())
|
||||||
val sendIntent = Intent()
|
|
||||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
|
|
||||||
sendIntent.type = "text/plain"
|
|
||||||
c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browserBtn!!.setOnClickListener {
|
mView.browserBtn.setOnClickListener {
|
||||||
val i = items[adapterPosition]
|
c.openInBrowserAsNewTask(items[adapterPosition])
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
|
||||||
c.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleCustomTabActions() {
|
private fun handleCustomTabActions() {
|
||||||
val customTabsIntent = buildCustomTabsIntent(c)
|
val customTabsIntent = c.buildCustomTabsIntent()
|
||||||
helper.bindCustomTabsService(app)
|
helper.bindCustomTabsService(app)
|
||||||
|
|
||||||
|
|
||||||
if (!clickBehavior) {
|
if (!clickBehavior) {
|
||||||
mView.setOnClickListener {
|
mView.setOnClickListener {
|
||||||
openItemUrl(items[adapterPosition],
|
c.openItemUrl(
|
||||||
customTabsIntent,
|
items,
|
||||||
internalBrowser,
|
adapterPosition,
|
||||||
articleViewer,
|
items[adapterPosition].getLinkDecoded(),
|
||||||
app,
|
customTabsIntent,
|
||||||
c)
|
internalBrowser,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
}
|
}
|
||||||
mView.setOnLongClickListener {
|
mView.setOnLongClickListener {
|
||||||
actionBarShowHide()
|
actionBarShowHide()
|
||||||
@ -268,12 +214,15 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
} else {
|
} else {
|
||||||
mView.setOnClickListener { actionBarShowHide() }
|
mView.setOnClickListener { actionBarShowHide() }
|
||||||
mView.setOnLongClickListener {
|
mView.setOnLongClickListener {
|
||||||
openItemUrl(items[adapterPosition],
|
c.openItemUrl(
|
||||||
customTabsIntent,
|
items,
|
||||||
internalBrowser,
|
adapterPosition,
|
||||||
articleViewer,
|
items[adapterPosition].getLinkDecoded(),
|
||||||
app,
|
customTabsIntent,
|
||||||
c)
|
internalBrowser,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,10 +230,11 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
|
|||||||
|
|
||||||
private fun actionBarShowHide() {
|
private fun actionBarShowHide() {
|
||||||
bars[adapterPosition] = true
|
bars[adapterPosition] = true
|
||||||
if (actionBar!!.visibility == View.GONE)
|
if (mView.actionBar.visibility == View.GONE) {
|
||||||
actionBar!!.visibility = View.VISIBLE
|
mView.actionBar.visibility = View.VISIBLE
|
||||||
else
|
} else {
|
||||||
actionBar!!.visibility = View.GONE
|
mView.actionBar.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.adapters
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
|
import android.support.v7.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.themes.AppColors
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.succeeded
|
||||||
|
import org.acra.ACRA
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
|
||||||
|
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
|
||||||
|
abstract var items: ArrayList<Item>
|
||||||
|
abstract val api: SelfossApi
|
||||||
|
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)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
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)
|
||||||
|
updateItems(items)
|
||||||
|
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)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemAtIndex(item: Item, position: Int) {
|
||||||
|
items.add(position, item)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemsAtEnd(newItems: List<Item>) {
|
||||||
|
val oldSize = items.size
|
||||||
|
items.addAll(newItems)
|
||||||
|
notifyItemRangeInserted(oldSize, newItems.size)
|
||||||
|
updateItems(items)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -2,104 +2,102 @@ package apps.amine.bou.readerforselfoss.adapters
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.support.constraint.ConstraintLayout
|
import android.support.constraint.ConstraintLayout
|
||||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
|
||||||
import com.amulyakhare.textdrawable.TextDrawable
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
import com.bumptech.glide.Glide
|
import kotlinx.android.synthetic.main.source_list_item.view.*
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class SourcesListAdapter(private val app: Activity, private val items: ArrayList<Sources>, private val api: SelfossApi) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
class SourcesListAdapter(
|
||||||
|
private val app: Activity,
|
||||||
|
private val items: ArrayList<Sources>,
|
||||||
|
private val api: SelfossApi
|
||||||
|
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
||||||
private val c: Context = app.baseContext
|
private val c: Context = app.baseContext
|
||||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val v = LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false) as ConstraintLayout
|
val v = LayoutInflater.from(c).inflate(
|
||||||
|
R.layout.source_list_item,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
) as ConstraintLayout
|
||||||
return ViewHolder(v)
|
return ViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val itm = items[position]
|
val itm = items[position]
|
||||||
|
|
||||||
val fHolder = holder
|
|
||||||
if (itm.getIcon(c).isEmpty()) {
|
if (itm.getIcon(c).isEmpty()) {
|
||||||
val color = generator.getColor(itm.title)
|
val color = generator.getColor(itm.title)
|
||||||
val textDrawable = StringBuilder()
|
|
||||||
for (s in itm.title.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
|
||||||
textDrawable.append(s[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = TextDrawable.builder().round()
|
val drawable =
|
||||||
|
TextDrawable
|
||||||
val drawable = builder.build(textDrawable.toString(), color)
|
.builder()
|
||||||
holder.sourceImage!!.setImageDrawable(drawable)
|
.round()
|
||||||
|
.build(itm.title.toTextDrawableString(c), color)
|
||||||
|
holder.mView.itemImage.setImageDrawable(drawable)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
|
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
|
||||||
override fun setResource(resource: Bitmap) {
|
|
||||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
|
|
||||||
circularBitmapDrawable.isCircular = true
|
|
||||||
fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.sourceTitle!!.text = itm.title
|
holder.mView.sourceTitle.text = itm.title
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = items.size
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||||
var sourceImage: ImageView? = null
|
|
||||||
var sourceTitle: TextView? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClickListeners() {
|
private fun handleClickListeners() {
|
||||||
sourceImage = mView.findViewById(R.id.itemImage) as ImageView
|
|
||||||
sourceTitle = mView.findViewById(R.id.sourceTitle) as TextView
|
|
||||||
|
|
||||||
val deleteBtn = mView.findViewById(R.id.deleteBtn) as Button
|
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||||
|
|
||||||
deleteBtn.setOnClickListener {
|
deleteBtn.setOnClickListener {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[adapterPosition]
|
||||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
api.deleteSource(id).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) {
|
if (response.body() != null && response.body()!!.isSuccess) {
|
||||||
items.removeAt(adapterPosition)
|
items.removeAt(adapterPosition)
|
||||||
notifyItemRemoved(adapterPosition)
|
notifyItemRemoved(adapterPosition)
|
||||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
R.string.can_delete_source,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||||
Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
app,
|
||||||
|
R.string.can_delete_source,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
package apps.amine.bou.readerforselfoss.api.mercury
|
||||||
|
|
||||||
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
@ -8,25 +7,33 @@ import retrofit2.Call
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
class MercuryApi(shouldLog: Boolean) {
|
||||||
class MercuryApi(private val key: String) {
|
|
||||||
private val service: MercuryService
|
private val service: MercuryService
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
val interceptor = HttpLoggingInterceptor()
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
interceptor.level = if (shouldLog) {
|
||||||
|
HttpLoggingInterceptor.Level.BODY
|
||||||
|
} else {
|
||||||
|
HttpLoggingInterceptor.Level.NONE
|
||||||
|
}
|
||||||
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
.setLenient()
|
.setLenient()
|
||||||
.create()
|
.create()
|
||||||
val retrofit = Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client)
|
val retrofit =
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.baseUrl("https://www.amine-bou.fr")
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
service = retrofit.create(MercuryService::class.java)
|
service = retrofit.create(MercuryService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseUrl(url: String): Call<ParsedContent> {
|
fun parseUrl(url: String): Call<ParsedContent> {
|
||||||
return service.parseUrl(url, this.key)
|
return service.parseUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,39 +2,43 @@ package apps.amine.bou.readerforselfoss.api.mercury
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class ParsedContent(
|
||||||
class ParsedContent(val title: String,
|
@SerializedName("title") val title: String,
|
||||||
val content: String,
|
@SerializedName("content") val content: String?,
|
||||||
val date_published: String,
|
@SerializedName("date_published") val date_published: String,
|
||||||
val lead_image_url: String,
|
@SerializedName("lead_image_url") val lead_image_url: String?,
|
||||||
val dek: String,
|
@SerializedName("dek") val dek: String,
|
||||||
val url: String,
|
@SerializedName("url") val url: String,
|
||||||
val domain: String,
|
@SerializedName("domain") val domain: String,
|
||||||
val excerpt: String,
|
@SerializedName("excerpt") val excerpt: String,
|
||||||
val total_pages: Int,
|
@SerializedName("total_pages") val total_pages: Int,
|
||||||
val rendered_pages: Int,
|
@SerializedName("rendered_pages") val rendered_pages: Int,
|
||||||
val next_page_url: String) : Parcelable {
|
@SerializedName("next_page_url") val next_page_url: String
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
|
@JvmField
|
||||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
val CREATOR: Parcelable.Creator<ParsedContent> =
|
||||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
object : Parcelable.Creator<ParsedContent> {
|
||||||
}
|
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
||||||
|
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: Parcel) : this(
|
constructor(source: Parcel) : this(
|
||||||
title = source.readString(),
|
title = source.readString(),
|
||||||
content = source.readString(),
|
content = source.readString(),
|
||||||
date_published = source.readString(),
|
date_published = source.readString(),
|
||||||
lead_image_url = source.readString(),
|
lead_image_url = source.readString(),
|
||||||
dek = source.readString(),
|
dek = source.readString(),
|
||||||
url = source.readString(),
|
url = source.readString(),
|
||||||
domain = source.readString(),
|
domain = source.readString(),
|
||||||
excerpt = source.readString(),
|
excerpt = source.readString(),
|
||||||
total_pages = source.readInt(),
|
total_pages = source.readInt(),
|
||||||
rendered_pages = source.readInt(),
|
rendered_pages = source.readInt(),
|
||||||
next_page_url = source.readString()
|
next_page_url = source.readString()
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.mercury
|
package apps.amine.bou.readerforselfoss.api.mercury
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
interface MercuryService {
|
interface MercuryService {
|
||||||
@GET("parser")
|
@GET("parser.php")
|
||||||
fun parseUrl(@Query("url") url: String, @Header("x-api-key") key: String): Call<ParsedContent>
|
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.google.gson.JsonDeserializationContext
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonDeserializer
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
|
||||||
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
|
||||||
|
|
||||||
@Throws(JsonParseException::class)
|
@Throws(JsonParseException::class)
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): Boolean? =
|
||||||
try {
|
try {
|
||||||
json.asInt == 1
|
json.asInt == 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
|
||||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
|
||||||
import com.burgstaller.okhttp.DispatchingAuthenticator
|
import com.burgstaller.okhttp.DispatchingAuthenticator
|
||||||
@ -18,93 +19,136 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class SelfossApi(
|
||||||
|
c: Context,
|
||||||
|
callingActivity: Activity,
|
||||||
|
isWithSelfSignedCert: Boolean,
|
||||||
|
shouldLog: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
class SelfossApi(c: Context) {
|
private lateinit var service: SelfossService
|
||||||
|
|
||||||
private val service: SelfossService
|
|
||||||
private val config: Config = Config(c)
|
private val config: Config = Config(c)
|
||||||
private val userName: String
|
private val userName: String
|
||||||
private val password: String
|
private val password: String
|
||||||
|
|
||||||
init {
|
fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder =
|
||||||
|
if (isWithSelfSignedCert) {
|
||||||
|
getUnsafeHttpClient()
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
val interceptor = HttpLoggingInterceptor()
|
fun Credentials.createAuthenticator(): DispatchingAuthenticator =
|
||||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
DispatchingAuthenticator.Builder()
|
||||||
|
.with("digest", DigestAuthenticator(this))
|
||||||
|
.with("basic", BasicAuthenticator(this))
|
||||||
|
.build()
|
||||||
|
|
||||||
val httpBuilder = OkHttpClient.Builder()
|
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean): OkHttpClient.Builder {
|
||||||
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
|
||||||
|
return OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.maybeWithSelfSigned(isWithSelfSignedCert)
|
||||||
|
.authenticator(CachingAuthenticatorDecorator(this, authCache))
|
||||||
|
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
||||||
|
}
|
||||||
|
|
||||||
val httpUserName = config.httpUserLogin
|
init {
|
||||||
val httpPassword = config.httpUserPassword
|
userName = config.userLogin
|
||||||
|
password = config.userPassword
|
||||||
|
|
||||||
val credentials = Credentials(httpUserName, httpPassword)
|
val authenticator =
|
||||||
val basicAuthenticator = BasicAuthenticator(credentials)
|
Credentials(
|
||||||
val digestAuthenticator = DigestAuthenticator(credentials)
|
config.httpUserLogin,
|
||||||
|
config.httpUserPassword
|
||||||
|
).createAuthenticator()
|
||||||
|
|
||||||
// note that all auth schemes should be registered as lowercase!
|
val gson =
|
||||||
val authenticator = DispatchingAuthenticator.Builder()
|
GsonBuilder()
|
||||||
.with("digest", digestAuthenticator)
|
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
||||||
.with("basic", basicAuthenticator)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val client = httpBuilder
|
|
||||||
.authenticator(CachingAuthenticatorDecorator(authenticator, authCache))
|
|
||||||
.addInterceptor(AuthenticationCacheInterceptor(authCache))
|
|
||||||
.addInterceptor(interceptor)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
|
|
||||||
val builder = GsonBuilder()
|
|
||||||
builder.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
|
|
||||||
|
|
||||||
val gson = builder
|
|
||||||
.setLenient()
|
.setLenient()
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
userName = config.userLogin
|
val logging = HttpLoggingInterceptor()
|
||||||
password = config.userPassword
|
|
||||||
val retrofit = Retrofit.Builder().baseUrl(config.baseUrl).client(client)
|
logging.level = if (shouldLog) {
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)).build()
|
HttpLoggingInterceptor.Level.BODY
|
||||||
service = retrofit.create(SelfossService::class.java)
|
} else {
|
||||||
|
HttpLoggingInterceptor.Level.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert)
|
||||||
|
|
||||||
|
httpClient.addInterceptor(logging)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val retrofit =
|
||||||
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.baseUrl(config.baseUrl)
|
||||||
|
.client(httpClient.build())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
|
service = retrofit.create(SelfossService::class.java)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(): Call<SuccessResponse> {
|
fun login(): Call<SuccessResponse> =
|
||||||
return service.loginToSelfoss(config.userLogin, config.userPassword)
|
service.loginToSelfoss(config.userLogin, config.userPassword)
|
||||||
}
|
|
||||||
|
|
||||||
val readItems: Call<List<Item>>
|
fun readItems(
|
||||||
get() = getItems("read")
|
tag: String?,
|
||||||
|
sourceId: Long?,
|
||||||
|
search: String?,
|
||||||
|
itemsNumber: Int,
|
||||||
|
offset: Int
|
||||||
|
): Call<List<Item>> =
|
||||||
|
getItems("read", tag, sourceId, search, itemsNumber, offset)
|
||||||
|
|
||||||
val unreadItems: Call<List<Item>>
|
fun newItems(
|
||||||
get() = getItems("unread")
|
tag: String?,
|
||||||
|
sourceId: Long?,
|
||||||
|
search: String?,
|
||||||
|
itemsNumber: Int,
|
||||||
|
offset: Int
|
||||||
|
): Call<List<Item>> =
|
||||||
|
getItems("unread", tag, sourceId, search, itemsNumber, offset)
|
||||||
|
|
||||||
val starredItems: Call<List<Item>>
|
fun starredItems(
|
||||||
get() = getItems("starred")
|
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): Call<List<Item>> {
|
private fun getItems(
|
||||||
return service.getItems(type, userName, password)
|
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> {
|
fun markItem(itemId: String): Call<SuccessResponse> =
|
||||||
return service.markAsRead(itemId, userName, password)
|
service.markAsRead(itemId, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun unmarkItem(itemId: String): Call<SuccessResponse> {
|
fun unmarkItem(itemId: String): Call<SuccessResponse> =
|
||||||
return service.unmarkAsRead(itemId, userName, password)
|
service.unmarkAsRead(itemId, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun readAll(ids: List<String>): Call<SuccessResponse> {
|
fun readAll(ids: List<String>): Call<SuccessResponse> =
|
||||||
return service.markAllAsRead(ids, userName, password)
|
service.markAllAsRead(ids, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun starrItem(itemId: String): Call<SuccessResponse> {
|
fun starrItem(itemId: String): Call<SuccessResponse> =
|
||||||
return service.starr(itemId, userName, password)
|
service.starr(itemId, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
|
fun unstarrItem(itemId: String): Call<SuccessResponse> =
|
||||||
fun unstarrItem(itemId: String): Call<SuccessResponse> {
|
service.unstarr(itemId, userName, password)
|
||||||
return service.unstarr(itemId, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val stats: Call<Stats>
|
val stats: Call<Stats>
|
||||||
get() = service.stats(userName, password)
|
get() = service.stats(userName, password)
|
||||||
@ -112,23 +156,24 @@ class SelfossApi(c: Context) {
|
|||||||
val tags: Call<List<Tag>>
|
val tags: Call<List<Tag>>
|
||||||
get() = service.tags(userName, password)
|
get() = service.tags(userName, password)
|
||||||
|
|
||||||
fun update(): Call<String> {
|
fun update(): Call<String> =
|
||||||
return service.update(userName, password)
|
service.update(userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
val sources: Call<List<Sources>>
|
val sources: Call<List<Sources>>
|
||||||
get() = service.sources(userName, password)
|
get() = service.sources(userName, password)
|
||||||
|
|
||||||
fun deleteSource(id: String): Call<SuccessResponse> {
|
fun deleteSource(id: String): Call<SuccessResponse> =
|
||||||
return service.deleteSource(id, userName, password)
|
service.deleteSource(id, userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun spouts(): Call<Map<String, Spout>> {
|
fun spouts(): Call<Map<String, Spout>> =
|
||||||
return service.spouts(userName, password)
|
service.spouts(userName, password)
|
||||||
}
|
|
||||||
|
|
||||||
fun createSource(title: String, url: String, spout: String, tags: String, filter: String): Call<SuccessResponse> {
|
|
||||||
return service.createSource(title, url, spout, tags, filter, userName, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun createSource(
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
spout: String,
|
||||||
|
tags: String,
|
||||||
|
filter: String
|
||||||
|
): Call<SuccessResponse> =
|
||||||
|
service.createSource(title, url, spout, tags, filter, userName, password)
|
||||||
}
|
}
|
||||||
|
@ -4,56 +4,75 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
import apps.amine.bou.readerforselfoss.utils.Config
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
private fun constructUrl(config: Config?, path: String, file: String): String {
|
private fun constructUrl(config: Config?, path: String, file: String): String {
|
||||||
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
|
||||||
baseUriBuilder.appendPath(path).appendPath(file)
|
baseUriBuilder.appendPath(path).appendPath(file)
|
||||||
|
|
||||||
return if (isEmptyOrNullOrNullString(file)) ""
|
return if (file.isEmptyOrNullOrNullString()) {
|
||||||
else baseUriBuilder.toString()
|
""
|
||||||
|
} else {
|
||||||
|
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(@SerializedName("success") val success: Boolean) {
|
||||||
|
|
||||||
class SuccessResponse(val success: Boolean) {
|
|
||||||
val isSuccess: Boolean
|
val isSuccess: Boolean
|
||||||
get() = success
|
get() = success
|
||||||
}
|
}
|
||||||
|
|
||||||
class Stats(val total: Int, val unread: Int, val starred: Int)
|
class Stats(
|
||||||
|
@SerializedName("total") val total: Int,
|
||||||
|
@SerializedName("unread") val unread: Int,
|
||||||
|
@SerializedName("starred") val starred: Int
|
||||||
|
)
|
||||||
|
|
||||||
data class Spout(val name: String, val description: String)
|
data class Spout(
|
||||||
|
@SerializedName("name") val name: String,
|
||||||
|
@SerializedName("description") val description: String
|
||||||
|
)
|
||||||
|
|
||||||
data class Sources(val id: String,
|
data class Sources(
|
||||||
val title: String,
|
@SerializedName("id") val id: String,
|
||||||
val tags: String,
|
@SerializedName("title") val title: String,
|
||||||
val spout: String,
|
@SerializedName("tags") val tags: String,
|
||||||
val error: String,
|
@SerializedName("spout") val spout: String,
|
||||||
val icon: String) {
|
@SerializedName("error") val error: String,
|
||||||
|
@SerializedName("icon") val icon: String
|
||||||
|
) {
|
||||||
var config: Config? = null
|
var config: Config? = null
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
fun getIcon(app: Context): String {
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
config = Config(app)
|
config = Config(app)
|
||||||
}
|
}
|
||||||
return constructUrl(config,"favicons", icon)
|
return constructUrl(config, "favicons", icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Item(val id: String,
|
data class Item(
|
||||||
val datetime: String,
|
@SerializedName("id") val id: String,
|
||||||
val title: String,
|
@SerializedName("datetime") val datetime: String,
|
||||||
val unread: Boolean,
|
@SerializedName("title") val title: String,
|
||||||
val starred: Boolean,
|
@SerializedName("content") val content: String,
|
||||||
val thumbnail: String,
|
@SerializedName("unread") val unread: Boolean,
|
||||||
val icon: String,
|
@SerializedName("starred") var starred: Boolean,
|
||||||
val link: String,
|
@SerializedName("thumbnail") val thumbnail: String,
|
||||||
val sourcetitle: String) : Parcelable {
|
@SerializedName("icon") val icon: String,
|
||||||
|
@SerializedName("link") val link: String,
|
||||||
|
@SerializedName("sourcetitle") val sourcetitle: String,
|
||||||
|
@SerializedName("tags") val tags: String
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
var config: Config? = null
|
var config: Config? = null
|
||||||
|
|
||||||
@ -65,15 +84,17 @@ data class Item(val id: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: Parcel) : this(
|
constructor(source: Parcel) : this(
|
||||||
id = source.readString(),
|
id = source.readString(),
|
||||||
datetime = source.readString(),
|
datetime = source.readString(),
|
||||||
title = source.readString(),
|
title = source.readString(),
|
||||||
unread = 0.toByte() != source.readByte(),
|
content = source.readString(),
|
||||||
starred = 0.toByte() != source.readByte(),
|
unread = 0.toByte() != source.readByte(),
|
||||||
thumbnail = source.readString(),
|
starred = 0.toByte() != source.readByte(),
|
||||||
icon = source.readString(),
|
thumbnail = source.readString(),
|
||||||
link = source.readString(),
|
icon = source.readString(),
|
||||||
sourcetitle = source.readString()
|
link = source.readString(),
|
||||||
|
sourcetitle = source.readString(),
|
||||||
|
tags = source.readString()
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
@ -82,12 +103,14 @@ data class Item(val id: String,
|
|||||||
dest.writeString(id)
|
dest.writeString(id)
|
||||||
dest.writeString(datetime)
|
dest.writeString(datetime)
|
||||||
dest.writeString(title)
|
dest.writeString(title)
|
||||||
|
dest.writeString(content)
|
||||||
dest.writeByte((if (unread) 1 else 0))
|
dest.writeByte((if (unread) 1 else 0))
|
||||||
dest.writeByte((if (starred) 1 else 0))
|
dest.writeByte((if (starred) 1 else 0))
|
||||||
dest.writeString(thumbnail)
|
dest.writeString(thumbnail)
|
||||||
dest.writeString(icon)
|
dest.writeString(icon)
|
||||||
dest.writeString(link)
|
dest.writeString(link)
|
||||||
dest.writeString(sourcetitle)
|
dest.writeString(sourcetitle)
|
||||||
|
dest.writeString(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(app: Context): String {
|
fun getIcon(app: Context): String {
|
||||||
@ -107,21 +130,27 @@ data class Item(val id: String,
|
|||||||
// TODO: maybe find a better way to handle these kind of urls
|
// TODO: maybe find a better way to handle these kind of urls
|
||||||
fun getLinkDecoded(): String {
|
fun getLinkDecoded(): String {
|
||||||
var stringUrl: String
|
var stringUrl: String
|
||||||
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
stringUrl =
|
||||||
if (link.contains("&url=")) {
|
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
|
||||||
stringUrl = link.substringAfter("&url=")
|
if (link.contains("&url=")) {
|
||||||
} else {
|
link.substringAfter("&url=")
|
||||||
stringUrl = this.link.replace("&", "&")
|
} else {
|
||||||
}
|
this.link.replace("&", "&")
|
||||||
} else {
|
}
|
||||||
stringUrl = this.link.replace("&", "&")
|
} else {
|
||||||
}
|
this.link.replace("&", "&")
|
||||||
|
}
|
||||||
|
|
||||||
// handle :443 => https
|
// handle :443 => https
|
||||||
if (stringUrl.contains(":443")) {
|
if (stringUrl.contains(":443")) {
|
||||||
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle url not starting with http
|
||||||
|
if (stringUrl.startsWith("//")) {
|
||||||
|
stringUrl = "http:$stringUrl"
|
||||||
|
}
|
||||||
|
|
||||||
return stringUrl
|
return stringUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,65 +1,118 @@
|
|||||||
package apps.amine.bou.readerforselfoss.api.selfoss
|
package apps.amine.bou.readerforselfoss.api.selfoss
|
||||||
|
|
||||||
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.Field
|
import retrofit2.http.Field
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
internal interface SelfossService {
|
internal interface SelfossService {
|
||||||
|
|
||||||
@GET("login")
|
@GET("login")
|
||||||
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
||||||
|
|
||||||
@GET("items")
|
@GET("items")
|
||||||
fun getItems(@Query("type") type: String, @Query("username") username: String, @Query("password") password: String): 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>>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("mark/{id}")
|
@POST("mark/{id}")
|
||||||
fun markAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun markAsRead(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("unmark/{id}")
|
@POST("unmark/{id}")
|
||||||
fun unmarkAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun unmarkAsRead(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("mark")
|
@POST("mark")
|
||||||
fun markAllAsRead(@Field("ids[]") ids: List<String>, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
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}")
|
@POST("starr/{id}")
|
||||||
fun starr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun starr(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||||
@POST("unstarr/{id}")
|
@POST("unstarr/{id}")
|
||||||
fun unstarr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun unstarr(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@GET("stats")
|
@GET("stats")
|
||||||
fun stats(@Query("username") username: String, @Query("password") password: String): Call<Stats>
|
fun stats(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<Stats>
|
||||||
|
|
||||||
@GET("tags")
|
@GET("tags")
|
||||||
fun tags(@Query("username") username: String, @Query("password") password: String): Call<List<Tag>>
|
fun tags(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<List<Tag>>
|
||||||
|
|
||||||
@GET("update")
|
@GET("update")
|
||||||
fun update(@Query("username") username: String, @Query("password") password: String): Call<String>
|
fun update(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<String>
|
||||||
|
|
||||||
@GET("sources/spouts")
|
@GET("sources/spouts")
|
||||||
fun spouts(@Query("username") username: String, @Query("password") password: String): Call<Map<String, Spout>>
|
fun spouts(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<Map<String, Spout>>
|
||||||
|
|
||||||
@GET("sources/list")
|
@GET("sources/list")
|
||||||
fun sources(@Query("username") username: String, @Query("password") password: String): Call<List<Sources>>
|
fun sources(
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<List<Sources>>
|
||||||
|
|
||||||
@DELETE("source/{id}")
|
@DELETE("source/{id}")
|
||||||
fun deleteSource(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun deleteSource(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("source")
|
@POST("source")
|
||||||
fun createSource(@Field("title") title: String, @Field("url") url: String, @Field("spout") spout: String, @Field("tags") tags: String, @Field("filter") filter: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
|
fun createSource(
|
||||||
|
@Field("title") title: String,
|
||||||
|
@Field("url") url: String,
|
||||||
|
@Field("spout") spout: String,
|
||||||
|
@Field("tags") tags: String,
|
||||||
|
@Field("filter") filter: String,
|
||||||
|
@Query("username") username: String,
|
||||||
|
@Query("password") password: String
|
||||||
|
): Call<SuccessResponse>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,428 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.customtabs.CustomTabsIntent
|
||||||
|
import android.support.design.widget.FloatingActionButton
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v4.widget.NestedScrollView
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
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.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.openItemUrl
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.shareLink
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
|
||||||
|
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
|
||||||
|
|
||||||
|
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 var showMalformedUrl: Boolean = false
|
||||||
|
private lateinit var editor: SharedPreferences.Editor
|
||||||
|
private lateinit var fab: FloatingActionButton
|
||||||
|
private lateinit var appColors: AppColors
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
editor = prefs.edit()
|
||||||
|
fontSize = prefs.getString("reader_font_size", "14").toInt()
|
||||||
|
showMalformedUrl = prefs.getBoolean("show_error_malformed_url", true)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
R.id.open_action -> activity!!.openItemUrl(
|
||||||
|
allItems,
|
||||||
|
pageNumber.toInt(),
|
||||||
|
url,
|
||||||
|
customTabsIntent,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
activity!!
|
||||||
|
)
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
url = response.body()!!.url
|
||||||
|
} 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) {
|
||||||
|
if (showMalformedUrl && context != null) {
|
||||||
|
val alertDialog = AlertDialog.Builder(context!!).create()
|
||||||
|
alertDialog.setTitle("Error")
|
||||||
|
alertDialog.setMessage("You are encountering a bug that I can't solve. Can you please contact me to solve the issue, please ?")
|
||||||
|
alertDialog.setButton(
|
||||||
|
AlertDialog.BUTTON_POSITIVE,
|
||||||
|
"Send mail",
|
||||||
|
{ dialog, _ ->
|
||||||
|
|
||||||
|
// This won't be translated because it should only be temporary.
|
||||||
|
val to = Config.feedbackEmail
|
||||||
|
val subject= "[ReaderForSelfoss MalformedURLException]"
|
||||||
|
val body= "Please specify the source, item and spout you are using for the url below : \n ${e.message}"
|
||||||
|
val mailTo = "mailto:" + to + "?&subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body)
|
||||||
|
|
||||||
|
val emailIntent = Intent(Intent.ACTION_VIEW)
|
||||||
|
emailIntent.data = Uri.parse(mailTo)
|
||||||
|
startActivity(emailIntent)
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
alertDialog.setButton(
|
||||||
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
|
"Not now",
|
||||||
|
{ dialog, _ -> dialog.dismiss() }
|
||||||
|
)
|
||||||
|
alertDialog.setButton(
|
||||||
|
AlertDialog.BUTTON_NEGATIVE,
|
||||||
|
"Don't show anymore.",
|
||||||
|
{ dialog, _ ->
|
||||||
|
editor.putBoolean("show_error_malformed_url", false)
|
||||||
|
editor.apply()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
alertDialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.webcontent.loadDataWithBaseURL(
|
||||||
|
baseUrl,
|
||||||
|
"""<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>$c""".trimMargin(),
|
||||||
|
"text/html; charset=utf-8",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,17 +1,28 @@
|
|||||||
package apps.amine.bou.readerforselfoss.settings;
|
package apps.amine.bou.readerforselfoss.settings;
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceActivity;
|
import android.preference.PreferenceActivity;
|
||||||
import android.support.annotation.LayoutRes;
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.AppBarLayout;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
import android.support.v7.app.AppCompatDelegate;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
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
|
* A {@link PreferenceActivity} which implements and proxies the necessary calls
|
||||||
@ -23,6 +34,8 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
new AppColors(this);
|
||||||
|
|
||||||
getDelegate().installViewFactory();
|
getDelegate().installViewFactory();
|
||||||
getDelegate().onCreate(savedInstanceState);
|
getDelegate().onCreate(savedInstanceState);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@ -31,6 +44,23 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
root.addView(bar, 0);
|
||||||
|
|
||||||
getDelegate().onPostCreate(savedInstanceState);
|
getDelegate().onPostCreate(savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +128,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
|||||||
getDelegate().onDestroy();
|
getDelegate().onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void invalidateOptionsMenu() {
|
public void invalidateOptionsMenu() {
|
||||||
getDelegate().invalidateOptionsMenu();
|
getDelegate().invalidateOptionsMenu();
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,39 @@ package apps.amine.bou.readerforselfoss.settings;
|
|||||||
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.Preference.OnPreferenceChangeListener;
|
import android.preference.Preference.OnPreferenceChangeListener;
|
||||||
|
import android.preference.Preference.OnPreferenceClickListener;
|
||||||
import android.preference.PreferenceActivity;
|
import android.preference.PreferenceActivity;
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.support.v7.app.ActionBar;
|
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.support.v7.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.view.MenuItem;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.BuildConfig;
|
||||||
import apps.amine.bou.readerforselfoss.R;
|
import apps.amine.bou.readerforselfoss.R;
|
||||||
|
import apps.amine.bou.readerforselfoss.themes.AppColors;
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +94,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
new AppColors(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setupActionBar();
|
setupActionBar();
|
||||||
}
|
}
|
||||||
@ -115,10 +131,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
* This method stops fragment injection in malicious applications.
|
* This method stops fragment injection in malicious applications.
|
||||||
* Make sure to deny any unknown fragments here.
|
* Make sure to deny any unknown fragments here.
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
protected boolean isValidFragment(String fragmentName) {
|
protected boolean isValidFragment(String fragmentName) {
|
||||||
return PreferenceFragment.class.getName().equals(fragmentName)
|
return PreferenceFragment.class.getName().equals(fragmentName)
|
||||||
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|
||||||
|| LinksPreferenceFragment.class.getName().equals(fragmentName);
|
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|
||||||
|
|| DebugPreferenceFragment.class.getName().equals(fragmentName)
|
||||||
|
|| LinksPreferenceFragment.class.getName().equals(fragmentName)
|
||||||
|
|| ThemePreferenceFragment.class.getName().equals(fragmentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,12 +157,123 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
|
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
|
||||||
tabOnTap.setEnabled(!cardViewActive.isChecked());
|
tabOnTap.setEnabled(!cardViewActive.isChecked());
|
||||||
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
|
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue){
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||||
boolean isEnabled = (Boolean) newValue;
|
boolean isEnabled = (Boolean) newValue;
|
||||||
tabOnTap.setEnabled(!isEnabled);
|
tabOnTap.setEnabled(!isEnabled);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
|
||||||
|
itemsNumber.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 <= 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
|
||||||
|
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 DebugPreferenceFragment extends PreferenceFragment {
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
addPreferencesFromResource(R.xml.pref_debug);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
SharedPreferences pref = getActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE);
|
||||||
|
final String id = pref.getString("unique_id", "...");
|
||||||
|
|
||||||
|
final Preference identifier = findPreference("debug_identifier");
|
||||||
|
final ClipboardManager clipboard = (ClipboardManager)
|
||||||
|
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
|
||||||
|
identifier.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
identifier.setTitle(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -162,18 +293,21 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
*/
|
*/
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
public static class LinksPreferenceFragment extends PreferenceFragment {
|
public static class LinksPreferenceFragment extends PreferenceFragment {
|
||||||
|
public void openUrl(Uri uri) {
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
startActivity(browserIntent);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
addPreferencesFromResource(R.xml.pref_links);
|
addPreferencesFromResource(R.xml.pref_links);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
Preference tracker = findPreference( "trackerLink" );
|
findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
tracker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.tracker_url)));
|
openUrl(Uri.parse(Config.trackerUrl));
|
||||||
startActivity(browserIntent);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -181,8 +315,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_url)));
|
openUrl(Uri.parse(Config.sourceUrl));
|
||||||
startActivity(browserIntent);
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
openUrl(Uri.parse(Config.translationUrl));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -199,6 +340,41 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.themes
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.annotation.ColorInt
|
||||||
|
import android.support.v7.view.ContextThemeWrapper
|
||||||
|
import android.util.TypedValue
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
class AppColors(a: Activity) {
|
||||||
|
|
||||||
|
@ColorInt val colorPrimary: Int
|
||||||
|
@ColorInt val colorPrimaryDark: Int
|
||||||
|
@ColorInt val colorAccent: Int
|
||||||
|
@ColorInt val colorAccentDark: Int
|
||||||
|
@ColorInt val cardBackgroundColor: Int
|
||||||
|
@ColorInt val colorBackground: Int
|
||||||
|
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
|
||||||
|
|
||||||
|
val typedCardBackground = TypedValue()
|
||||||
|
a.theme.resolveAttribute(R.attr.cardBackgroundColor, typedCardBackground, true)
|
||||||
|
|
||||||
|
cardBackgroundColor = typedCardBackground.data
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.themes
|
||||||
|
|
||||||
|
enum class Toppings(val value: Int) {
|
||||||
|
PRIMARY(1),
|
||||||
|
PRIMARY_DARK(2),
|
||||||
|
ACCENT(3),
|
||||||
|
ACCENT_DARK(4)
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.transformers
|
||||||
|
|
||||||
|
import android.support.v4.view.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,12 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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,87 +2,39 @@ package apps.amine.bou.readerforselfoss.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v7.app.AlertDialog
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Patterns
|
|
||||||
import apps.amine.bou.readerforselfoss.BuildConfig
|
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
private fun isStoreVersion(context: Context): Boolean {
|
fun String?.isEmptyOrNullOrNullString(): Boolean =
|
||||||
var result = false
|
this == null || this == "null" || this.isEmpty()
|
||||||
try {
|
|
||||||
val installer = context.packageManager
|
fun String.longHash(): Long {
|
||||||
.getInstallerPackageName(context.packageName)
|
var h = 98764321261L
|
||||||
result = !TextUtils.isEmpty(installer)
|
val l = this.length
|
||||||
} catch (e: Throwable) {
|
val chars = this.toCharArray()
|
||||||
|
|
||||||
|
for (i in 0 until l) {
|
||||||
|
h = 31 * h + chars[i].toLong()
|
||||||
}
|
}
|
||||||
|
return h
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkAndDisplayStoreApk(context: Context) =
|
fun String.toStringUriWithHttp(): String =
|
||||||
if (!isStoreVersion(context) && !BuildConfig.GITHUB_VERSION) {
|
if (!this.startsWith("https://") && !this.startsWith("http://")) {
|
||||||
val alertDialog = AlertDialog.Builder(context).create()
|
"http://" + this
|
||||||
alertDialog.setTitle(context.getString(R.string.warning_version))
|
} else {
|
||||||
alertDialog.setMessage(context.getString(R.string.text_version))
|
this
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
|
|
||||||
{ dialog, _ -> dialog.dismiss() })
|
|
||||||
alertDialog.show()
|
|
||||||
} else Unit
|
|
||||||
|
|
||||||
|
|
||||||
fun isUrlValid(url: String): Boolean {
|
|
||||||
val baseUrl = HttpUrl.parse(url)
|
|
||||||
var existsAndEndsWithSlash = false
|
|
||||||
if (baseUrl != null) {
|
|
||||||
val pathSegments = baseUrl.pathSegments()
|
|
||||||
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return Patterns.WEB_URL.matcher(url).matches() && existsAndEndsWithSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmptyOrNullOrNullString(str: String?): Boolean =
|
|
||||||
str == null || str == "null" || str.isEmpty()
|
|
||||||
|
|
||||||
fun checkApkVersion(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
|
|
||||||
mFirebaseRemoteConfig.fetch(43200)
|
|
||||||
.addOnCompleteListener { task ->
|
|
||||||
if (task.isSuccessful) {
|
|
||||||
mFirebaseRemoteConfig.activateFetched()
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
|
|
||||||
isThereAnUpdate(settings, editor, context, mFirebaseRemoteConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isThereAnUpdate(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
|
|
||||||
val APK_LINK = "github_apk"
|
|
||||||
|
|
||||||
val apkLink = mFirebaseRemoteConfig.getString(APK_LINK)
|
|
||||||
val storedLink = settings.getString(APK_LINK, "")
|
|
||||||
if (apkLink != storedLink && !apkLink.isEmpty()) {
|
|
||||||
val alertDialog = AlertDialog.Builder(context).create()
|
|
||||||
alertDialog.setTitle(context.getString(R.string.new_apk_available_title))
|
|
||||||
alertDialog.setMessage(context.getString(R.string.new_apk_available_message))
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.new_apk_available_get)) { _, _ ->
|
|
||||||
editor.putString(APK_LINK, apkLink)
|
|
||||||
editor.apply()
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink))
|
|
||||||
context.startActivity(browserIntent)
|
|
||||||
}
|
|
||||||
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.new_apk_available_no),
|
|
||||||
{ dialog, _ ->
|
|
||||||
editor.putString(APK_LINK, apkLink)
|
|
||||||
editor.apply()
|
|
||||||
dialog.dismiss()
|
|
||||||
})
|
|
||||||
alertDialog.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.shareLink(itemUrl: String) {
|
||||||
|
val sendIntent = Intent()
|
||||||
|
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
|
||||||
|
sendIntent.type = "text/plain"
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
getString(R.string.share)
|
||||||
|
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,16 +1,14 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import apps.amine.bou.readerforselfoss.LoginActivity
|
||||||
|
|
||||||
class Config(c: Context) {
|
class Config(c: Context) {
|
||||||
|
|
||||||
private val settings: SharedPreferences
|
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
init {
|
|
||||||
this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val baseUrl: String
|
val baseUrl: String
|
||||||
get() = settings.getString("url", "")
|
get() = settings.getString("url", "")
|
||||||
@ -28,7 +26,33 @@ class Config(c: Context) {
|
|||||||
get() = settings.getString("httpPassword", "")
|
get() = settings.getString("httpPassword", "")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val settingsName = "paramsselfoss"
|
const val settingsName = "paramsselfoss"
|
||||||
|
|
||||||
|
const val feedbackEmail = "aminecmi@gmail.com"
|
||||||
|
|
||||||
|
const val translationUrl = "https://crwd.in/readerforselfoss"
|
||||||
|
|
||||||
|
const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss"
|
||||||
|
|
||||||
|
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
|
||||||
|
|
||||||
|
fun logoutAndRedirect(
|
||||||
|
c: Context,
|
||||||
|
callingActivity: Activity,
|
||||||
|
editor: SharedPreferences.Editor,
|
||||||
|
baseUrlFail: Boolean = false
|
||||||
|
): Boolean {
|
||||||
|
editor.remove("url")
|
||||||
|
editor.remove("login")
|
||||||
|
editor.remove("password")
|
||||||
|
editor.apply()
|
||||||
|
val intent = Intent(c, LoginActivity::class.java)
|
||||||
|
if (baseUrlFail) {
|
||||||
|
intent.putExtra("baseUrlFail", baseUrlFail)
|
||||||
|
}
|
||||||
|
c.startActivity(intent)
|
||||||
|
callingActivity.finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.security.cert.CertificateException
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
fun getUnsafeHttpClient(): OkHttpClient.Builder =
|
||||||
|
try {
|
||||||
|
// Create a trust manager that does not validate certificate chains
|
||||||
|
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> =
|
||||||
|
arrayOf()
|
||||||
|
|
||||||
|
@Throws(CertificateException::class)
|
||||||
|
override fun checkClientTrusted(
|
||||||
|
chain: Array<java.security.cert.X509Certificate>,
|
||||||
|
authType: String
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(CertificateException::class)
|
||||||
|
override fun checkServerTrusted(
|
||||||
|
chain: Array<java.security.cert.X509Certificate>,
|
||||||
|
authType: String
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install the all-trusting trust manager
|
||||||
|
val sslContext = SSLContext.getInstance("SSL")
|
||||||
|
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
|
||||||
|
|
||||||
|
val sslSocketFactory = sslContext.socketFactory
|
||||||
|
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||||
|
.hostnameVerifier { _, _ -> true }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
|
import org.acra.ACRA
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun String.toTextDrawableString(c: Context): String {
|
||||||
|
val textDrawable = StringBuilder()
|
||||||
|
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 {
|
||||||
|
val formattedDate: String = try {
|
||||||
|
" " + DateUtils.getRelativeTimeSpanString(
|
||||||
|
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
|
||||||
|
Date().time,
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
|
)
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
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.split(",")
|
||||||
|
tags.map {
|
||||||
|
item.copy(tags = it.trim())
|
||||||
|
}
|
||||||
|
}
|
@ -7,75 +7,142 @@ import android.content.Intent
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.customtabs.CustomTabsIntent
|
import android.support.customtabs.CustomTabsIntent
|
||||||
|
import android.util.Patterns
|
||||||
|
import android.widget.Toast
|
||||||
import apps.amine.bou.readerforselfoss.R
|
import apps.amine.bou.readerforselfoss.R
|
||||||
import apps.amine.bou.readerforselfoss.ReaderActivity
|
import apps.amine.bou.readerforselfoss.ReaderActivity
|
||||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
import apps.amine.bou.readerforselfoss.api.selfoss.Item
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
|
||||||
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
fun buildCustomTabsIntent(c: Context): CustomTabsIntent {
|
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||||
|
|
||||||
fun createPendingShareIntent(c: Context): PendingIntent {
|
val actionIntent = Intent(Intent.ACTION_SEND)
|
||||||
val actionIntent = Intent(Intent.ACTION_SEND)
|
actionIntent.type = "text/plain"
|
||||||
actionIntent.type = "text/plain"
|
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
return PendingIntent.getActivity(
|
this,
|
||||||
c, 0, actionIntent, 0)
|
0,
|
||||||
}
|
actionIntent,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
val intentBuilder = CustomTabsIntent.Builder()
|
val intentBuilder = CustomTabsIntent.Builder()
|
||||||
|
|
||||||
// TODO: change to primary when it's possible to customize custom tabs title color
|
// TODO: change to primary when it's possible to customize custom tabs title color
|
||||||
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
|
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
|
||||||
intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark))
|
intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
|
||||||
intentBuilder.setShowTitle(true)
|
intentBuilder.setShowTitle(true)
|
||||||
|
|
||||||
|
|
||||||
intentBuilder.setStartAnimations(c,
|
intentBuilder.setStartAnimations(
|
||||||
R.anim.slide_in_right,
|
this,
|
||||||
R.anim.slide_out_left)
|
R.anim.slide_in_right,
|
||||||
intentBuilder.setExitAnimations(c,
|
R.anim.slide_out_left
|
||||||
android.R.anim.slide_in_left,
|
)
|
||||||
android.R.anim.slide_out_right)
|
intentBuilder.setExitAnimations(
|
||||||
|
this,
|
||||||
|
android.R.anim.slide_in_left,
|
||||||
|
android.R.anim.slide_out_right
|
||||||
|
)
|
||||||
|
|
||||||
val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp)
|
val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
|
||||||
intentBuilder.setCloseButtonIcon(closeicon)
|
intentBuilder.setCloseButtonIcon(closeicon)
|
||||||
|
|
||||||
val shareLabel = c.getString(R.string.label_share)
|
val shareLabel = this.getString(R.string.label_share)
|
||||||
val icon = BitmapFactory.decodeResource(c.resources,
|
val icon = BitmapFactory.decodeResource(
|
||||||
R.drawable.ic_share_white_24dp)
|
resources,
|
||||||
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c))
|
R.drawable.ic_share_white_24dp
|
||||||
|
)
|
||||||
|
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
|
||||||
|
|
||||||
return intentBuilder.build()
|
return intentBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openItemUrl(i: Item,
|
fun Context.openItemUrlInternally(
|
||||||
customTabsIntent: CustomTabsIntent,
|
allItems: ArrayList<Item>,
|
||||||
internalBrowser: Boolean,
|
currentItem: Int,
|
||||||
articleViewer: Boolean,
|
linkDecoded: String,
|
||||||
app: Activity,
|
customTabsIntent: CustomTabsIntent,
|
||||||
c: Context) {
|
articleViewer: Boolean,
|
||||||
if (!internalBrowser) {
|
app: Activity
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
) {
|
||||||
intent.data = Uri.parse(i.getLinkDecoded())
|
if (articleViewer) {
|
||||||
|
ReaderActivity.allItems = allItems
|
||||||
|
val intent = Intent(this, ReaderActivity::class.java)
|
||||||
|
intent.putExtra("currentItem", currentItem)
|
||||||
app.startActivity(intent)
|
app.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
if (articleViewer) {
|
try {
|
||||||
val intent = Intent(c, ReaderActivity::class.java)
|
CustomTabActivityHelper.openCustomTab(
|
||||||
|
app,
|
||||||
DragDismissIntentBuilder(c)
|
customTabsIntent,
|
||||||
.setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side
|
Uri.parse(linkDecoded)
|
||||||
.setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL) // Larger elasticities will make it easier to dismiss.
|
|
||||||
.build(intent)
|
|
||||||
|
|
||||||
intent.putExtra("url", i.getLinkDecoded())
|
|
||||||
app.startActivity(intent)
|
|
||||||
} else {
|
|
||||||
CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(i.getLinkDecoded())
|
|
||||||
) { _, uri ->
|
) { _, uri ->
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
c.startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
openInBrowser(linkDecoded, app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.openItemUrl(
|
||||||
|
allItems: ArrayList<Item>,
|
||||||
|
currentItem: Int,
|
||||||
|
linkDecoded: String,
|
||||||
|
customTabsIntent: CustomTabsIntent,
|
||||||
|
internalBrowser: Boolean,
|
||||||
|
articleViewer: Boolean,
|
||||||
|
app: Activity
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!linkDecoded.isUrlValid()) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
this.getString(R.string.cant_open_invalid_url),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
if (!internalBrowser) {
|
||||||
|
openInBrowser(linkDecoded, app)
|
||||||
|
} else {
|
||||||
|
this.openItemUrlInternally(
|
||||||
|
allItems,
|
||||||
|
currentItem,
|
||||||
|
linkDecoded,
|
||||||
|
customTabsIntent,
|
||||||
|
articleViewer,
|
||||||
|
app
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInBrowser(linkDecoded: String, app: Activity) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(linkDecoded)
|
||||||
|
app.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.support.design.widget.CoordinatorLayout
|
||||||
|
import android.support.design.widget.FloatingActionButton
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
class ScrollAwareFABBehavior(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet
|
||||||
|
) : CoordinatorLayout.Behavior<FloatingActionButton>() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: FloatingActionButton,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
nestedScrollAxes: Int
|
||||||
|
): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: FloatingActionButton,
|
||||||
|
target: View,
|
||||||
|
dxConsumed: Int,
|
||||||
|
dyConsumed: Int,
|
||||||
|
dxUnconsumed: Int,
|
||||||
|
dyUnconsumed: Int
|
||||||
|
) {
|
||||||
|
super.onNestedScroll(
|
||||||
|
coordinatorLayout,
|
||||||
|
child,
|
||||||
|
target,
|
||||||
|
dxConsumed,
|
||||||
|
dyConsumed,
|
||||||
|
dxUnconsumed,
|
||||||
|
dyUnconsumed
|
||||||
|
)
|
||||||
|
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
|
||||||
|
child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||||
|
override fun onHidden(fab: FloatingActionButton?) {
|
||||||
|
super.onHidden(fab)
|
||||||
|
fab!!.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
|
||||||
|
child.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
|
||||||
|
val Int.toPx: Int
|
||||||
|
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
val Int.toDp: Int
|
||||||
|
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
@ -0,0 +1,12 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.bottombar
|
||||||
|
|
||||||
|
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||||
|
|
||||||
|
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||||
|
this.setText("")
|
||||||
|
this.hide()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextBadgeItem.maybeShow(): TextBadgeItem =
|
||||||
|
if (this.isHidden) this.show() else this
|
@ -1,7 +1,7 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.customtabs.CustomTabsClient;
|
import android.support.customtabs.CustomTabsClient;
|
||||||
@ -11,20 +11,22 @@ import android.support.customtabs.CustomTabsSession;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
/**
|
||||||
public class CustomTabActivityHelper {
|
* This is a helper class to manage the connection to the Custom Tabs Service.
|
||||||
|
*/
|
||||||
|
public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||||
private CustomTabsSession mCustomTabsSession;
|
private CustomTabsSession mCustomTabsSession;
|
||||||
private CustomTabsClient mClient;
|
private CustomTabsClient mClient;
|
||||||
private CustomTabsServiceConnection mConnection;
|
private CustomTabsServiceConnection mConnection;
|
||||||
private ConnectionCallback mConnectionCallback;
|
private ConnectionCallback mConnectionCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView
|
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
|
||||||
*
|
*
|
||||||
* @param activity The host activity
|
* @param activity The host activity.
|
||||||
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available
|
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
||||||
* @param uri the Uri to be opened
|
* @param uri the Uri to be opened.
|
||||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available
|
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
||||||
*/
|
*/
|
||||||
public static void openCustomTab(Activity activity,
|
public static void openCustomTab(Activity activity,
|
||||||
CustomTabsIntent customTabsIntent,
|
CustomTabsIntent customTabsIntent,
|
||||||
@ -32,7 +34,7 @@ public class CustomTabActivityHelper {
|
|||||||
CustomTabFallback fallback) {
|
CustomTabFallback fallback) {
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||||
|
|
||||||
//If we cant find a package name, it means there's no browser that supports
|
//If we cant find a package name, it means theres no browser that supports
|
||||||
//Chrome Custom Tabs installed. So, we fallback to the webview
|
//Chrome Custom Tabs installed. So, we fallback to the webview
|
||||||
if (packageName == null) {
|
if (packageName == null) {
|
||||||
if (fallback != null) {
|
if (fallback != null) {
|
||||||
@ -45,22 +47,22 @@ public class CustomTabActivityHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbinds the Activity from the Custom Tabs Service
|
* Unbinds the Activity from the Custom Tabs Service.
|
||||||
* @param activity the activity that is connected to the service
|
*
|
||||||
|
* @param activity the activity that is connected to the service.
|
||||||
*/
|
*/
|
||||||
public void unbindCustomTabsService(Activity activity) {
|
public void unbindCustomTabsService(Activity activity) {
|
||||||
try {
|
if (mConnection == null) return;
|
||||||
if (mConnection == null) return;
|
activity.unbindService(mConnection);
|
||||||
activity.unbindService(mConnection);
|
mClient = null;
|
||||||
mClient = null;
|
mCustomTabsSession = null;
|
||||||
mCustomTabsSession = null;
|
mConnection = null;
|
||||||
} catch (RuntimeException e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or retrieves an exiting CustomTabsSession
|
* Creates or retrieves an exiting CustomTabsSession.
|
||||||
*
|
*
|
||||||
* @return a CustomTabsSession
|
* @return a CustomTabsSession.
|
||||||
*/
|
*/
|
||||||
public CustomTabsSession getSession() {
|
public CustomTabsSession getSession() {
|
||||||
if (mClient == null) {
|
if (mClient == null) {
|
||||||
@ -72,7 +74,8 @@ public class CustomTabActivityHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service
|
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
||||||
|
*
|
||||||
* @param connectionCallback
|
* @param connectionCallback
|
||||||
*/
|
*/
|
||||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
||||||
@ -80,33 +83,24 @@ public class CustomTabActivityHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the Activity to the Custom Tabs Service
|
* Binds the Activity to the Custom Tabs Service.
|
||||||
* @param activity the activity to be binded to the service
|
*
|
||||||
|
* @param activity the activity to be binded to the service.
|
||||||
*/
|
*/
|
||||||
public void bindCustomTabsService(Activity activity) {
|
public void bindCustomTabsService(Activity activity) {
|
||||||
if (mClient != null) return;
|
if (mClient != null) return;
|
||||||
|
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||||
if (packageName == null) return;
|
if (packageName == null) return;
|
||||||
mConnection = new CustomTabsServiceConnection() {
|
|
||||||
@Override
|
|
||||||
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
|
|
||||||
mClient = client;
|
|
||||||
mClient.warmup(0L);
|
|
||||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
|
|
||||||
//Initialize a session as soon as possible.
|
|
||||||
getSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
mConnection = new ServiceConnection(this);
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
mClient = null;
|
|
||||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
|
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if call to mayLaunchUrl was accepted.
|
||||||
|
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
||||||
|
*/
|
||||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
||||||
if (mClient == null) return false;
|
if (mClient == null) return false;
|
||||||
|
|
||||||
@ -115,32 +109,45 @@ public class CustomTabActivityHelper {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(CustomTabsClient client) {
|
||||||
|
mClient = client;
|
||||||
|
mClient.warmup(0L);
|
||||||
|
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected() {
|
||||||
|
mClient = null;
|
||||||
|
mCustomTabsSession = null;
|
||||||
|
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Callback for when the service is connected or disconnected. Use those callbacks to
|
* A Callback for when the service is connected or disconnected. Use those callbacks to
|
||||||
* handle UI changes when the service is connected or disconnected
|
* handle UI changes when the service is connected or disconnected.
|
||||||
*/
|
*/
|
||||||
public interface ConnectionCallback {
|
public interface ConnectionCallback {
|
||||||
/**
|
/**
|
||||||
* Called when the service is connected
|
* Called when the service is connected.
|
||||||
*/
|
*/
|
||||||
void onCustomTabsConnected();
|
void onCustomTabsConnected();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the service is disconnected
|
* Called when the service is disconnected.
|
||||||
*/
|
*/
|
||||||
void onCustomTabsDisconnected();
|
void onCustomTabsDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To be used as a fallback to open the Uri when Custom Tabs is not available
|
* To be used as a fallback to open the Uri when Custom Tabs is not available.
|
||||||
*/
|
*/
|
||||||
public interface CustomTabFallback {
|
public interface CustomTabFallback {
|
||||||
/**
|
/**
|
||||||
*
|
* @param activity The Activity that wants to open the Uri.
|
||||||
* @param activity The Activity that wants to open the Uri
|
* @param uri The uri to be opened by the fallback.
|
||||||
* @param uri The uri to be opened by the fallback
|
|
||||||
*/
|
*/
|
||||||
void openUri(Activity activity, Uri uri);
|
void openUri(Activity activity, Uri uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
@ -9,11 +10,12 @@ import android.net.Uri;
|
|||||||
import android.support.customtabs.CustomTabsService;
|
import android.support.customtabs.CustomTabsService;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
|
||||||
|
|
||||||
@SuppressWarnings("ALL")
|
@SuppressWarnings("ALL")
|
||||||
class CustomTabsHelper {
|
class CustomTabsHelper {
|
||||||
private static final String TAG = "CustomTabsHelper";
|
private static final String TAG = "CustomTabsHelper";
|
||||||
@ -26,7 +28,8 @@ class CustomTabsHelper {
|
|||||||
|
|
||||||
private static String sPackageNameToUse;
|
private static String sPackageNameToUse;
|
||||||
|
|
||||||
private CustomTabsHelper() {}
|
private CustomTabsHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
public static void addKeepAliveExtra(Context context, Intent intent) {
|
||||||
Intent keepAliveIntent = new Intent().setClassName(
|
Intent keepAliveIntent = new Intent().setClassName(
|
||||||
@ -38,7 +41,7 @@ class CustomTabsHelper {
|
|||||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
||||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||||
* valid package name.
|
* valid package name.
|
||||||
*
|
* <p>
|
||||||
* This is <strong>not</strong> threadsafe.
|
* This is <strong>not</strong> threadsafe.
|
||||||
*
|
*
|
||||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
||||||
@ -92,6 +95,7 @@ class CustomTabsHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to check whether there is a specialized handler for a given intent.
|
* Used to check whether there is a specialized handler for a given intent.
|
||||||
|
*
|
||||||
* @param intent The intent to check with.
|
* @param intent The intent to check with.
|
||||||
* @return Whether there is a specialized handler for the given intent.
|
* @return Whether there is a specialized handler for the given intent.
|
||||||
*/
|
*/
|
||||||
@ -101,7 +105,7 @@ class CustomTabsHelper {
|
|||||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||||
intent,
|
intent,
|
||||||
PackageManager.GET_RESOLVED_FILTER);
|
PackageManager.GET_RESOLVED_FILTER);
|
||||||
if (handlers == null || handlers.size() == 0) {
|
if (handlers == null || handlers.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (ResolveInfo resolveInfo : handlers) {
|
for (ResolveInfo resolveInfo : handlers) {
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.support.customtabs.CustomTabsClient;
|
||||||
|
import android.support.customtabs.CustomTabsServiceConnection;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation for the CustomTabsServiceConnection that avoids leaking the
|
||||||
|
* ServiceConnectionCallback
|
||||||
|
*/
|
||||||
|
public class ServiceConnection extends CustomTabsServiceConnection {
|
||||||
|
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
|
||||||
|
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
|
||||||
|
|
||||||
|
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
|
||||||
|
mConnectionCallback = new WeakReference<>(connectionCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
|
||||||
|
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||||
|
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||||
|
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.customtabs;
|
||||||
|
|
||||||
|
|
||||||
|
import android.support.customtabs.CustomTabsClient;
|
||||||
|
|
||||||
|
|
||||||
|
public interface ServiceConnectionCallback {
|
||||||
|
/**
|
||||||
|
* Called when the service is connected.
|
||||||
|
*
|
||||||
|
* @param client a CustomTabsClient
|
||||||
|
*/
|
||||||
|
void onServiceConnected(CustomTabsClient client);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the service is disconnected.
|
||||||
|
*/
|
||||||
|
void onServiceDisconnected();
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
|
||||||
|
package apps.amine.bou.readerforselfoss.utils.drawer
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
|
||||||
|
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
var icon: ImageView = view.findViewById(R.id.material_drawer_icon)
|
||||||
|
var name: TextView = view.findViewById(R.id.material_drawer_name)
|
||||||
|
var description: TextView = view.findViewById(R.id.material_drawer_description)
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlBasePrimaryDrawerItem.java */
|
||||||
|
package apps.amine.bou.readerforselfoss.utils.drawer
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.annotation.ColorInt
|
||||||
|
import android.support.annotation.ColorRes
|
||||||
|
import android.support.annotation.StringRes
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
|
||||||
|
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||||
|
import com.mikepenz.materialdrawer.holder.ImageHolder
|
||||||
|
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||||
|
import com.mikepenz.materialdrawer.model.BaseDrawerItem
|
||||||
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
|
import com.mikepenz.materialdrawer.util.DrawerUIUtils
|
||||||
|
import com.mikepenz.materialize.util.UIUtils
|
||||||
|
|
||||||
|
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
|
||||||
|
BaseDrawerItem<T, VH>() {
|
||||||
|
fun withIcon(url: String): T {
|
||||||
|
this.icon = ImageHolder(url)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withIcon(uri: Uri): T {
|
||||||
|
this.icon = ImageHolder(uri)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: StringHolder? = null
|
||||||
|
private set
|
||||||
|
var descriptionTextColor: ColorHolder? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun withDescription(description: String): T {
|
||||||
|
this.description = StringHolder(description)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withDescription(@StringRes descriptionRes: Int): T {
|
||||||
|
this.description = StringHolder(descriptionRes)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withDescriptionTextColor(@ColorInt color: Int): T {
|
||||||
|
this.descriptionTextColor = ColorHolder.fromColor(color)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withDescriptionTextColorRes(@ColorRes colorRes: Int): T {
|
||||||
|
this.descriptionTextColor = ColorHolder.fromColorRes(colorRes)
|
||||||
|
return this as T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a helper method to have the logic for all secondaryDrawerItems only once
|
||||||
|
|
||||||
|
* @param viewHolder
|
||||||
|
*/
|
||||||
|
protected fun bindViewHelper(viewHolder: CustomBaseViewHolder) {
|
||||||
|
val ctx = viewHolder.itemView.context
|
||||||
|
|
||||||
|
//set the identifier from the drawerItem here. It can be used to run tests
|
||||||
|
viewHolder.itemView.id = hashCode()
|
||||||
|
|
||||||
|
//set the item selected if it is
|
||||||
|
viewHolder.itemView.isSelected = isSelected
|
||||||
|
|
||||||
|
//get the correct color for the background
|
||||||
|
val selectedColor = getSelectedColor(ctx)
|
||||||
|
//get the correct color for the text
|
||||||
|
val color = getColor(ctx)
|
||||||
|
val selectedTextColor = getSelectedTextColor(ctx)
|
||||||
|
//get the correct color for the icon
|
||||||
|
val iconColor = getIconColor(ctx)
|
||||||
|
val selectedIconColor = getSelectedIconColor(ctx)
|
||||||
|
|
||||||
|
//set the background for the item
|
||||||
|
UIUtils.setBackground(
|
||||||
|
viewHolder.view,
|
||||||
|
UIUtils.getSelectableBackground(ctx, selectedColor, true)
|
||||||
|
)
|
||||||
|
//set the text for the name
|
||||||
|
StringHolder.applyTo(this.getName(), viewHolder.name)
|
||||||
|
//set the text for the description or hide
|
||||||
|
StringHolder.applyToOrHide(this.description, viewHolder.description)
|
||||||
|
|
||||||
|
//set the colors for textViews
|
||||||
|
viewHolder.name.setTextColor(getTextColorStateList(color, selectedTextColor))
|
||||||
|
//set the description text color
|
||||||
|
ColorHolder.applyToOr(
|
||||||
|
descriptionTextColor,
|
||||||
|
viewHolder.description,
|
||||||
|
getTextColorStateList(color, selectedTextColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
//define the typeface for our textViews
|
||||||
|
if (getTypeface() != null) {
|
||||||
|
viewHolder.name.typeface = getTypeface()
|
||||||
|
viewHolder.description.typeface = getTypeface()
|
||||||
|
}
|
||||||
|
|
||||||
|
//we make sure we reset the image first before setting the new one in case there is an empty one
|
||||||
|
DrawerImageLoader.getInstance().cancelImage(viewHolder.icon)
|
||||||
|
viewHolder.icon.setImageBitmap(null)
|
||||||
|
//get the drawables for our icon and set it
|
||||||
|
ImageHolder.applyTo(icon, viewHolder.icon, "customUrlItem")
|
||||||
|
|
||||||
|
//for android API 17 --> Padding not applied via xml
|
||||||
|
DrawerUIUtils.setDrawerVerticalPadding(viewHolder.view)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
/* 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 android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import apps.amine.bou.readerforselfoss.R
|
||||||
|
import com.mikepenz.materialdrawer.holder.BadgeStyle
|
||||||
|
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||||
|
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable
|
||||||
|
|
||||||
|
class CustomUrlPrimaryDrawerItem :
|
||||||
|
CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(),
|
||||||
|
ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
|
||||||
|
protected var mBadge: StringHolder = StringHolder("")
|
||||||
|
protected var mBadgeStyle = BadgeStyle()
|
||||||
|
|
||||||
|
override fun withBadge(badge: StringHolder): CustomUrlPrimaryDrawerItem {
|
||||||
|
this.mBadge = badge
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withBadge(badge: String): CustomUrlPrimaryDrawerItem {
|
||||||
|
this.mBadge = StringHolder(badge)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withBadge(@StringRes badgeRes: Int): CustomUrlPrimaryDrawerItem {
|
||||||
|
this.mBadge = StringHolder(badgeRes)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withBadgeStyle(badgeStyle: BadgeStyle): CustomUrlPrimaryDrawerItem {
|
||||||
|
this.mBadgeStyle = badgeStyle
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBadge(): StringHolder {
|
||||||
|
return mBadge
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBadgeStyle(): BadgeStyle {
|
||||||
|
return mBadgeStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(): Int {
|
||||||
|
return R.id.material_drawer_item_custom_url_item
|
||||||
|
}
|
||||||
|
|
||||||
|
@LayoutRes
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.material_drawer_item_primary
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindView(viewHolder: ViewHolder, payloads: List<*>?) {
|
||||||
|
super.bindView(viewHolder, payloads)
|
||||||
|
|
||||||
|
val ctx = viewHolder.itemView.context
|
||||||
|
|
||||||
|
//bind the basic view parts
|
||||||
|
bindViewHelper(viewHolder)
|
||||||
|
|
||||||
|
//set the text for the badge or hide
|
||||||
|
val badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge)
|
||||||
|
//style the badge if it is visible
|
||||||
|
if (badgeVisible) {
|
||||||
|
mBadgeStyle.style(
|
||||||
|
viewHolder.badge,
|
||||||
|
getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx))
|
||||||
|
)
|
||||||
|
viewHolder.badgeContainer.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
viewHolder.badgeContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
//define the typeface for our textViews
|
||||||
|
if (getTypeface() != null) {
|
||||||
|
viewHolder.badge.typeface = getTypeface()
|
||||||
|
}
|
||||||
|
|
||||||
|
//call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
|
||||||
|
onPostBindView(this, viewHolder.itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getViewHolder(v: View): ViewHolder {
|
||||||
|
return ViewHolder(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : CustomBaseViewHolder(view) {
|
||||||
|
val badgeContainer: View = view.findViewById(R.id.material_drawer_badge_container)
|
||||||
|
val badge: TextView = view.findViewById(R.id.material_drawer_badge)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.support.v4.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)
|
||||||
|
|
||||||
|
fun Context.bitmapFitCenter(url: String, iv: ImageView) =
|
||||||
|
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) {
|
||||||
|
override fun setResource(resource: Bitmap?) {
|
||||||
|
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
|
||||||
|
resources,
|
||||||
|
resource
|
||||||
|
)
|
||||||
|
circularBitmapDrawable.isCircular = true
|
||||||
|
iv.setImageDrawable(circularBitmapDrawable)
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,33 @@
|
|||||||
|
package apps.amine.bou.readerforselfoss.utils.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.Config
|
||||||
|
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.GlideBuilder
|
||||||
|
import com.bumptech.glide.Registry
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.module.GlideModule
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class SelfSignedGlideModule : GlideModule {
|
||||||
|
|
||||||
|
override fun applyOptions(context: Context?, builder: GlideBuilder?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||||
|
if (pref.getBoolean("isSelfSignedCert", false)) {
|
||||||
|
val client = getUnsafeHttpClient().build()
|
||||||
|
|
||||||
|
registry?.append(
|
||||||
|
GlideUrl::class.java,
|
||||||
|
InputStream::class.java,
|
||||||
|
com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 680 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_bug_report.png
Normal file
After Width: | Height: | Size: 271 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_color_lens_black_24dp.png
Normal file
After Width: | Height: | Size: 458 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_info_outline.png
Normal file
After Width: | Height: | Size: 551 B |
Before Width: | Height: | Size: 953 B |
BIN
app/src/main/res/drawable-hdpi/ic_open_in_browser.png
Normal file
After Width: | Height: | Size: 204 B |
BIN
app/src/main/res/drawable-hdpi/ic_settings.png
Normal file
After Width: | Height: | Size: 498 B |
Before Width: | Height: | Size: 434 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 442 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_bug_report.png
Normal file
After Width: | Height: | Size: 212 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_color_lens_black_24dp.png
Normal file
After Width: | Height: | Size: 268 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_info_outline.png
Normal file
After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 655 B |
BIN
app/src/main/res/drawable-mdpi/ic_open_in_browser.png
Normal file
After Width: | Height: | Size: 157 B |
BIN
app/src/main/res/drawable-mdpi/ic_settings.png
Normal file
After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 307 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 634 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_bug_report.png
Normal file
After Width: | Height: | Size: 312 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_color_lens_black_24dp.png
Normal file
After Width: | Height: | Size: 504 B |
BIN
app/src/main/res/drawable-xhdpi/ic_history.png
Normal file
After Width: | Height: | Size: 684 B |
BIN
app/src/main/res/drawable-xhdpi/ic_info_outline.png
Normal file
After Width: | Height: | Size: 725 B |
Before Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_open_in_browser.png
Normal file
After Width: | Height: | Size: 230 B |
BIN
app/src/main/res/drawable-xhdpi/ic_settings.png
Normal file
After Width: | Height: | Size: 606 B |
Before Width: | Height: | Size: 542 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_search.png
Normal file
After Width: | Height: | Size: 1.2 KiB |