Compare commits

...

424 Commits

Author SHA1 Message Date
AmineB 236e1cca90 Fix recycleview article positions (#163)
Reviewed-on: #163
Reviewed-by: Amine Bouabdallaoui <amineb@hidden.hidden>
2024-11-20 09:32:29 +01:00
davidoskky 3a33cb4510 Provide method to update items in the home
Removed in the previous commit, the item adapter accepts a method to update the articles list in the home page.
Renamed the method to make its objective more clear.
Removed a debugging log.
Reverted change to function name.
2024-11-20 01:28:47 +01:00
davidoskky 0bf9ca9a49 Fix recycleview article positions
The articles were being opened by setting a click listener on the binding.
In card view this was being done through a function and as such it used the overall viewbind of the view rather than the binding of the item viewholder.
Now the functions are more explicit to avoid future errors.
Pulled up a few members from ItemCardAdapter and ItemListAdapter to ItemAdapter.
2024-11-19 01:50:58 +01:00
aminecmi 61e0087894 Changelog for v124041081 [CI SKIP] 2024-04-17 10:57:12 +00:00
aminecmi 1ec05d9913 chore: comment.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
continuous-integration/drone/tag Build is passing
2024-04-17 12:39:45 +02:00
aminecmi 859bd91bbb fix: Last time fixing the parsing date hack before moving it to os version. 2024-04-17 12:22:33 +02:00
aminecmi 204b736c53 Changelog for v124030731 [CI SKIP] 2024-03-13 19:51:14 +00:00
aminecmi f24609c143 fix: Basic auth and password can have non whitspace characters. Fixes 142.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-03-13 20:31:24 +01:00
aminecmi b94d7dc537 Changelog for v124020451 [CI SKIP] 2024-02-14 19:54:05 +00:00
aminecmi 41910cc4cd fix: Fixed handling of position in card adapter.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-02-13 21:31:02 +01:00
aminecmi db166ca9d4 Changelog for v124010301 [CI SKIP] 2024-01-30 19:36:37 +00:00
aminecmi db0d5a4a85 fix: This may fix the oom errors.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-29 20:55:29 +01:00
aminecmi 3bc0d7cf95 Changelog for v124010191 [CI SKIP] 2024-01-19 21:16:23 +00:00
aminecmi 8f464d95fd fix: moving listeners.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-19 21:32:43 +01:00
aminecmi 5ccd6a3368 chore: removed a useless log. 2024-01-19 21:32:43 +01:00
aminecmi cdbded246e Changelog for v124010032 [CI SKIP] 2024-01-03 22:16:35 +00:00
aminecmi 750c7758bd fix: Another date format thing.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 23:07:57 +01:00
aminecmi 22f8b14ecd Changelog for v124010031 [CI SKIP] 2024-01-03 21:46:48 +00:00
aminecmi 6e27d6d4e6 fix: Checking if selfoss instance.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 22:28:57 +01:00
aminecmi 14ff4dbd05 fix: handle three characters lenght hexcode colors. 2024-01-03 21:35:58 +01:00
aminecmi 390c2d0cf3 Changelog for v123113311 [CI SKIP] 2023-11-27 20:48:32 +00:00
aminecmi e58914ef58 chore: Source tracker url in the menu.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-11-27 21:31:46 +01:00
aminecmi a03f08fca1 fix: Handle kodein proguard rules. 2023-11-27 21:31:26 +01:00
aminecmi 8e9b87f00c Changelog for v123102961 [CI SKIP] 2023-10-23 21:11:15 +00:00
aminecmi f765224a86 chore: domain changes.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-21 23:41:17 +02:00
aminecmi 14d2219eb8 Changelog for v123102852 [CI SKIP] 2023-10-12 18:48:06 +00:00
aminecmi 137580ccf9 chore: lint cleaning.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-12 20:38:01 +02:00
aminecmi f101d22f54 Changelog for v123102841 [CI SKIP] 2023-10-12 20:13:17 +02:00
aminecmi 68aedb7641 chore: verbose.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is failing
2023-10-11 21:13:10 +02:00
Amine Louveau 754d526b49 chore: cleaning ci steps and upgrading dependencies.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is failing
## 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.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/150
2023-10-10 20:52:26 +00:00
Amine Louveau c458871569 feat: Self signed ssl support.
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/141
2023-09-17 18:28:47 +00:00
davidoskky 056825aa0c Revert xmlns changes
continuous-integration/drone/pr Build is passing
2023-09-17 12:01:32 +02:00
davidoskky 16b19fc5ce Revert version upgrades 2023-09-17 11:59:08 +02:00
davidoskky 4ad4a23ed8 Revert to private functions
continuous-integration/drone/pr Build is failing
2023-09-12 00:38:37 +02:00
davidoskky d8c215eacc Reintroduce removed parameter 2023-09-12 00:36:56 +02:00
davidoskky 2b446ab22b Revert dependency version changes 2023-09-12 00:36:04 +02:00
davidoskky a029d8a7dc Move api client creation function within api class
continuous-integration/drone/pr Build is passing
2023-09-10 21:33:28 +02:00
davidoskky 4482234e1a Remove unused strings file
continuous-integration/drone/pr Build is passing
2023-09-10 21:15:04 +02:00
davidoskky b5de30f561 Add translations 2023-09-10 21:14:43 +02:00
davidoskky 70ad5f322c Handle most HTTP client creation in common code 2023-09-10 21:14:43 +02:00
davidoskky d167092c83 Add a login switch to disable SSL verification 2023-09-10 20:24:22 +02:00
davidoskky c4f4bafe85 Add a switch in the login screen to disable SSL 2023-07-13 14:55:48 +02:00
davidoskky ed06b22a77 Tentative self signed ssl support 2023-07-13 14:55:48 +02:00
aminecmi 172362b533 Changelog for v123061811 [CI SKIP] 2023-06-30 19:06:44 +00:00
aminecmi ad72cb6f56 feat: Added confirmation dialog for disconnect item menu.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-06-30 20:37:43 +02:00
aminecmi 9057ee0052 Changelog for v123061651 [CI SKIP] 2023-06-14 18:23:59 +00:00
aminecmi 50d0b44315 i18n: Translation update.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-06-13 20:52:42 +02:00
aminecmi 21b08ed384 i18n: Translation update.
continuous-integration/drone/push Build is running
2023-06-13 20:26:36 +02:00
aminecmi 993c4d2ee9 i18n: Translation update.
continuous-integration/drone/push Build is passing
2023-06-12 20:50:26 +02:00
aminecmi 57a9d51027 fix: avoid trying to open invalid image urls.
continuous-integration/drone/push Build is running
2023-06-12 20:34:35 +02:00
aminecmi 673f0edb8b Changelog for v123051471 [CI SKIP] 2023-05-27 19:25:35 +00:00
aminecmi 7f96798f13 fix: images could be null.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-05-27 21:06:56 +02:00
aminecmi 6e5704a45b fix: Check if color is not empty before parsing it. 2023-05-27 21:02:25 +02:00
aminecmi 495591159f chore: Removed unused log. 2023-05-27 21:01:54 +02:00
aminecmi 718fe7c5ee Changelog for v123051331 [CI SKIP] 2023-05-13 20:24:57 +00:00
aminecmi ecd23213f9 fix: illegal input.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-05-13 22:14:25 +02:00
aminecmi e6baed8cb4 Changelog for v123051321 [CI SKIP] 2023-05-12 19:19:35 +00:00
aminecmi c87abec0b9 debug: Debug null context.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-05-12 20:31:40 +02:00
Amine Louveau 0aba41d8bf Changelog for v123051301 [CI SKIP] 2023-05-10 19:36:31 +00:00
Amine Louveau 2a2d1047b4 feat: Basic auth from url. Fixes #142 (#143)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/143
2023-05-10 19:19:11 +00:00
aminecmi 66ef1ccf32 debug: Debug index out of bound exception.
continuous-integration/drone/push Build is failing
2023-05-10 20:50:13 +02:00
aminecmi 677ede5bc7 Changelog for v123051211 [CI SKIP] 2023-05-01 18:26:12 +00:00
aminecmi 996a7ed22c fix: Sometimes url isn't even defined.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-04-30 18:41:15 +02:00
Amine Louveau 85208c4e5a Changelog for v123041021 [CI SKIP] 2023-04-12 19:01:21 +00:00
Amine Louveau 5cfec50cba fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/138
2023-04-12 18:26:37 +00:00
davidoskky 76ad71e1dc Enable Core Library Desugaring to support older Android versions
continuous-integration/drone/pr Build is failing
2023-04-12 16:29:47 +02:00
davidoskky 0277fb507c Changelog for v123030851 [CI SKIP] 2023-03-26 18:21:43 +00:00
davidoskky 8d7d3174aa chore: replace textDrawable library (#136)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
## Types of changes

- [x] I have read the **CONTRIBUTING** document.
- [x] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This is **NOT** translation related.

This closes issue #120

Removed the dependency `com.amulyakhare.textdrawable` and slightly simplified the logic required to set circular images.

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/136
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-03-26 11:12:01 +00:00
aminecmi 00eb3333fe refactor: Remove slow login check. Closes #135.
continuous-integration/drone/push Build is passing
2023-03-25 20:28:30 +01:00
aminecmi 629ca01d99 ci: send the mapping file after a release.
continuous-integration/drone/push Build is passing
2023-03-17 10:10:38 +01:00
aminecmi c2d8681ce8 Changelog for v123030751 [CI SKIP] 2023-03-16 19:41:34 +00:00
aminecmi 08f79cb148 debug: added a lot to pinpoint the url issue.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-16 20:26:36 +01:00
davidoskky e21906e70d feat: Use /sources/stats in the home (#133)
continuous-integration/drone/push Build is passing
## Types of changes

- [x] I have read the **CONTRIBUTING** document.
- [x] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This is **NOT** translation related.

This is implements feature #131 and it will allow implementing #132
With this, public mode functions perfectly and also has source filtering.

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/133
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-03-13 16:26:54 +00:00
aminecmi 9d2cc32bc9 Changelog for v123030681 [CI SKIP] 2023-03-09 20:27:08 +00:00
aminecmi d9d057c8dc fix: Unread and starred can be null.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-09 20:46:09 +01:00
aminecmi 1f3fa0c4a6 Fixed version number issue.
continuous-integration/drone/push Build is failing
2023-03-05 21:02:03 +01:00
aminecmi dea3def385 Changelog for v123030621 [CI SKIP] 2023-03-03 20:07:52 +00:00
aminecmi f72ef2f5d4 fix: url required issue.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-03 20:39:33 +01:00
aminecmi f28cb759df fix: Canvas reused issue. 2023-03-03 20:39:20 +01:00
aminecmi b9d69c3e64 Changelog for v123020572 [CI SKIP] 2023-02-26 19:13:41 +00:00
aminecmi c2a1c9eaac fix: requirecontext issues ?
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-26 16:05:26 +01:00
aminecmi bf37209a15 debug: activity not found exception. 2023-02-26 15:40:58 +01:00
aminecmi 2c558fe6fd Changelog for v123020571 [CI SKIP] 2023-02-26 07:49:16 +00:00
aminecmi ad88011454 chore: remove errors logging.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-25 22:57:59 +01:00
aminecmi 559c17bc1d fix: quickfix for url param not provided for some sources. 2023-02-25 22:46:04 +01:00
Amine Louveau ab9c46f0eb Update 'CHANGELOG.md'
continuous-integration/drone/push Build is passing
2023-02-21 15:00:37 +00:00
Amine Louveau aa799d2ca8 Changelog for v123020523 [CI SKIP] 2023-02-21 14:49:14 +00:00
Amine Louveau 177c978474 fix: Git changelog.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 14:05:32 +00:00
Amine Louveau 39b9991413 Changelog for v123020522 [CI SKIP] 2023-02-21 13:25:11 +00:00
Amine Louveau b303f110f1 fix: still fixing changelog versions.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 12:59:38 +00:00
Amine Louveau f851941a6a Changelog for v123020521 [CI SKIP] 2023-02-21 11:31:18 +00:00
Amine Louveau a313552976 fix: change changelog version on release.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 11:20:43 +00:00
aminecmi 6ac97ed3fe Changelog for v123020491 [CI SKIP] 2023-02-20 09:39:40 +00:00
aminecmi d583b937b7 fix: Fixed acra bug reporting.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-20 10:29:00 +01:00
aminecmi 15b9a2d935 Changelog for v123010301 [CI SKIP] 2023-02-18 20:01:03 +00:00
aminecmi 5a8ce15961 Chore: acra config.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-18 20:38:06 +01:00
davidoskky e1c64cef46 Changelog for v123010281 [CI SKIP] 2023-01-30 19:55:23 +00:00
davidoskky ee064f3cb4 improvement: Improve right to left support (#130)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-01-29 12:55:28 +00:00
davidoskky 3e46e2ff29 Changelog for v123010261 [CI SKIP] 2023-01-28 10:57:06 +00:00
davidoskky f28e702549 feat: Handle public instances (#126)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-01-28 10:25:28 +00:00
aminecmi fc31a4399c ci: Pull request should trigger ci.
continuous-integration/drone/push Build is running
2023-01-28 11:23:00 +01:00
Amine Louveau 9b23053b66 fix: Complete the disconnection before redirecting to the login screen
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/129
2023-01-28 11:20:19 +01:00
davidoskky 389a04d250 Complete the disconnection before redirecting to the login screen 2023-01-27 14:21:33 +01:00
Amine Louveau 40e1d1478b Changelog for v123010241 [CI SKIP] 2023-01-26 10:55:43 +00:00
Amine Louveau 2154ff3c33 Merge pull request 'feat: swipe down to close images' (#122) from davidoskky/ReaderForSelfoss-multiplatform:swipe_down into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/122
2023-01-26 10:42:15 +00:00
davidoskky 2245565f95 Remove unnecessary definition 2023-01-25 21:25:32 +01:00
davidoskky 014858f06b Remove unused import 2023-01-25 20:40:20 +01:00
davidoskky 3f1f86a78e Adjust the image closing animation 2023-01-25 10:34:03 +01:00
davidoskky a549169a7c Add a dark hue to the underlying article when swiping to close images 2023-01-25 10:27:38 +01:00
davidoskky be7cae365a Rename activity style to avoid interferences 2023-01-25 02:09:41 +01:00
davidoskky cef3b2e593 Adapt the style of the image activity to the rest of the application 2023-01-25 01:54:35 +01:00
davidoskky ae927ebc57 Resolve issues when swiping down to close images 2023-01-25 00:46:43 +01:00
Amine Louveau 90532cf501 Changelog for v123010041 [CI SKIP] 2023-01-24 12:41:52 +00:00
Amine Louveau ab0678d61e Merge pull request 'scroll-tag-filters' (#124) from scroll-tag-filters into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/124
2023-01-23 21:42:42 +00:00
aminecmi a1b7d22d26 fix: added POST_NOTIFICATIONS to fix notifications issues.
continuous-integration/drone/push Build is passing
2023-01-23 22:32:52 +01:00
aminecmi 29eae4b1f6 fix: scrollable filter sheet.
continuous-integration/drone/push Build is failing
2023-01-23 21:13:10 +01:00
aminecmi f5bbc63481 enhancement: Ellipsize chips text. 2023-01-23 21:12:56 +01:00
davidoskky ddc72d85b0 Close the image fragment only if the image has been dragged down 2023-01-21 16:37:25 +01:00
davidoskky 68bbf5b2d3 Animate swipe down to close images 2023-01-20 16:36:52 +01:00
aminecmi 95e76a55da Cleaning.
continuous-integration/drone/push Build is passing
2023-01-12 22:01:36 +01:00
davidoskky 2b6659f4ec Swipe down to close images 2023-01-11 22:28:14 +01:00
aminecmi e0c118a73e Changelog for v122123641 [CI SKIP] 2023-01-04 19:27:56 +00:00
aminecmi 4e61b2aed6 feat: Disable the failing source in the filter sheet.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-03 21:35:21 +01:00
aminecmi ba2758c0a3 feat: Display the source error in the sources list. 2023-01-03 21:28:40 +01:00
aminecmi c718b966a1 Changelog for v122123631 [CI SKIP] 2022-12-30 19:24:48 +00:00
aminecmi 99438e142f build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-30 20:10:08 +01:00
aminecmi 4d8076c3cf build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
continuous-integration/drone/push Build is failing
2022-12-29 21:34:13 +01:00
aminecmi db75c5b74a debug: trying to resolve Canvas: trying to use a recycled bitmap.
continuous-integration/drone/push Build is passing
2022-12-29 20:40:39 +01:00
aminecmi 966a082147 fix: NPE may be caused by the binding or the title that was null.
continuous-integration/drone/push Build is running
2022-12-29 20:35:03 +01:00
aminecmi cd20a5ec29 chore: Skip drone pipeline on changelog push.
continuous-integration/drone/push Build was killed
2022-12-29 14:11:29 +01:00
aminecmi cc4c1c9201 Changelog for v122123621
continuous-integration/drone/push Build was killed
2022-12-29 13:08:23 +00:00
aminecmi ff021d572c fix: Automatic CHANGELOG generation.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-29 13:11:23 +01:00
Amine Louveau 89992967be Merge pull request 'Sources Upsert' (#119) from sources-edit into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/119
2022-12-28 21:31:49 +00:00
aminecmi 3c68bde62b Source update screen.
continuous-integration/drone/push Build is passing
2022-12-28 22:19:11 +01:00
aminecmi c38251f5b3 Sources menu. 2022-12-28 21:45:00 +01:00
aminecmi a01f6d2322 chore: Automatic CHANGELOG generation.
continuous-integration/drone/push Build is passing
2022-12-28 21:43:56 +01:00
Amine Louveau 417a33eb25 Merge pull request 'Running migrations.' (#118) from fix-migration into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/118
2022-12-28 14:49:11 +00:00
aminecmi 2e7f7f23b3 No duplicate builds for PRs.
continuous-integration/drone/push Build is passing
2022-12-28 15:34:22 +01:00
aminecmi e5e182761e Running migrations.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-12-28 15:27:17 +01:00
Amine Louveau a094d88799 Merge pull request 'Make the author field nullable' (#117) from davidoskky/ReaderForSelfoss-multiplatform:author into master
continuous-integration/drone/push Build was killed
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/117
2022-12-28 14:25:36 +00:00
davidoskky e51915d1cd Include author field when updating the database
continuous-integration/drone/pr Build is passing
2022-12-28 14:25:56 +01:00
davidoskky 3a654f6ede Migrate the database table 2022-12-28 14:25:34 +01:00
davidoskky 5227751dca Make the author field nullable
continuous-integration/drone/pr Build is passing
2022-12-28 11:02:43 +01:00
aminecmi 27eafe4ff4 Delete sources from DB and reload items on source deletion.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-26 22:27:28 +01:00
aminecmi 8c83a9408b Drone should work better.
continuous-integration/drone/push Build was killed
2022-12-26 22:26:28 +01:00
aminecmi fe2410f719 Handling author field.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-12-26 21:49:55 +01:00
aminecmi a5e86bfb77 Date format issues.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-12-26 15:02:19 +01:00
aminecmi 23be633798 Add api version to the reports.
continuous-integration/drone/push Build is running
2022-12-25 22:45:12 +01:00
aminecmi 813e0707d8 Date format issue.
continuous-integration/drone/push Build is running
2022-12-25 22:41:34 +01:00
aminecmi 9ed9bf07fc Items in repository.
continuous-integration/drone/push Build is passing
2022-12-23 22:53:16 +01:00
aminecmi 47265c10d0 Trying nexus build.
continuous-integration/drone/push Build is passing
2022-12-23 14:59:58 +01:00
aminecmi 5cc633246a Debugging images issues.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-22 20:28:49 +01:00
aminecmi 1f40385786 Context should not be null, but handle the case for now.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-19 22:08:28 +01:00
aminecmi eb2876324a This seems to be needed.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-19 20:47:04 +01:00
aminecmi 633b817d76 Remonving matomo.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-18 21:07:42 +01:00
aminecmi 2cfaa9b285 Logout fix.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-18 20:42:13 +01:00
aminecmi f42ae97326 Explicitly failing for non selfoss rss files.
continuous-integration/drone/push Build is running
2022-12-18 20:41:17 +01:00
aminecmi 3b0028164b Glide update + trying requests.
continuous-integration/drone/push Build is passing
2022-12-17 22:25:23 +01:00
aminecmi 7420adeb5c Do not ignore git version.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-14 21:42:56 +01:00
aminecmi 316027ca3b Tag for build.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-14 21:26:33 +01:00
aminecmi 9d58fba5c9 Cleaning.
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is passing
2022-12-14 21:07:03 +01:00
aminecmi 284c19ef89 More cleaning.
continuous-integration/drone/push Build is passing
2022-12-14 20:54:48 +01:00
aminecmi 7cfd17231a Cleaning.
continuous-integration/drone/push Build is passing
2022-12-13 22:22:31 +01:00
aminecmi 527830a5ae Merge branch 'sonar-qube'
continuous-integration/drone/push Build is running
2022-12-13 21:53:10 +01:00
aminecmi c4ed30f594 Fixes #112.
continuous-integration/drone/push Build is passing
2022-12-13 21:32:48 +01:00
aminecmi 156c1681cf Fixes #111.
continuous-integration/drone/push Build is passing
2022-12-13 21:19:05 +01:00
aminecmi 3593fbca78 Sonar scanner.
continuous-integration/drone/push Build is passing
2022-12-13 21:11:38 +01:00
aminecmi 430fc8e8cb Fixes #110.
continuous-integration/drone/push Build is passing
2022-12-13 20:40:50 +01:00
aminecmi 4fce19bad4 Trying to set code coverage.
continuous-integration/drone/push Build is passing
2022-12-13 20:29:51 +01:00
Amine Louveau 49f5848e7b Merge pull request 'Fixes #108.' (#109) from bug/lateinit into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/109
2022-12-12 20:15:44 +00:00
aminecmi 90452100a4 Fixes #108.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-12-12 21:11:26 +01:00
aminecmi bf1196dd0f Translations.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-09 20:27:38 +01:00
aminecmi 4316dc6516 Removing hidden tags.
continuous-integration/drone/push Build is running
2022-12-09 20:23:01 +01:00
aminecmi 9833a66a64 Cleaning tags duplications.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-08 14:02:30 +01:00
aminecmi 797bf06a9c Retry to fix post login issues.
continuous-integration/drone/push Build is passing
2022-12-08 13:09:02 +01:00
Amine Louveau d98b00533d Merge pull request 'filters' (#107) from filters into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/107
2022-12-07 19:13:46 +00:00
aminecmi bf8f7d8667 Cleaning.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-07 12:11:58 +01:00
aminecmi 89c570f34f Fixing tests.
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-06 22:36:15 +01:00
aminecmi d6a562863a Big cleaning.
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-06 22:18:38 +01:00
aminecmi a02f06fe2e Big drawer cleaning. 2022-12-06 21:39:41 +01:00
aminecmi 7b088d7bb4 Hidden tags. 2022-12-06 21:39:41 +01:00
aminecmi 477883ed39 Tags sources reset. 2022-12-06 21:39:41 +01:00
aminecmi 748ed41096 Filters working. 2022-12-06 21:39:41 +01:00
aminecmi 86c50d4881 Loading sources and tags. 2022-12-06 21:39:41 +01:00
aminecmi c4c92e6dd9 No need to login without password and username.
continuous-integration/drone/push Build is passing
2022-12-06 21:39:32 +01:00
aminecmi 7f0ba193ec Fonts are a pain in the a$$.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-05 21:04:32 +01:00
aminecmi 87ed5b0fa8 Cleaning, and fixing socket timeout log.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-04 20:46:43 +01:00
aminecmi 6947743ac0 More details for silent reports.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-03 21:39:46 +01:00
Amine Louveau 07e3710d44 Merge pull request 'acra' (#104) from acra into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/104
2022-12-01 21:01:18 +00:00
aminecmi e68da7764f Settings for acra and analytics. Closes #98.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-12-01 21:30:20 +01:00
aminecmi c3ff894027 Initial integration. 2022-11-30 20:53:11 +01:00
Amine Louveau f09f731d30 Merge pull request 'Cookies login and logout.' (#103) from login into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/103
2022-11-30 10:04:13 +00:00
aminecmi 956c4341c7 Cookies login and logout.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-11-29 21:38:58 +01:00
aminecmi 7b68264dd7 Cleaning.
continuous-integration/drone/push Build is passing
2022-11-21 20:20:27 +01:00
aminecmi cfcf030bf8 Removing gradle props.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-11-14 13:38:10 +01:00
aminecmi 0e7d7a5835 Conditionnal siteId
continuous-integration/drone/push Build is failing
2022-11-14 13:18:20 +01:00
aminecmi 0856ebb889 Removing matomo url from build config.
continuous-integration/drone/push Build is running
2022-11-14 13:10:17 +01:00
Amine Louveau 25bf68cf0c Merge pull request 'Initial Matomo integration.' (#101) from login into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/101
2022-11-13 13:18:49 +00:00
aminecmi afc6f392c6 Initial Matomo integration.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-11-13 13:13:03 +01:00
Amine Louveau a0b5e2052b Merge pull request 'Fixed theme reload issues.' (#100) from fix-theme-reload into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/100
2022-11-11 20:29:08 +00:00
aminecmi 87d1ef2bce Fixed theme reload issues.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-11-11 20:51:49 +01:00
Amine Louveau 537a6d3a0b Merge pull request 'Checkerboard background for transparency in zoomed images' (#92) from davidoskky/ReaderForSelfoss-multiplatform:checkerboard into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/92
2022-11-11 19:29:40 +00:00
davidoskky dbe97f564e Revert imageview changes
continuous-integration/drone/pr Build is passing
2022-11-11 09:40:36 +01:00
aminecmi 3a3bf03114 Bigger checktile.
continuous-integration/drone/push Build is passing
2022-11-10 21:41:55 +01:00
davidoskky c09a32e9ad Add checkerboard background to the images in the image view
continuous-integration/drone/pr Build is passing
A checkerboard is drawn beneath the image in the imageview to allow
a simpler viewing of images with transparency
2022-11-09 16:39:00 +01:00
davidoskky b02a588dff Add a checkerboard background drawable 2022-11-09 16:34:37 +01:00
Amine Louveau a4527940b8 Merge pull request 'Mercury issues fixing.' (#96) from mercury-common into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/96
2022-11-08 21:17:23 +00:00
aminecmi 9e8a25ed3e Fixing tests.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-11-08 22:02:20 +01:00
aminecmi 8ea46e146b Cleaning.
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-11-08 21:54:12 +01:00
aminecmi 5ecf3c3f87 Mercury api in common code. 2022-11-08 21:31:40 +01:00
aminecmi 325f103417 Timeout.
continuous-integration/drone/push Build is failing
2022-11-08 20:49:18 +01:00
Amine Louveau ab4b1ae644 Merge pull request 'Theme should automatically change on phone settings change.' (#95) from theme-update into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/95
2022-11-08 07:38:54 +00:00
aminecmi 87ea44754e Font update.
continuous-integration/drone/push Build is passing
2022-11-07 22:36:20 +01:00
aminecmi 04dec50808 Theme should automatically change on phone settings change.
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2022-11-07 22:07:35 +01:00
Amine Louveau e36189e2e7 Merge pull request 'About config upgrade.' (#93) from aboutconfig into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/93
2022-11-05 21:20:17 +00:00
aminecmi d6bdf510a4 About config upgrade.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-11-05 22:00:16 +01:00
Amine Louveau a464e93370 Merge pull request 'Immediately update bottom badges after reading or starring articles' (#91) from davidoskky/ReaderForSelfoss-multiplatform:badges into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/91
2022-11-02 19:06:48 +00:00
davidoskky 4b63afe62a Update badges tests
continuous-integration/drone/pr Build is passing
2022-11-01 21:51:46 +01:00
davidoskky ac4c4b9441 Merge branch 'master' into badges
continuous-integration/drone/pr Build is failing
2022-11-01 20:35:13 +00:00
Amine Louveau 16b10dc1b7 Merge pull request 'Show all sources in the sources list' (#90) from davidoskky/ReaderForSelfoss-multiplatform:sources into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/90
2022-11-01 20:30:30 +00:00
davidoskky 02d734eee8 Do not edit the repository items from outside the repository
continuous-integration/drone/pr Build is running
2022-11-01 21:29:04 +01:00
davidoskky c5cdfc0d53 Update bottom bar badges through a state flow 2022-11-01 21:28:14 +01:00
davidoskky 6d610ed61a Fix repeating items in recyclerview
continuous-integration/drone/pr Build is passing
2022-11-01 19:53:22 +01:00
davidoskky 792950be7c Remove unreachable condition 2022-11-01 19:52:43 +01:00
Amine Louveau af8969ce4a Merge pull request 'Cleaning.' (#88) from cleaning into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/88
2022-10-31 20:42:20 +00:00
aminecmi 27c55e59a1 Cleaning still.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-10-31 21:28:11 +01:00
aminecmi 94a0747947 More cleaning.
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2022-10-30 22:02:07 +01:00
aminecmi d862bfba4f Still cleaning.
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-30 21:38:04 +01:00
aminecmi b0d1d9c29a No daemon
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-30 21:27:53 +01:00
aminecmi 7b40a31979 Cleaning.
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-30 21:21:43 +01:00
aminecmi 823a8c3692 Date formatter 2022-10-30 21:12:01 +01:00
aminecmi 5494978db8 Cleaning.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-10-29 22:58:25 +02:00
Amine Louveau 6076eb1cee Merge pull request 'chore/mock-multiplatform' (#86) from chore/mock-multiplatform into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/86
2022-10-29 12:10:51 +00:00
aminecmi 131101d2ee Date api issues.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-10-29 13:50:00 +02:00
aminecmi 62ad1f45ba Unit tests are on the android side.
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-10-29 13:37:46 +02:00
aminecmi 402d18b889 Versions update. SettingsKey constants. 2022-10-28 20:32:23 +02:00
Amine Louveau e32699c93f Merge pull request 'ios/init' (#85) from ios/init into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/85
2022-10-24 18:18:16 +00:00
aminecmi 059a237b99 Commenting tests for now.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-10-23 20:39:09 +02:00
aminecmi d2bdbae6c8 Launching ios APP. 2022-10-23 20:38:43 +02:00
Amine Louveau 510fcbe47e Merge pull request 'Prevent crash when logging in' (#81) from davidoskky/ReaderForSelfoss-multiplatform:login_crash into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/81
2022-10-22 19:53:59 +00:00
davidoskky 667e9c1a5d Adjust tests to changes in the repository
continuous-integration/drone/pr Build is passing
2022-10-21 22:56:35 +02:00
davidoskky 53b1d1f8b2 Rework repository initialization 2022-10-21 22:42:32 +02:00
davidoskky c25e8889a4 Prevent crash when logging in
continuous-integration/drone/pr Build was killed
2022-10-17 19:35:52 +02:00
Amine Louveau 8b0bbe71c9 Merge pull request 'Allow offline filtering' (#75) from davidoskky/ReaderForSelfoss-multiplatform:offline_filters into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/75
2022-10-14 07:18:42 +00:00
davidoskky 8bfe14c019 Actually filter database items
continuous-integration/drone/pr Build is passing
2022-10-14 00:10:35 +02:00
davidoskky 208babbce3 Correct tests 2022-10-14 00:03:20 +02:00
davidoskky 02098a7aa9 Rearrange filtering steps
continuous-integration/drone/pr Build is passing
2022-10-11 00:52:12 +02:00
davidoskky d0a982f385 Add tests for offline filtering
continuous-integration/drone/pr Build is passing
2022-10-08 17:15:41 +02:00
davidoskky 1d1c121aab Filter items from database according to tag and source 2022-10-08 17:15:22 +02:00
davidoskky fe12819163 Correct database source title 2022-10-08 17:14:12 +02:00
Amine Louveau 023a30c008 Merge pull request 'Simplify sources and tags handling' (#70) from davidoskky/ReaderForSelfoss-multiplatform:drawer_data into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/70
2022-10-04 18:52:39 +00:00
Amine Louveau a2862a2587 Merge pull request 'Correct mechanism of mark and unmark snackbars' (#74) from davidoskky/ReaderForSelfoss-multiplatform:snackbar-mark into master
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/74
2022-10-04 18:43:44 +00:00
Amine Louveau 054e936657 Merge pull request 'Correct boolean serialization' (#73) from davidoskky/ReaderForSelfoss-multiplatform:swiping into master
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/73
2022-10-04 18:40:14 +00:00
davidoskky 1d2e5069b8 Avoid double snackbar generation
continuous-integration/drone/pr Build is passing
2022-10-04 16:47:13 +02:00
davidoskky a147646743 Correct mechanism of mark and unmark snackbars
continuous-integration/drone/pr Build is running
2022-10-04 16:43:21 +02:00
davidoskky 32e7fc0038 Correct boolean serialization
continuous-integration/drone/pr Build is passing
2022-10-04 15:01:22 +02:00
davidoskky c15bf44032 Adjust repository tests
continuous-integration/drone/pr Build is passing
2022-10-02 01:01:39 +02:00
davidoskky 0bcd55bd4e Add translated strings
continuous-integration/drone/pr Build is failing
2022-10-01 22:51:09 +02:00
davidoskky ebef0b3511 Start monitoring connectivity status when the repository is initiated.
continuous-integration/drone/pr Build is running
2022-10-01 22:43:48 +02:00
davidoskky 713ceb05bf Remove unnecessary data class
continuous-integration/drone/pr Build is failing
2022-09-30 15:07:17 +02:00
davidoskky dc8381b661 Add missing string 2022-09-30 15:00:25 +02:00
davidoskky b5b820c64b Remove database access from the Home 2022-09-30 15:00:01 +02:00
davidoskky f7055626d9 Start monitoring the connectivity before loading the Repository 2022-09-30 14:56:10 +02:00
Amine Louveau 6ec3e96909 Merge pull request 'Repository Unit Tests' (#50) from davidoskky/ReaderForSelfoss-multiplatform:repository_tests into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/50
2022-09-30 11:31:55 +00:00
davidoskky 22da30eaa8 Remove unnecessary call to api
continuous-integration/drone/pr Build is passing
2022-09-30 13:17:40 +02:00
davidoskky 79fd115f5e Only return new cached items 2022-09-30 13:16:42 +02:00
davidoskky 8dc3d319cd Cleanup
continuous-integration/drone/pr Build is failing
2022-09-30 11:59:08 +02:00
davidoskky 27bb056397 Cleanup
continuous-integration/drone/pr Build is running
2022-09-30 11:49:31 +02:00
davidoskky f9ba13dc32 Always cache images in background
continuous-integration/drone/pr Build is passing
2022-09-30 11:23:43 +02:00
davidoskky 6f60ef4346 Remove unnecessary return value
continuous-integration/drone/pr Build is failing
2022-09-30 09:11:55 +02:00
davidoskky 28b950f467 Merge branch 'master' into repository_tests
continuous-integration/drone/pr Build is running
2022-09-30 07:04:09 +00:00
davidoskky a9c7ec3dc1 Cache items in background without filtering
continuous-integration/drone/pr Build is passing
2022-09-30 01:19:28 +02:00
davidoskky 920d4ac1ef Correctly implement disabling sources update 2022-09-29 19:37:33 +02:00
davidoskky 0e96d313ec Add tags parameters explicitly 2022-09-29 19:09:09 +02:00
davidoskky 7211fdb1a3 Fix update remote tests 2022-09-29 18:58:00 +02:00
aminecmi 381d6acc82 Analyzing should be with the rest.
continuous-integration/drone/push Build is passing
2022-09-29 15:34:12 +02:00
davidoskky d311c2cdeb Fix sources tests 2022-09-28 18:45:21 +02:00
davidoskky 219cae5d74 Fix tags tests 2022-09-28 18:22:06 +02:00
davidoskky 2968aee309 Fix badges tests 2022-09-28 09:43:28 +02:00
davidoskky 6cb4b35c93 Introduce useful assertions in repository instantiation tests 2022-09-28 09:14:47 +02:00
davidoskky 15ec0f2d26 Merge branch 'master' into repository_tests
continuous-integration/drone/pr Build is failing
2022-09-28 06:57:02 +00:00
davidoskky 4781e30da2 Remove unnecessary safe calls
continuous-integration/drone/pr Build is passing
2022-09-27 23:44:42 +02:00
davidoskky c8759cc035 Fix tags tests
continuous-integration/drone/pr Build is running
2022-09-27 23:37:30 +02:00
davidoskky cb4f2f02ef Fix repository.tags() returning null 2022-09-27 23:26:44 +02:00
davidoskky 7517626ab7 Include database return definition within test function 2022-09-27 23:25:47 +02:00
davidoskky 41c951b659 Add test cases for repository instantiation cases
continuous-integration/drone/pr Build is passing
2022-09-27 23:16:30 +02:00
aminecmi ad279c6683 Translation.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-09-27 16:57:13 +02:00
aminecmi 5f0817ddb7 Formating issue.
continuous-integration/drone/push Build is running
2022-09-27 16:54:57 +02:00
aminecmi 7124cbcacd Fixed issue with drone failing silently.
Analyzation is now detached.
2022-09-27 16:53:18 +02:00
aminecmi 2a710a1a08 Translations.
continuous-integration/drone/push Build is running
2022-09-27 16:50:06 +02:00
Amine Louveau 82ec2445a1 Merge pull request 'chore/theme-cleaning' (#69) from chore/theme-cleaning into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/69
2022-09-27 14:37:23 +00:00
aminecmi cabb6d494d Cleaning.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-27 15:09:33 +02:00
aminecmi 5c12481813 Article viewer theming.
continuous-integration/drone/push Build is passing
2022-09-27 12:36:01 +02:00
aminecmi b16f86dda1 All theme issues should be resolved.
continuous-integration/drone/push Build is passing
2022-09-27 12:02:59 +02:00
aminecmi 2bc28db2cc Removed chrome custom tabs. 2022-09-27 11:23:16 +02:00
aminecmi bf1b680b4a Daark theme working.
continuous-integration/drone/push Build is passing
2022-09-27 10:21:42 +02:00
davidoskky e2afff0b8e Add comment
continuous-integration/drone/pr Build is passing
2022-09-26 23:19:31 +02:00
davidoskky a382fc89ea Test item caching
continuous-integration/drone/pr Build is running
2022-09-26 23:11:26 +02:00
davidoskky 3f0a3903ae Test refresh login information
continuous-integration/drone/pr Build is passing
2022-09-26 22:50:55 +02:00
davidoskky f46f98cef0 Test login
continuous-integration/drone/pr Build is running
2022-09-26 22:46:37 +02:00
davidoskky bf6f1a917e Test update remote
continuous-integration/drone/pr Build is running
2022-09-26 22:42:24 +02:00
davidoskky 71c0a4d340 Test delete source
continuous-integration/drone/pr Build is passing
2022-09-26 22:26:01 +02:00
davidoskky 63c550ead3 Test create source
continuous-integration/drone/pr Build is running
2022-09-26 22:21:48 +02:00
Amine Louveau d81fb79b4f Merge pull request 'enhancement/too-many-calls' (#68) from enhancement/too-many-calls into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/68
2022-09-26 18:26:22 +00:00
aminecmi 144067d5b6 This helps a lot.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build was killed
2022-09-26 16:16:40 +02:00
aminecmi 8106faa45c Only fetch intems if item caching is enabled. 2022-09-26 16:05:07 +02:00
aminecmi d9ef301e0f Warning. 2022-09-26 15:58:05 +02:00
aminecmi 90b52232ab Updating ktor and changing the engine. This speeds things a little bit too. 2022-09-26 15:54:11 +02:00
aminecmi 0f000ea359 Only sort if the data is from the DB. This speeds things a little bit. 2022-09-26 15:53:39 +02:00
aminecmi fb8f81a4c8 One call less.
continuous-integration/drone/push Build is passing
2022-09-26 14:05:57 +02:00
Amine Louveau a76b3dd2a9 Merge pull request 'Theme refresh' (#65) from feature/55 into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/65
2022-09-26 06:54:28 +00:00
aminecmi 91aed5a777 Hacky way to fix #55.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-25 22:19:47 +02:00
davidoskky 366b2e10f1 Adjust tests to changes in the data models
continuous-integration/drone/pr Build is passing
2022-09-25 22:02:25 +02:00
aminecmi 8823cc6c6c Cleaning. 2022-09-25 21:39:57 +02:00
davidoskky d2436bb976 Merge branch 'master' into repository_tests
# Conflicts:
#	.drone.yml
2022-09-25 20:24:46 +02:00
Amine Louveau 74ef4da15b Merge pull request '2.18 serialization issues fix. Fixes #61.' (#64) from bugfix/old-version-serialization into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/64
2022-09-24 20:16:39 +00:00
aminecmi bd96c67788 2.18 serialization issues fix. Fixes #61.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
continuous-integration/drone Build was killed
2022-09-24 21:55:25 +02:00
Amine Louveau da71de6806 Merge pull request 'bugfix/ktor-404' (#62) from bugfix/ktor-404 into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/62
2022-09-24 14:55:44 +00:00
aminecmi 0264da8ccc Fixing #54.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-24 15:00:33 +02:00
aminecmi 270d959ee0 Cleaning.
continuous-integration/drone/push Build is running
2022-09-24 14:55:06 +02:00
aminecmi 6d11dfb80c Fixing #59. 2022-09-24 14:43:24 +02:00
aminecmi 4184bbb900 Update gradle. 2022-09-24 13:54:48 +02:00
davidoskky ef994460c1 get sources tests
continuous-integration/drone/pr Build is failing
2022-09-18 18:14:14 +02:00
davidoskky 758708e18d Tags tests
continuous-integration/drone/pr Build is failing
2022-09-17 22:04:24 +02:00
davidoskky c0381144d1 Add CI test step
continuous-integration/drone/pr Build is failing
2022-09-17 21:29:37 +02:00
davidoskky cda3ba6cb4 Test badge fetching
continuous-integration/drone/pr Build is passing
2022-09-16 12:04:05 +02:00
davidoskky a4636cc0c8 Add item fetching tests
continuous-integration/drone/pr Build is passing
2022-09-15 14:07:50 +02:00
aminecmi 4c12c9d570 added badge.
continuous-integration/drone/push Build is passing
2022-09-14 14:54:13 +02:00
Amine Louveau f4db02521d drone-sign (#57)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
## 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.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/57
2022-09-14 10:32:00 +00:00
aminecmi 01763556b1 Fixed release build issues.
continuous-integration/drone/push Build is running
continuous-integration/drone/tag Build is running
2022-09-13 09:00:54 +02:00
davidoskky 60c24fc75a Check that the api is being used rather than the db
continuous-integration/drone/pr Build is passing
2022-09-10 09:37:14 +02:00
davidoskky 5853a19937 Normal items fetch test
continuous-integration/drone/pr Build is passing
2022-09-10 09:08:26 +02:00
davidoskky 99f2c04bf6 Initial testing setup
continuous-integration/drone/pr Build is passing
2022-09-09 13:43:53 +02:00
aminecmi e2411c00d8 Shorter description.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-09-08 19:00:19 +02:00
aminecmi 0473a5f7bc Changelog.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-09-07 21:43:46 +02:00
aminecmi d0d6a4378c Trying to fix fdroid build.
continuous-integration/drone/tag Build is passing
2022-09-07 21:24:38 +02:00
aminecmi 1dfa3c9f07 Version scripts.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-09-07 20:24:19 +02:00
Amine Louveau 815f00e764 Merge pull request 'Correctly handle the back button in settings' (#48) from davidoskky/ReaderForSelfoss-multiplatform:Settings_back into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/48
2022-09-06 18:20:38 +00:00
davidoskky bdc77ab8ef Remove unused lambda
continuous-integration/drone/pr Build is passing
continuous-integration/drone/tag Build is passing
2022-09-06 12:36:37 +02:00
davidoskky 6bd06cb458 Correctly handle the back button in settings
continuous-integration/drone/pr Build is running
2022-09-06 12:28:31 +02:00
aminecmi e9e8bee6c9 Disable all the backups.
continuous-integration/drone/push Build is passing
2022-08-29 15:37:43 +02:00
aminecmi ff6038dbd4 Removing the backup function for security reasons.
continuous-integration/drone/push Build is passing
2022-08-28 13:41:48 +02:00
Amine Louveau 8146cff011 Merge pull request 'Closes #44' (#46) from chore/cleaning_drawer_data into master
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/46
2022-08-28 11:41:16 +00:00
aminecmi fc4c48dd12 Closes #44
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2022-08-28 10:30:03 +02:00
aminecmi 94f1ec943c Cleaning.
continuous-integration/drone/push Build is passing
2022-08-27 15:09:25 +02:00
Amine Louveau 0278540fb2 Merge pull request 'feature/api_timeout_and_settings_cleaning' (#45) from feature/api_timeout_and_settings_cleaning into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/45
2022-08-27 12:20:54 +00:00
aminecmi 109050d0cf Fixing a todo.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2022-08-27 13:06:16 +02:00
aminecmi 0392e1b406 Typo fix. 2022-08-27 13:00:19 +02:00
aminecmi 8b2be5c55c Refreshing settings.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-08-26 23:39:30 +02:00
aminecmi e6afb4702d Cleaning code. 2022-08-26 23:37:02 +02:00
aminecmi 8cf462a755 Fixing api version issue on login. 2022-08-26 23:37:02 +02:00
aminecmi 17293e5574 Removing self signed certificate (Closes #43) and removing http login (Closes #37) 2022-08-26 23:37:02 +02:00
aminecmi a99286a9b7 Really big settings cleaning. 2022-08-26 23:37:02 +02:00
aminecmi 5531034086 WIP: Big settings cleaning. 2022-08-26 23:37:01 +02:00
Amine Louveau fbcb428e96 Merge pull request 'Fetch api version on login' (#39) from davidoskky/ReaderForSelfoss-multiplatform:loginApi into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/39
2022-08-25 02:25:27 +00:00
davidoskky e281751bb0 Fetch api version on login
continuous-integration/drone/pr Build is passing
2022-08-24 23:26:49 +02:00
aminecmi 0eed9a8d07 Translations.
continuous-integration/drone/push Build is passing
2022-08-24 14:41:57 +02:00
Amine Louveau 9603860bae Merge pull request 'chore/sonarqube-fixes' (#35) from chore/sonarqube-fixes into master
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/35
2022-08-24 12:37:37 +00:00
aminecmi 75b566a38d Some cleaning to handle actions on connection restore.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-08-24 13:58:24 +02:00
aminecmi fb572dbb27 Closes #36
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-08-24 13:34:05 +02:00
aminecmi 34028949d7 More code cleaning.
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2022-08-23 22:52:36 +02:00
aminecmi 44a0469b17 Fixes #25;
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is running
2022-08-23 22:46:48 +02:00
aminecmi c87473e8f1 Cache from settings. 2022-08-23 22:45:19 +02:00
aminecmi de43abf019 Fixed mercury issues. 2022-08-23 22:45:19 +02:00
aminecmi e60f3a9d91 Push everywhere ? 2022-08-23 22:45:19 +02:00
aminecmi 255fbcb12f Added build cache. 2022-08-23 22:45:19 +02:00
aminecmi 0caeb94e64 Translations.
continuous-integration/drone/push Build was killed
2022-08-23 22:43:02 +02:00
Amine Louveau 6f6a42b878 Merge pull request 'DB caching.' (#33) from feature/sqldelight into master
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/33
2022-08-23 20:39:19 +00:00
aminecmi 67d54f0dd7 Big code cleaning.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2022-08-23 22:16:54 +02:00
aminecmi 437aa0abec Big code cleaning.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-08-23 22:03:33 +02:00
aminecmi 216c639a23 Closes #23
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2022-08-23 20:47:07 +02:00
aminecmi 7258452625 Closes #23
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-08-23 20:38:53 +02:00
aminecmi d0d82751e2 Filtering DB items.
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2022-08-23 20:34:20 +02:00
aminecmi 3b8f4991e9 Inserting items in the DB.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-08-23 16:56:04 +02:00
aminecmi 2547ce824a Repository should be DB ok.
continuous-integration/drone/push Build is passing
2022-08-23 16:19:24 +02:00
aminecmi 59eb399cfa File renaming.
continuous-integration/drone/push Build is passing
2022-08-23 15:17:47 +02:00
aminecmi 495b101355 Replacing room with sqldelight. Big cleaning.
continuous-integration/drone/push Build is failing
2022-08-23 15:12:01 +02:00
aminecmi afcc55e907 Urls.
continuous-integration/drone/push Build is passing
2022-08-22 22:07:52 +02:00
aminecmi 0c570efc47 Urls.
continuous-integration/drone/push Build was killed
2022-08-22 22:04:07 +02:00
aminecmi a23a4cea0e Translation.
continuous-integration/drone/push Build was killed
2022-08-22 21:58:15 +02:00
aminecmi 78cb5d047f Translations.
continuous-integration/drone/push Build was killed
2022-08-22 21:53:31 +02:00
Amine Louveau a9caaefb4d Merge pull request 'network' (#28) from davidoskky/ReaderForSelfoss-multiplatform:network into master
continuous-integration/drone/push Build is failing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/28
2022-08-22 19:01:15 +00:00
davidoskky 4c78b22614 Use the connectivity-status library from the repository rather than the local copy
continuous-integration/drone/pr Build is failing
2022-08-22 19:33:58 +02:00
davidoskky 1d5ab3205e Localize strings
continuous-integration/drone/pr Build is failing
2022-08-21 23:34:47 +02:00
davidoskky df4903cae5 Include the connectivity status library as a aar file 2022-08-21 23:34:47 +02:00
davidoskky 2a78be69b5 Send toast messages at the application level and not on a per activity basis 2022-08-21 23:34:45 +02:00
davidoskky 8c69bb8c3c Send a message regarding connectivity loss/retrieval on all activities 2022-08-21 23:34:17 +02:00
davidoskky 9203012a97 Include a local copy of the connectivity-status library to solve a bug 2022-08-21 23:34:17 +02:00
davidoskky 2a44162c5a Send toast messages to the home activity on connectivity changes 2022-08-21 23:34:17 +02:00
davidoskky 20588aab81 Add comment to remember the problem with the connectivity-status library 2022-08-21 23:34:17 +02:00
davidoskky 0c8e49214f Don't reset offline override before updating remote 2022-08-21 23:34:17 +02:00
davidoskky 97d5063339 Consider offline override before updating remote 2022-08-21 23:34:17 +02:00
davidoskky 7c37b183d7 Refactor functions 2022-08-21 23:34:17 +02:00
davidoskky 82c4a5a1f9 Don't send toast messages from the repository 2022-08-21 23:34:17 +02:00
davidoskky 47b7062e16 Remove unused function 2022-08-21 23:34:17 +02:00
davidoskky b9497ca939 Prepare the repository functions for DB implementation 2022-08-21 23:34:17 +02:00
davidoskky 1258ed3ad3 Don't create the mercury api if not connection is available 2022-08-21 23:34:17 +02:00
davidoskky d838f509d4 Stop monitoring the network when the app goes in background 2022-08-21 23:34:17 +02:00
davidoskky 3c5b606a02 Do not change the network override from within the repository 2022-08-21 23:34:17 +02:00
davidoskky d1481a1db6 Reintroduce network checks where required 2022-08-21 23:34:17 +02:00
davidoskky d654b1b0bd Refactor connectivity check 2022-08-21 23:34:17 +02:00
davidoskky f56861a3c2 Show a message when the network connection is lost 2022-08-21 23:34:17 +02:00
davidoskky 492e7e4aed Update todo comments 2022-08-21 23:34:17 +02:00
davidoskky 551a3e3caa Remove all connectivity checks outside the repository 2022-08-21 23:34:17 +02:00
davidoskky c224b8a0b3 Remove network checks from the home activity 2022-08-21 23:34:17 +02:00
davidoskky 13ea7a693b Do not fake offline mode when updating remote 2022-08-21 23:34:17 +02:00
davidoskky 0f3c48dd8e Handle the offline override in the repository 2022-08-21 23:34:17 +02:00
davidoskky d4c2373bac Simplify network connectivity status check 2022-08-21 23:34:17 +02:00
davidoskky 4f32097821 Perform network connectivity checks in the repository 2022-08-21 23:34:17 +02:00
davidoskky 37fa4a1a8e Add multiplatform connectivity check 2022-08-21 23:34:17 +02:00
Amine Louveau 112194dd4f Merge pull request 'Implement logging in the android application' (#32) from davidoskky/ReaderForSelfoss-multiplatform:logging into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/32
2022-08-20 18:18:48 +00:00
davidoskky 72d9ef92d2 Implement logging in the android application
continuous-integration/drone/pr Build is passing
2022-08-20 12:29:04 +02:00
Amine Louveau 1392e2a571 Merge pull request 'Fixing some sonarqube issues.' (#30) from chore/sonarqube-fixes into master
continuous-integration/drone/push Build was killed
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/30
2022-08-17 19:10:47 +00:00
aminecmi e9cb3d2f37 Fixing some sonarqube issues.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-08-17 21:00:58 +02:00
Amine Louveau dec620a409 Merge pull request 'Changed ids to items.' (#29) from id-to-int into master
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/29
2022-08-17 14:31:11 +00:00
aminecmi 4d29ee0b92 Last fixes.
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build was killed
2022-08-17 16:16:11 +02:00
aminecmi 33333ca998 This may work.
continuous-integration/drone/push Build is passing
2022-08-17 14:52:03 +02:00
aminecmi 8d87eef0fc More fixes.
continuous-integration/drone/push Build is failing
2022-08-17 14:24:28 +02:00
aminecmi 5a26513ed7 These params need to be here too.
continuous-integration/drone/push Build is failing
2022-08-17 14:06:56 +02:00
aminecmi 5b7f5225d8 Can't be detached because of a lock file.
continuous-integration/drone/push Build was killed
2022-08-17 14:03:32 +02:00
aminecmi 03f53bf9c9 Detached scan 2022-08-17 11:04:44 +02:00
aminecmi e06e6d580d Detached scan.
continuous-integration/drone/push Build is failing
2022-08-17 11:00:02 +02:00
aminecmi 63e8649512 Fixing issues with build.
continuous-integration/drone/push Build is running
2022-08-17 10:50:04 +02:00
aminecmi 6260c3fc06 Fixes and drone build should work.
continuous-integration/drone/push Build is failing
2022-08-17 10:43:56 +02:00
209 changed files with 8239 additions and 7770 deletions
+162 -12
View File
@@ -1,20 +1,170 @@
kind: pipeline
type: docker
name: test
steps:
- name: Lint
failure: ignore
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Install linters..."
- curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- echo "---------------------------------------------------------"
- echo "Linting..."
- ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
- echo "---------------------------------------------------------"
- echo "Detecting..."
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
- echo "---------------------------------------------------------"
command_timeout: 1m
- name: BuildAndTest
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Configure java..."
- . ~/.bash_profile
- jenv global 17.0
- java --version
- date
- echo "---------------------------------------------------------"
- echo "Building and testing..."
- ./gradlew build
- echo "---------------------------------------------------------"
trigger:
event:
- push
- pull_request
---
kind: pipeline
type: docker
name: Publish
steps:
- name: createTagAndChangelog
image: ubuntu:latest
commands:
- apt-get update && apt-get install -y git
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]"
environment:
TZ: Europe/Paris
- name: git-push
image: appleboy/drone-git-push
settings:
branch: master
remote:
from_secret: remoteUrl
followtags: true
ssh_key:
from_secret: privateKey
skip_verify: true
- name: scpFiles
image: appleboy/drone-scp
settings:
host: amine-bouabdallaoui.fr
username: ubuntu
key:
from_secret: privateKey
port: 22
target: /home/ubuntu/
source: version.txt
- name: deploy
image: appleboy/drone-ssh
settings:
host: amine-bouabdallaoui.fr
user: ubuntu
key:
from_secret: privateKey
command_timeout: 2m
script:
- cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
trigger:
event:
- promote
target:
- production
---
kind: pipeline
type: docker
name: Release
steps:
- name: build
image: mingc/android-build-box:latest
commands:
- ./gradlew build
- name: code-analysis
image: mingc/android-build-box:latest
failure: ignore
commands:
- ls -la
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.sources=. -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
- echo "---------------------------------------------------------"
- echo "Fetch tags..."
- git fetch --tags
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease
- echo "---------------------------------------------------------"
- echo "Get Key"
- wget https://amine-bouabdallaoui.fr/key
- echo "---------------------------------------------------------"
- echo "Zipalign"
- $ANDROID_HOME/build-tools/31.0.0/zipalign -f -v 4 androidApp/build/outputs/apk/githubConfig/release/androidApp-githubConfig-release-unsigned.apk androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- echo "---------------------------------------------------------"
- echo "Sign"
- $ANDROID_HOME/build-tools/31.0.0/apksigner sign -v --out signed.apk --ks ./key --ks-key-alias $YOUR_KEY_ALIAS --ks-pass pass:$YOUR_KEYSTORE_PASSWORD --v1-signing-enabled true --v2-signing-enabled true androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- echo "---------------------------------------------------------"
- echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment:
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass
YOUR_KEY_ALIAS:
from_secret: keyAlias
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: giteaAPI
base_url: https://gitea.amine-bouabdallaoui.fr
files: signed.apk
- name: notify
image: drillster/drone-email
failure: ignore
settings:
host:
from_secret: smtpHOST
port:
from_secret: smtpPORT
username:
from_secret: smtpUSERNAME
password:
from_secret: smtpPASSWORD
from:
from_secret: smtpFROM
subject: Mapping file
recipients:
from_secret: smtpTO
recipients_only: true
skip_verify: true
attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
trigger:
event:
- tag
+2 -27
View File
@@ -10,7 +10,7 @@ Please read the guidelines before contributing, and follow them (or try to) when
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)
You can fork the repository, and [help me solve some issues](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues)
### What I can't help you with.
@@ -28,7 +28,7 @@ Always check if the web version of your instance is working.
### Pull requests
* Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why.
* Don't create a PR for translations.
* 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.
@@ -46,28 +46,3 @@ Always check if the web version of your instance is working.
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
All the details to need are [here](https://selfoss.aditu.de/).
# Build the project
You can directly import this project into IntellIJ/Android Studio.
You'll have to:
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
### Examples:
#### Inside ~/.gradle/gradle.properties
```
appLoginUrl="URL" # It can be empty.
appLoginUsername="LOGIN" # It can be empty.
appLoginPassword="PASS" # It can be empty.
```
#### As gradle parameters
```
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
```
+1 -1
View File
@@ -5,7 +5,7 @@
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654))
- [ ] This is **NOT** translation related.
This closes issue #XXX
+2
View File
@@ -319,3 +319,5 @@ fabric.properties
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
crowdin.properties
+292
View File
@@ -1,3 +1,295 @@
**v124041081**
- chore: comment.
- fix: Last time fixing the parsing date hack before moving it to os version.
- Changelog for v124030731 [CI SKIP]
--------------------------------------------------------------------
**v124030731**
- fix: Basic auth and password can have non whitspace characters. Fixes 142.
- Changelog for v124020451 [CI SKIP]
--------------------------------------------------------------------
**v124020451**
- fix: Fixed handling of position in card adapter.
- Changelog for v124010301 [CI SKIP]
--------------------------------------------------------------------
**v124010301**
- fix: This may fix the oom errors.
- Changelog for v124010191 [CI SKIP]
--------------------------------------------------------------------
**v124010191**
- fix: moving listeners.
- chore: removed a useless log.
- Changelog for v124010032 [CI SKIP]
--------------------------------------------------------------------
**v124010032**
- fix: Another date format thing.
- Changelog for v124010031 [CI SKIP]
--------------------------------------------------------------------
**v124010031**
- fix: Checking if selfoss instance.
- fix: handle three characters lenght hexcode colors.
- Changelog for v123113311 [CI SKIP]
--------------------------------------------------------------------
**v123113311**
- chore: Source tracker url in the menu.
- fix: Handle kodein proguard rules.
- Changelog for v123102961 [CI SKIP]
--------------------------------------------------------------------
**v123102961**
- chore: domain changes.
- Changelog for v123102852 [CI SKIP]
--------------------------------------------------------------------
**v123102852**
- chore: lint cleaning.
- Changelog for v123102841 [CI SKIP]
--------------------------------------------------------------------
**v123102841**
- chore: cleaning ci steps and upgrading dependencies.
- feat: Self signed ssl support.
- Changelog for v123061811 [CI SKIP]
--------------------------------------------------------------------
**v123061811**
- feat: Added confirmation dialog for disconnect item menu.
- Changelog for v123061651 [CI SKIP]
--------------------------------------------------------------------
**v123061651**
- i18n: Translation update.
- i18n: Translation update.
- i18n: Translation update.
- fix: avoid trying to open invalid image urls.
- Changelog for v123051471 [CI SKIP]
--------------------------------------------------------------------
**v123051471**
- fix: images could be null.
- fix: Check if color is not empty before parsing it.
- chore: Removed unused log.
- Changelog for v123051331 [CI SKIP]
--------------------------------------------------------------------
**v123051331**
- fix: illegal input.
- Changelog for v123051321 [CI SKIP]
--------------------------------------------------------------------
**v123051321**
- debug: Debug null context.
- Changelog for v123051301 [CI SKIP]
--------------------------------------------------------------------
**v123051301**
- feat: Basic auth from url. Fixes #142 (#143)
- debug: Debug index out of bound exception.
- Changelog for v123051211 [CI SKIP]
--------------------------------------------------------------------
**v123051211**
- fix: Sometimes url isn't even defined.
- Changelog for v123041021 [CI SKIP]
--------------------------------------------------------------------
**v123041021**
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
- Enable Core Library Desugaring to support older Android versions
- Changelog for v123030851 [CI SKIP]
--------------------------------------------------------------------
**v123030851**
- chore: replace textDrawable library (#136)
- refactor: Remove slow login check. Closes #135.
- ci: send the mapping file after a release.
- Changelog for v123030751 [CI SKIP]
--------------------------------------------------------------------
**v123030751**
- debug: added a lot to pinpoint the url issue.
- feat: Use /sources/stats in the home (#133)
- Changelog for v123030681 [CI SKIP]
--------------------------------------------------------------------
**v123030681**
- fix: Unread and starred can be null.
- Fixed version number issue.
- Changelog for v123030621 [CI SKIP]
--------------------------------------------------------------------
**v123030621**
- fix: url required issue.
- fix: Canvas reused issue.
- Changelog for v123020572 [CI SKIP]
--------------------------------------------------------------------
**v123020572**
- fix: requirecontext issues ?
- debug: activity not found exception.
- Changelog for v123020571 [CI SKIP]
--------------------------------------------------------------------
**v123020571**
- chore: remove errors logging.
- fix: quickfix for url param not provided for some sources.
- Update 'CHANGELOG.md'
- Changelog for v123020523 [CI SKIP]
--------------------------------------------------------------------
**v123020523**
- fix: Git changelog.
--------------------------------------------------------------------
**v123020491**
- fix: Fixed acra bug reporting.
--------------------------------------------------------------------
**v123010301**
- Chore: acra config.
--------------------------------------------------------------------
**v123010281**
- improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
--------------------------------------------------------------------
**v123010261**
- feat: Handle public instances (#126) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
- ci: Pull request should trigger ci.
- fix: Complete the disconnection before redirecting to the login screen
- Complete the disconnection before redirecting to the login screen
--------------------------------------------------------------------
**v123010241**
- Merge pull request 'feat: swipe down to close images' (#122) from davidoskky/ReaderForSelfoss-multiplatform:swipe_down into master
- Remove unnecessary definition
- Remove unused import
- Adjust the image closing animation
- Add a dark hue to the underlying article when swiping to close images
- Rename activity style to avoid interferences
- Adapt the style of the image activity to the rest of the application
- Resolve issues when swiping down to close images
- Close the image fragment only if the image has been dragged down
- Animate swipe down to close images
- Swipe down to close images
--------------------------------------------------------------------
**v123010041**
- Merge pull request 'scroll-tag-filters' (#124) from scroll-tag-filters into master
- fix: added POST_NOTIFICATIONS to fix notifications issues.
- fix: scrollable filter sheet.
- enhancement: Ellipsize chips text.
- Cleaning.
--------------------------------------------------------------------
**v122123641**
- feat: Disable the failing source in the filter sheet.
- feat: Display the source error in the sources list.
--------------------------------------------------------------------
**v122123631**
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
- debug: trying to resolve `Canvas: trying to use a recycled bitmap`.
- fix: NPE may be caused by the binding or the title that was null.
- chore: Skip drone pipeline on changelog push.
--------------------------------------------------------------------
**v122123621**
- fix: Automatic CHANGELOG generation.
- Merge pull request 'Sources Upsert' (#119) from sources-edit into master
- Source update screen.
- Sources menu.
- chore: Automatic CHANGELOG generation.
--------------------------------------------------------------------
# V2/Multiplatform rewrite
**v1**
- The app has the same functionalities as before.
--------------------------------------------------------------------
# Old version changes
**1.7.x**
- Hiding tags with 0 articles
+6 -6
View File
@@ -1,4 +1,4 @@
# ReaderForSelfoss-multiplatform
# ReaderForSelfoss-multiplatform [![Build Status](https://build.amine-bouabdallaoui.fr/api/badges/Louvorg/ReaderForSelfoss-multiplatform/status.svg)](https://build.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss)
@@ -22,15 +22,15 @@ If you are a user, you can still create new issues. I'll fix them when I can.
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/.github/CONTRIBUTING.md).
2. Check the [Contribution guide](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
3. Build the project by following [these steps](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
## Useful links
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/blob/master/CHANGELOG.md)
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/projects/1)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss-multiplatform/issues)
- [Check what changed](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
- [See what I'm doing](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
- [Create an issue, or request a new feature](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
## Contributors (V1) (Alphabetical order) ❤️
+98 -76
View File
@@ -1,17 +1,22 @@
import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project
val acraVersion = "5.9.7"
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover")
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
var result: String = ByteArrayOutputStream().use { outputStream ->
val result: String = ByteArrayOutputStream().use { outputStream ->
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
isIgnoreExitValue = ignore ?: false
isIgnoreExitValue = ignore
}
outputStream.toString()
}
@@ -19,44 +24,55 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
}
fun gitVersion(): String {
var process = ""
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
process = if (maybeTagOfCurrentCommit.isEmpty()) {
val process = if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else {
println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD")
}
return process.replace("'", "").substring(1).replace("\\.", "").trim()
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
}
fun versionCodeFromGit(): Int {
if (ignoreGitVersion == "true") {
// don't care
return 1
}
println("version code " + gitVersion())
return gitVersion().toInt()
}
fun versionNameFromGit(): String {
if (ignoreGitVersion == "true") {
// don't care
return "1"
}
println("version name " + gitVersion())
return gitVersion()
}
android {
compileOptions {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
compileSdk = 31
buildToolsVersion = "31.0.0"
// For Kotlin projects
kotlinOptions {
jvmTarget = "17"
}
compileSdk = 34
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21
targetSdk = 31
minSdk = 25
targetSdk = 34
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@@ -68,11 +84,10 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
@@ -82,111 +97,118 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
getByName("debug") {
buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String)
buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String)
buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String)
}
}
flavorDimensions.add("build")
productFlavors {
create("github") {
create("githubConfig") {
versionNameSuffix = "-github"
dimension = "build"
}
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "bou.amine.apps.readerforselfossv2.android"
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
implementation(project(":shared"))
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.preference:preference-ktx:1.2.1")
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0-alpha02")
androidTestImplementation("androidx.test:runner:1.3.1-alpha02")
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0-alpha02")
// Espresso-intents for validation and stubbing of Intents
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0-alpha02")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("org.jsoup:jsoup:1.14.3")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("androidx.annotation:annotation:1.7.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.15.4")
//multidex
implementation("androidx.multidex:multidex:2.0.1")
// About
implementation("com.mikepenz:aboutlibraries-core:8.9.4")
implementation("com.mikepenz:aboutlibraries:8.9.4")
implementation("com.mikepenz:aboutlibraries-definitions:8.9.4")
// Async
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
// Retrofit + http logging + okhttp
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.burgstaller:okhttp-digest:2.5")
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:10.5.1")
// Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
// glide
kapt("com.github.bumptech.glide:compiler:4.11.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.1.1")
// Drawer
implementation("com.mikepenz:materialdrawer:8.4.5")
kapt("com.github.bumptech.glide:compiler:4.15.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
// Themes
implementation("com.52inc:scoops:1.0.0")
implementation("com.jaredrummler:colorpicker:1.1.0")
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager
implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0")
implementation("org.kodein.di:kodein-di:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
//Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
//Logging
implementation("io.github.aakira:napier:2.6.1")
//PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.room:room-ktx:2.4.0-beta01")
kapt("androidx.room:room-compiler:2.4.0-beta01")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
implementation("android.arch.work:work-runtime-ktx:1.0.1")
// SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.4")
//test
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion")
}
tasks.withType<Test> {
outputs.upToDateWhen { false }
useJUnit()
testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
)
showStandardStreams = true
}
}
aboutLibraries {
offlineMode = true
fetchRemoteLicense = false
fetchRemoteFunding = false
includePlatform = false
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
}
+41 -9
View File
@@ -30,15 +30,8 @@
<fields>;
}
-dontwarn okio.**
-dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
-keepattributes *Annotation*,Signature
-keepattributes Exceptions
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
@@ -55,11 +48,50 @@
public *;
}
-dontwarn com.anupcowkur.reservoir.**
-dontwarn javax.annotation.**
-keep class android.support.v7.widget.SearchView { *; }
# maybe remove later ?
-keep class * extends androidx.fragment.app.Fragment
-dontwarn org.slf4j.impl.StaticLoggerBinder
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontwarn io.mockk.**
-keep class io.mockk.** { *; }
# Kodein
-keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference
-keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
-11
View File
@@ -1,11 +0,0 @@
#!/bin/bash
# NOTE: This is copy/pasted in jenkins
rm -f version.txt
printf "versionName=$1-github\nversionCode=$1" >> version.txt
# You'll need to change server as your server and define a VERSION_PATH.
scp version.txt server:$VERSION_PATH
rm version.txt
@@ -1,96 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "08ca537d7ac9d4dd216e8e395d70801a",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")"
]
}
}
@@ -1,176 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "6fa6944b04100d68eab61039876a8804",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6fa6944b04100d68eab61039876a8804\")"
]
}
}
@@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "7ad9c4961992c13b670128485ebb3efc",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7ad9c4961992c13b670128485ebb3efc\")"
]
}
}
@@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9cf8b03d32f80dfd58160599a1df197d\")"
]
}
}
@@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9cf8b03d32f80dfd58160599a1df197d')"
]
}
}
@@ -1,3 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
// TODO: test source adding
@@ -1,102 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressKey
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import android.view.KeyEvent
import androidx.test.espresso.matcher.RootMatchers.isDialog
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.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())
onView(withText(android.R.string.ok))
.inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
openActionBarOverflowOrOptionsMenu(context)
onView(withText(R.string.action_disconnect)).perform(click())
intended(hasComponent(LoginActivity::class.java.name))
}
// TODO: test articles opening and actions for cards and lists
@After
fun releaseIntents() {
Intents.release()
}
}
@@ -1,180 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.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.urlView)).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.loginView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.loginView)).perform(click()).perform(
typeText(username),
closeSoftKeyboard()
)
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.passwordView)).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.urlView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.passwordView)).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())
Thread.sleep(2000)
intended(hasComponent(HomeActivity::class.java.name))
}
@After
fun releaseIntents() {
Intents.release()
}
}
@@ -1,81 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.test.InstrumentationRegistry.getInstrumentation
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.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 MainActivityEspressoTest {
lateinit var intent: Intent
lateinit var preferencesEditor: SharedPreferences.Editor
private lateinit var url: String
private lateinit var username: String
private lateinit var password: String
@Rule @JvmField
val rule = ActivityTestRule(MainActivity::class.java, true, false)
@Before
fun setUp() {
intent = Intent()
val context = getInstrumentation().targetContext
// create a SharedPreferences editor
preferencesEditor = context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE).edit()
url = BuildConfig.LOGIN_URL
username = BuildConfig.LOGIN_USERNAME
password = BuildConfig.LOGIN_PASSWORD
Intents.init()
}
@Test
fun checkFirstOpenLaunchesIntro() {
preferencesEditor.putString("url", "")
preferencesEditor.putString("password", "")
preferencesEditor.putString("login", "")
preferencesEditor.commit()
rule.launchActivity(intent)
intended(hasComponent(LoginActivity::class.java.name))
intended(hasComponent(HomeActivity::class.java.name), times(0))
}
@Test
fun checkNotFirstOpenLaunchesLogin() {
preferencesEditor.putString("url", url)
preferencesEditor.putString("password", password)
preferencesEditor.putString("login", username)
preferencesEditor.commit()
rule.launchActivity(intent)
intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(HomeActivity::class.java.name))
}
@After
fun releaseIntents() {
Intents.release()
}
}
@@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.matcher.ViewMatchers
import android.view.View
import android.widget.EditText
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 EditText) {
return false
}
return item.error.isNotEmpty()
}
}
fun withMenu(id: Int, titleId: Int): Matcher<View> =
Matchers.anyOf(
ViewMatchers.withId(id),
ViewMatchers.withText(titleId)
)
@@ -1,32 +0,0 @@
// TODO
//package bou.amine.apps.readerforselfossv2.android.utils
//
//import bou.amine.apps.readerforselfossv2.android.utils.Config
//import bou.amine.apps.readerforselfossv2.android.utils.parseDate
//import org.junit.Test
//
//class DateUtilsTest {
//
// @Test
// fun parseDateV4() {
//
// Config.apiVersion = 4
// val dateString = "2013-04-07T13:43:00+01:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds : Long = 1365338580000
//
// assert(milliseconds == correctMilliseconds)
// }
//
// @Test
// fun parseDateV1() {
// Config.apiVersion = 0
// val dateString = "2013-04-07 13:43:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds = 1365342180000
//
// assert(milliseconds == correctMilliseconds)
// }
//}
+11 -12
View File
@@ -1,18 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="bou.amine.apps.readerforselfossv2.android">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="false"
android:fullBackupContent="false"
tools:replace="android:allowBackup"
android:name=".MyApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar">
android:theme="@style/NoBar"
android:dataExtractionRules="@xml/data_extraction_rules"
android:configChanges="uiMode">
<activity
android:name=".MainActivity"
android:theme="@style/SplashTheme"
@@ -48,7 +53,7 @@
android:value=".HomeActivity" />
</activity>
<activity
android:name=".AddSourceActivity"
android:name=".UpsertSourceActivity"
android:parentActivityName=".SourcesActivity"
android:exported="true">
<meta-data
@@ -65,13 +70,10 @@
android:name=".ReaderActivity">
</activity>
<activity
android:name=".ImageActivity">
android:name=".ImageActivity"
android:theme="@style/Theme.AppCompat.ImageActivity">
</activity>
<meta-data
android:name="bou.amine.apps.readerforselfossv2.android.utils.glide.SelfSignedGlideModule"
android:value="GlideModule" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
@@ -79,8 +81,5 @@
android:value="true" />
<meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>
@@ -0,0 +1,9 @@
package bou.amine.apps.readerforselfossv2.android
import org.acra.ACRA
import org.acra.ktx.sendSilentlyWithAcra
fun Throwable.sendSilentlyWithAcraWithName(name: String) {
ACRA.errorReporter.putCustomData("error_source", name)
this.sendSilentlyWithAcra()
}
@@ -1,192 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class AddSourceActivity : AppCompatActivity(), DIAware {
private var mSpoutsValue: String? = null
private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding
override val di by closestDI()
private val repository : Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity)
super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
val drawable = binding.nameInput.background
drawable.setTint(appColors.colorAccent)
// TODO: clean
binding.nameInput.background = drawable
val drawable1 = binding.sourceUri.background
drawable1.setTint(appColors.colorAccent)
binding.sourceUri.background = drawable1
val drawable2 = binding.tags.background
drawable2.setTint(appColors.colorAccent)
binding.tags.background = drawable2
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
binding.saveBtn.setTextColor(appColors.colorAccent)
binding.saveBtn.setOnClickListener {
handleSaveSource(
binding.tags,
binding.nameInput.text.toString(),
binding.sourceUri.text.toString()
)
}
}
override fun onResume() {
super.onResume()
val config = Config()
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer)
}
}
private fun handleSpoutsSpinner(
spoutsSpinner: Spinner,
mProgress: ProgressBar,
formContainer: ConstraintLayout
) {
val spoutsKV = HashMap<String, String>()
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
if (view != null) {
val spoutName = (view as TextView).text.toString()
mSpoutsValue = spoutsKV[spoutName]
}
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
mSpoutsValue = null
}
}
CoroutineScope(Dispatchers.Main).launch {
val items = repository.getSpouts()
if (items != null) {
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV[value.name] = key
}
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 {
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 mustLoginToAddSource() {
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
val i = Intent(this, LoginActivity::class.java)
startActivity(i)
finish()
}
private fun handleSaveSource(tags: EditText, title: String, url: String) {
val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
when {
sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
}
else -> {
CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = repository.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
"",
)
if (successfullyAddedSource) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -23,7 +24,6 @@ class ImageActivity : AppCompatActivity() {
setContentView(view)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
@@ -31,12 +31,52 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false)
val transitionListener =
object : MotionLayout.TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
) {
// Nothing
}
override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
progress: Float,
) {
// Nothing
}
override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int,
) {
if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0)
}
}
override fun onTransitionTrigger(
motionLayout: MotionLayout?,
triggerId: Int,
positive: Boolean,
progress: Float,
) {
// Nothing
}
}
binding.root.setTransitionListener(transitionListener)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
return true
}
}
@@ -45,7 +85,6 @@ class ImageActivity : AppCompatActivity() {
}
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
@@ -2,7 +2,9 @@ package bou.amine.apps.readerforselfossv2.android
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
@@ -10,40 +12,38 @@ import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.mikepenz.aboutlibraries.LibsBuilder
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.acra.ACRA
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class LoginActivity() : AppCompatActivity(), DIAware {
class LoginActivity : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0
private var isWithSelfSignedCert = false
private var isWithLogin = false
private var isWithHTTPLogin = false
private val settings = Settings()
private lateinit var appColors: AppColors
private lateinit var binding: ActivityLoginBinding
override val di by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@LoginActivity)
super.onCreate(savedInstanceState)
handleTheme()
binding = ActivityLoginBinding.inflate(layoutInflater)
val view = binding.root
@@ -53,22 +53,20 @@ class LoginActivity() : AppCompatActivity(), DIAware {
handleBaseUrlFail()
if (settings.getString("url", "").isNotEmpty()) {
if (appSettingsService.getBaseUrl().isNotEmpty()) {
showProgress(true)
goToMain()
}
handleActions()
}
private fun handleActions() {
binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
isWithSelfSignedCert = !isWithSelfSignedCert
val visi: Int = if (b) View.VISIBLE else View.GONE
binding.warningText.visibility = visi
@SuppressLint("WrongConstant") // Constant is fetched from the settings
private fun handleTheme() {
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
}
private fun handleActions() {
binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
@@ -76,7 +74,7 @@ class LoginActivity() : AppCompatActivity(), DIAware {
return@OnEditorActionListener true
}
false
}
},
)
binding.signInButton.setOnClickListener { attemptLogin() }
@@ -88,14 +86,6 @@ class LoginActivity() : AppCompatActivity(), DIAware {
binding.loginView.visibility = visi
binding.passwordView.visibility = visi
}
binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
isWithHTTPLogin = !isWithHTTPLogin
val visi: Int = if (b) View.VISIBLE else View.GONE
binding.httpLoginView.visibility = visi
binding.httpPasswordView.visibility = visi
}
}
private fun handleBaseUrlFail() {
@@ -105,54 +95,107 @@ class LoginActivity() : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
"OK",
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
}
}
private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
}
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}
private fun preferenceError(t: Throwable) {
settings.remove("url")
settings.remove("login")
settings.remove("httpUserName")
settings.remove("password")
settings.remove("httpPassword")
private fun preferenceError() {
appSettingsService.resetLoginInformation()
binding.urlView.error = getString(R.string.wrong_infos)
binding.loginView.error = getString(R.string.wrong_infos)
binding.passwordView.error = getString(R.string.wrong_infos)
binding.httpLoginView.error = getString(R.string.wrong_infos)
binding.httpPasswordView.error = getString(R.string.wrong_infos)
}
private fun attemptLogin() {
// Reset errors.
binding.urlView.error = null
binding.loginView.error = null
binding.httpLoginView.error = null
binding.passwordView.error = null
binding.httpPasswordView.error = null
// Store values at the time of the login attempt.
val url = binding.urlView.text.toString()
val login = binding.loginView.text.toString()
val httpLogin = binding.httpLoginView.text.toString()
val password = binding.passwordView.text.toString()
val httpPassword = binding.httpPasswordView.text.toString()
val url = binding.urlView.text.toString().trim()
val login = binding.loginView.text.toString().trim()
val password = binding.passwordView.text.toString().trim()
failInvalidUrl(url)
failLoginDetails(password, login)
showProgress(true)
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.Main).launch {
try {
repository.updateApiInformation()
} catch (e: Exception) {
if (e.message?.startsWith("No transformation found") == true) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG,
).show()
preferenceError()
showProgress(false)
}
}
val result = repository.login()
if (result) {
val errorFetching = repository.checkIfFetchFails()
if (!errorFetching) {
goToMain()
} else {
preferenceError()
}
} else {
preferenceError()
}
showProgress(false)
}
}
private fun failLoginDetails(
password: String,
login: String,
) {
var lastFocusedView: View? = null
var cancel = false
var focusView: View? = null
if (!url.isBaseUrlValid(this@LoginActivity)) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
lastFocusedView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
lastFocusedView = binding.loginView
cancel = true
}
}
maybeCancelAndFocusView(cancel, lastFocusedView)
}
private fun failInvalidUrl(url: String) {
val focusView = binding.urlView
var cancel = false
if (url.isBaseUrlInvalid()) {
cancel = true
binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++
if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create()
@@ -160,61 +203,21 @@ class LoginActivity() : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
"OK",
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
inValidCount = 0
}
}
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
cancel = true
}
}
if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) {
binding.httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = binding.httpPasswordView
cancel = true
}
if (TextUtils.isEmpty(httpLogin)) {
binding.httpLoginView.error = getString(R.string.error_field_required)
focusView = binding.httpLoginView
cancel = true
}
maybeCancelAndFocusView(cancel, focusView)
}
private fun maybeCancelAndFocusView(
cancel: Boolean,
focusView: View?,
) {
if (cancel) {
focusView?.requestFocus()
} else {
showProgress(true)
repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
CoroutineScope(Dispatchers.IO).launch {
val result = repository.login()
if (result) {
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
}
}
}
}
showProgress(false)
}
}
@@ -226,12 +229,13 @@ class LoginActivity() : AppCompatActivity(), DIAware {
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() {
if (show) 0F else 1F,
).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
}
}
},
)
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
@@ -239,12 +243,13 @@ class LoginActivity() : AppCompatActivity(), DIAware {
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() {
if (show) 1F else 0F,
).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
}
}
},
)
}
@@ -255,10 +260,17 @@ class LoginActivity() : AppCompatActivity(), DIAware {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.issue_tracker -> {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
startActivity(browserIntent)
return true
}
R.id.about -> {
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl)
.start(this)
true
}
@@ -8,7 +8,6 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -3,46 +3,141 @@ package bou.amine.apps.readerforselfossv2.android
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.widget.ImageView
import android.widget.Toast
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.russhwolf.settings.Settings
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.httpSender
import org.acra.config.toast
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
import(networkModule)
bind<Repository>() with singleton { Repository(instance(), instance()) }
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with
singleton {
Repository(
instance(),
instance(),
isConnectionAvailable,
instance(),
)
}
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
}
private lateinit var config: Config
private lateinit var settings : Settings
private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance()
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
override fun onCreate() {
super.onCreate()
config = Config()
settings = Settings()
initDrawerImageLoader()
initTheme()
Napier.base(DebugAntilog())
if (!ACRA.isACRASenderServiceProcess()) {
tryToHandleBug()
handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver(
connectivityStatus,
repository,
),
)
CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage =
if (networkAvailable) {
repository.handleDBActions()
R.string.network_connectivity_retrieved
} else {
R.string.network_connectivity_lost
}
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT,
).show()
}
}
}
repository.migrate(driverFactory)
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initAcra {
reportFormat = StringFormat.JSON
reportContent =
listOf(
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,
)
toast {
// required
text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT
}
httpSender {
uri =
"https://bugs.amine-bouabdallaoui.fr/report" // best guess, you may need to adjust this
basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST
}
}
}
private fun handleNotificationChannels() {
@@ -51,55 +146,52 @@ class MyApp : MultiDexApplication(), DIAware {
val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
val newItemsChannelmChannel =
NotificationChannel(
AppSettingsService.newItemsChannelId,
newItemsChannelname,
newItemsChannelimportance,
)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
}
}
private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(imageView.context)
.loadMaybeBasicAuth(config, uri.toString())
.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 {
if (e is NoClassDefFoundError &&
e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug")
}) {
Unit
}
) {
// Nothing
} else {
oldHandler.uncaughtException(thread, e)
}
}
}
class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus,
val repository: Repository,
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
repository.connectionMonitored = true
connectivityStatus.start()
}
override fun onPause(owner: LifecycleOwner) {
repository.connectionMonitored = false
connectivityStatus.stop()
super.onPause(owner)
}
}
}
@@ -8,22 +8,13 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.room.Room
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding
import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.toggleStar
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -32,28 +23,23 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware {
private var markOnScroll: Boolean = false
private var currentItem: Int = 0
private lateinit var appColors: AppColors
private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1
private val JUSTIFY = 1
private val ALIGN_LEFT = 2
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
override val di by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE)
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
} else {
toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED)
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
}
}
@@ -65,39 +51,30 @@ class ReaderActivity : AppCompatActivity(), DIAware {
showMenuItem(false)
}
private var settings = Settings()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appColors = AppColors(this)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
markOnScroll = settings.getBoolean("mark_on_scroll", false)
activeAlignment = settings.getInt("text_align", JUSTIFY)
currentItem = intent.getIntExtra("currentItem", 0)
if (allItems.isEmpty()) {
allItems = repository.getReaderItems()
if (allItems.isEmpty() || currentItem > allItems.size) {
finish()
}
currentItem = intent.getIntExtra("currentItem", 0)
try {
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
finish()
}
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false)
@@ -110,10 +87,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
private fun readItem(item: SelfossModel.Item) {
if (markOnScroll) {
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item.id)
// TODO: Handle failure
repository.markAsRead(item)
}
}
}
@@ -125,22 +101,25 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
override fun onKeyDown(
keyCode: Int,
event: KeyEvent?,
): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollDown()
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
val currentFragment = supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollUp()
true
}
@@ -151,7 +130,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
private fun alignmentMenu() {
val showJustify = activeAlignment == ALIGN_LEFT
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
}
@@ -161,16 +140,19 @@ class ReaderActivity : AppCompatActivity(), DIAware {
inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu
alignmentMenu()
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star)
} else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
alignmentMenu()
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
@@ -181,8 +163,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
readItem(allItems[position])
}
}
},
)
}
return true
}
@@ -201,40 +184,36 @@ class ReaderActivity : AppCompatActivity(), DIAware {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
return true
}
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem].id)
// TODO: Handle failure
repository.unstarr(allItems[binding.pager.currentItem])
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem].id)
// TODO: Handle failure
repository.starr(allItems[binding.pager.currentItem])
}
afterSave()
}
}
R.id.align_left -> {
activeAlignment = ALIGN_LEFT
switchAlignmentSetting()
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment()
}
R.id.align_justify -> {
activeAlignment = JUSTIFY
switchAlignmentSetting()
switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment()
}
}
return super.onOptionsItemSelected(item)
}
private fun switchAlignmentSetting() {
settings.putInt("text_align", activeAlignment)
private fun switchAlignmentSetting(allignment: Int) {
appSettingsService.changeAllignment(allignment)
alignmentMenu()
}
@@ -244,8 +223,4 @@ class ReaderActivity : AppCompatActivity(), DIAware {
startActivity(intent)
overridePendingTransition(0, 0)
}
companion object {
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
}
}
@@ -8,12 +8,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,22 +18,15 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance
class SourcesActivity : AppCompatActivity(), DIAware {
private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding
override val di by closestDI()
private val repository: Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity)
binding = ActivitySourcesBinding.inflate(layoutInflater)
val view = binding.root
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
super.onCreate(savedInstanceState)
setContentView(view)
@@ -46,8 +35,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = appColors.colorAccentDark
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
}
override fun onStop() {
@@ -59,40 +48,33 @@ class SourcesActivity : AppCompatActivity(), DIAware {
super.onResume()
val mLayoutManager = LinearLayoutManager(this)
var items: ArrayList<SelfossModel.Source>
var items: ArrayList<SelfossModel.SourceDetail>
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
val response = repository.getSourcesDetails()
if (response.isNotEmpty()) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
val mAdapter =
SourcesListAdapter(
this@SourcesActivity,
items,
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
Toast.LENGTH_SHORT,
).show()
}
}
}
binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
}
}
}
@@ -0,0 +1,210 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null
private lateinit var binding: ActivityUpsertSourceBinding
override val di by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUpsertSourceBinding.inflate(layoutInflater)
val view = binding.root
existingSource = repository.getSelectedSource()
if (existingSource != null) {
binding.formContainer.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
}
val title = if (existingSource == null) R.string.add_source else R.string.update_source
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle(title)
}
}
setContentView(view)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener {
handleSaveSource()
}
}
private fun initFields(items: Map<String, SelfossModel.Spout>) {
binding.nameInput.setText(existingSource!!.title)
binding.tags.setText(existingSource!!.tags?.joinToString(", "))
binding.sourceUri.setText(existingSource!!.params?.url)
binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
}
override fun onResume() {
super.onResume()
val baseUrl = appSettingsService.getBaseUrl()
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner()
}
}
private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>()
binding.spoutsSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>,
view: View?,
i: Int,
l: Long,
) {
if (view != null) {
val spoutName = (view as TextView).text.toString()
mSpoutsValue = spoutsKV[spoutName]
}
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
mSpoutsValue = null
}
}
fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText(
this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT,
).show()
binding.progress.visibility = View.GONE
}
CoroutineScope(Dispatchers.Main).launch {
try {
val items = repository.getSpouts()
if (items.isNotEmpty()) {
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV[value.name] = key
}
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@UpsertSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings,
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter
if (existingSource != null) {
initFields(items)
}
} else {
handleSpoutFailure()
}
} catch (e: NetworkUnavailableException) {
handleSpoutFailure(networkIssue = true)
}
}
}
private fun maybeGetDetailsFromIntentSharing(intent: Intent) {
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
}
}
private fun mustLoginToAddSource() {
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
val i = Intent(this, LoginActivity::class.java)
startActivity(i)
finish()
}
private fun handleSaveSource() {
val url = binding.sourceUri.text.toString()
val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
when {
sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
}
else -> {
CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource =
if (existingSource != null) {
repository.updateSource(
existingSource!!.id,
binding.nameInput.text.toString(),
url,
mSpoutsValue!!,
binding.tags.text.toString(),
)
} else {
repository.createSource(
binding.nameInput.text.toString(),
url,
mSpoutsValue!!,
binding.tags.text.toString(),
)
}
if (successfullyAddedSource) {
finish()
} else {
Toast.makeText(
this@UpsertSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
repository.unsetSelectedSource()
}
}
@@ -1,7 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -9,18 +8,18 @@ import android.widget.ImageView.ScaleType
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -31,43 +30,80 @@ import org.kodein.di.instance
class ItemCardAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
private val fullHeightCards: Boolean,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
override lateinit var binding: CardItemBinding
private val imageMaxHeight: Int =
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
override val di: DI by closestDI(app)
override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
holderBinding.favButton.setOnClickListener {
val item = items[position]
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
}
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
}
binding.favButton.isSelected = true
}
}
binding.shareBtn.setOnClickListener {
val item = items[position]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[position])
}
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
with(holder) {
val itm = items[position]
handleClickListeners(binding, position)
handleLinkOpening(binding, position)
binding.favButton.isSelected = itm.starred
binding.title.text = itm.getTitleDecoded()
if (appSettingsService.getPublicAccess()) {
binding.favButton.visibility = View.GONE
}
binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent)
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
binding.sourceTitleAndDate.text = try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
itm.sourceAuthorOnly()
}
if (!fullHeightCards) {
if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP
}
@@ -78,82 +114,16 @@ class ItemCardAdapter(
binding.itemImage.setImageDrawable(null)
} else {
binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable)
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
}
}
}
override fun getItemCount(): Int {
return items.size
}
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleClickListeners()
handleCustomTabActions()
}
private fun handleClickListeners() {
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
if (c.isNetworkAvailable()) {
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item.id)
// TODO: Handle failure
}
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item.id)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
}
}
}
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
binding.root.setOnClickListener {
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
}
}
}
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
}
@@ -1,104 +1,76 @@
package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import org.kodein.di.DI
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ItemListAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext
override lateinit var binding: ListItemBinding
override val di: DI by closestDI(app)
override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
with(holder) {
val itm = items[position]
binding.title.text = itm.getTitleDecoded()
handleLinkOpening(binding, position)
binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent)
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
binding.sourceTitleAndDate.text = try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
itm.sourceAuthorOnly()
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable)
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
} else {
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
}
}
override fun getItemCount(): Int = items.size
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleCustomTabActions()
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
binding.root.setOnClickListener {
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
}
}
}
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
}
@@ -1,15 +1,16 @@
package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
@@ -18,31 +19,35 @@ import kotlinx.coroutines.launch
import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract var items: ArrayList<SelfossModel.Item>
abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository
abstract val db: AppDatabase
abstract val binding: ViewBinding
abstract val appSettingsService: AppSettingsService
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
abstract val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit
protected val c: Context get() = app.baseContext
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
this.items = items
this.items.clear()
this.items.addAll(items)
updateHomeItems(items)
notifyDataSetChanged()
updateItems(this.items)
}
private fun unmarkSnackbar(i: SelfossModel.Item, position: Int) {
val s = Snackbar
private fun unmarkSnackbar(
item: SelfossModel.Item,
position: Int,
) {
val s =
Snackbar
.make(
app.findViewById(R.id.coordLayout),
R.string.marked_as_read,
Snackbar.LENGTH_LONG
Snackbar.LENGTH_LONG,
)
.setAction(R.string.undo_string) {
CoroutineScope(Dispatchers.IO).launch {
unreadItemAtIndex(position, false)
}
unreadItemAtIndex(item, position, false)
}
val view = s.view
@@ -51,15 +56,19 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}
private fun markSnackbar(position: Int) {
val s = Snackbar
private fun markSnackbar(
item: SelfossModel.Item,
position: Int,
) {
val s =
Snackbar
.make(
app.findViewById(R.id.coordLayout),
R.string.marked_as_unread,
Snackbar.LENGTH_LONG
Snackbar.LENGTH_LONG,
)
.setAction(R.string.undo_string) {
readItemAtIndex(position)
readItemAtIndex(item, position, false)
}
val view = s.view
@@ -68,56 +77,76 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}
protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
holderBinding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
position,
items[position].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app,
)
}
}
fun handleItemAtIndex(position: Int) {
if (items[position].unread) {
readItemAtIndex(position)
readItemAtIndex(items[position], position)
} else {
unreadItemAtIndex(position)
unreadItemAtIndex(items[position], position)
}
}
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
val i = items[position]
private fun readItemAtIndex(
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(i.id)
repository.markAsRead(item)
}
if (repository.displayedItems == ItemType.UNREAD) {
items.remove(i)
items.remove(item)
notifyItemRemoved(position)
updateItems(items)
notifyItemRangeChanged(position, itemCount)
updateHomeItems(items)
} else {
notifyItemChanged(position)
}
if (showSnackbar) {
unmarkSnackbar(i, position)
unmarkSnackbar(item, position)
}
}
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
private fun unreadItemAtIndex(
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(items[position].id)
// Todo: SharedItems.unreadItem(app, api, db, items[position])
// TODO: update db
repository.unmarkAsRead(item)
}
notifyItemChanged(position)
if (showSnackbar) {
markSnackbar(position)
markSnackbar(item, position)
}
}
fun addItemAtIndex(item: SelfossModel.Item, position: Int) {
fun addItemAtIndex(
item: SelfossModel.Item,
position: Int,
) {
items.add(position, item)
notifyItemInserted(position)
updateItems(items)
updateHomeItems(items)
}
fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
val oldSize = items.size
items.addAll(newItems)
notifyItemRangeInserted(oldSize, newItems.size)
updateItems(items)
updateHomeItems(items)
}
}
override fun getItemCount(): Int = items.size
}
@@ -2,24 +2,22 @@ package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -30,72 +28,76 @@ import org.kodein.di.instance
class SourcesListAdapter(
private val app: Activity,
private val items: ArrayList<SelfossModel.Source>
private val items: ArrayList<SelfossModel.SourceDetail>,
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app)
private val repository: Repository by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
val itm = items[position]
config = Config()
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.getTitleDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getTitleDecoded().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
}
binding.sourceTitle.text = itm.getTitleDecoded()
}
override fun getItemCount(): Int = items.size
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
init {
handleClickListeners()
}
private fun handleClickListeners() {
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition]
val (id, title) = items[position]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id)
val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
items.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
Toast.LENGTH_SHORT,
).show()
}
}
}
holder.mView.setOnClickListener {
val source = items[position]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
if (!itm.error.isNullOrBlank()) {
binding.errorText.visibility = View.VISIBLE
binding.errorText.text = itm.error
} else {
binding.errorText.visibility = View.GONE
}
binding.sourceTitle.text = itm.title.getHtmlDecoded()
}
override fun getItemId(position: Int) = position.toLong()
override fun getItemViewType(position: Int) = position
override fun getItemCount(): Int = items.size
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
}
@@ -1,35 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MercuryApi() {
private val service: MercuryService
init {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.NONE
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
val gson = GsonBuilder()
.setLenient()
.create()
val retrofit =
Retrofit
.Builder()
.baseUrl("https://www.amine-bou.fr")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
service = retrofit.create(MercuryService::class.java)
}
fun parseUrl(url: String): Call<ParsedContent> {
return service.parseUrl(url)
}
}
@@ -1,59 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
class ParsedContent(
@SerializedName("title") val title: String,
@SerializedName("content") val content: String?,
@SerializedName("date_published") val date_published: String,
@SerializedName("lead_image_url") val lead_image_url: String?,
@SerializedName("dek") val dek: String,
@SerializedName("url") val url: String,
@SerializedName("domain") val domain: String,
@SerializedName("excerpt") val excerpt: String,
@SerializedName("total_pages") val total_pages: Int,
@SerializedName("rendered_pages") val rendered_pages: Int,
@SerializedName("next_page_url") val next_page_url: String
) : Parcelable {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<ParsedContent> =
object : Parcelable.Creator<ParsedContent> {
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
title = source.readString().orEmpty(),
content = source.readString(),
date_published = source.readString().orEmpty(),
lead_image_url = source.readString(),
dek = source.readString().orEmpty(),
url = source.readString().orEmpty(),
domain = source.readString().orEmpty(),
excerpt = source.readString().orEmpty(),
total_pages = source.readInt(),
rendered_pages = source.readInt(),
next_page_url = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(title)
dest.writeString(content)
dest.writeString(date_published)
dest.writeString(lead_image_url)
dest.writeString(dek)
dest.writeString(url)
dest.writeString(domain)
dest.writeString(excerpt)
dest.writeInt(total_pages)
dest.writeInt(rendered_pages)
dest.writeString(next_page_url)
}
}
@@ -1,10 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface MercuryService {
@GET("parser.php")
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
}
@@ -8,24 +8,16 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_LOW
import androidx.room.Room
import androidx.work.Worker
import androidx.work.WorkerParameters
import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.MyApp
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.preloadImages
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.russhwolf.settings.Settings
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -33,79 +25,40 @@ import org.kodein.di.DIAware
import org.kodein.di.instance
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware {
lateinit var db: AppDatabase
class LoadingWorker(val context: Context, params: WorkerParameters) :
Worker(context, params),
DIAware {
override val di by lazy { (applicationContext as MyApp).di }
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun doWork(): Result {
val settings = Settings()
val periodicRefresh = settings.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
if (context.isNetworkAvailable()) {
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setChannelId(AppSettingsService.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
val notifyNewItems = settings.getBoolean("notify_new_items", false)
repository.handleDBActions()
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
repository.markAsRead(action.articleId.toInt()),
action
)
action.unread -> doAndReportOnFail(
repository.unmarkAsRead(action.articleId.toInt()),
action
)
action.starred -> doAndReportOnFail(
repository.starr(action.articleId.toInt()),
action
)
action.unstarred -> doAndReportOnFail(
repository.unstarr(action.articleId.toInt()),
action
)
}
}
if (context.isNetworkAvailable()) {
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isNotifyNewItemsEnabled()) {
launch {
try {
val newItems = repository.allItems(ItemType.UNREAD)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
val readItems = repository.allItems(ItemType.ALL)
val starredItems = repository.allItems(ItemType.STARRED)
// TODO: save all to DB
} catch (e: Throwable) {}
}
handleNewItemsNotification(apiItems, notificationManager)
}
}
apiItems.map { it.preloadImages(context) }
}
}
return Result.success()
@@ -113,37 +66,40 @@ override fun doWork(): Result {
private fun handleNewItemsNotification(
newItems: List<SelfossModel.Item>?,
notifyNewItems: Boolean,
notificationManager: NotificationManager
notificationManager: NotificationManager,
) {
CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty()
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
if (newSize > 0) {
val intent =
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pflags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
NotificationCompat.Builder(
applicationContext,
AppSettingsService.newItemsChannelId,
)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
newSize,
),
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setChannelId(AppSettingsService.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
@@ -152,19 +108,9 @@ override fun doWork(): Result {
notificationManager.notify(2, newItemsNotification.build())
}
}
apiItems.map { it.preloadImages(context) }
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
private fun doAndReportOnFail(result: Boolean, action: ActionEntity) {
// TODO: Failures should be reported
if (result) {
thread {
db.actionsDao().delete(action)
}
}
}
}
@@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.TypedArray
@@ -8,6 +9,7 @@ import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
@@ -15,36 +17,33 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -52,18 +51,16 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException
import java.net.URL
import java.util.*
import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg"
class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16
private lateinit var item: SelfossModel.Item
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
private lateinit var url: String
private lateinit var contentText: String
private lateinit var contentSource: String
@@ -71,132 +68,61 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var contentTitle: String
private lateinit var allImages: ArrayList<String>
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private lateinit var binding: FragmentArticleBinding
override val di: DI by closestDI()
private val repository: Repository by instance()
private var settings = Settings()
private val appSettingsService: AppSettingsService by instance()
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
private var staticBar = false
override fun onStop() {
super.onStop()
if (mCustomTabActivityHelper != null) {
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
}
}
private val mercuryApi: MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(requireActivity())
config = Config()
super.onCreate(savedInstanceState)
val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!!
item = pi.toModel()
db = Room.databaseBuilder(
requireContext(),
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View {
try {
_binding = FragmentArticleBinding.inflate(inflater, container, false)
binding = FragmentArticleBinding.inflate(inflater, container, false)
url = item.getLinkDecoded()
contentText = item.content
contentTitle = item.getTitleDecoded()
contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText(repository.dateUtils)
contentSource = try {
item.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("Article Fragment parse date")
item.sourceAuthorOnly()
}
allImages = item.getImages()
fontSize = settings.getString("reader_font_size", "16").toInt()
staticBar = settings.getBoolean("reader_static_bar", false)
font = settings.getString("reader_font", "")
if (font.isNotEmpty()) {
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
// Just to be sure
null
}
}
fontSize = appSettingsService.getFontSize()
staticBar = appSettingsService.isStaticBarEnabled()
font = appSettingsService.getFont()
refreshAlignment()
fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = appColors.colorAccentDark
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = requireActivity().buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent)
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item.id)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item.id)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
}
}
)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
if (staticBar) {
fab.hide()
@@ -208,28 +134,7 @@ class ArticleFragment : Fragment(), DIAware {
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent)
} else {
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
handleContent()
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
@@ -243,183 +148,270 @@ class ArticleFragment : Fragment(), DIAware {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
}
},
)
} catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available")
if (context != null) {
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
.setPositiveButton(
android.R.string.ok,
) { _, _ ->
settings.putBoolean("prefer_article_viewer", false)
appSettingsService.disableArticleViewer()
requireActivity().finish()
}
.create()
.show()
}
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable()) {
getContentFromMercury()
}
} else {
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
}
private fun handleFloatingToolbar(): FloatingToolbar {
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
if (appSettingsService.getPublicAccess()) {
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
}
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action ->
if (context != null) {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG,
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG,
).show()
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
// We do nothing
}
},
)
return floatingToolbar
}
private fun refreshAlignment() {
textAlignment = when (settings.getInt("text_align", 1)) {
textAlignment =
when (appSettingsService.getActiveAllignment()) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
}
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
if ((context != null && requireContext().isNetworkAvailable(null)) || context == null) {
private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
override fun onResponse(
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
CoroutineScope(Dispatchers.Main).launch {
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.body()!!.title
val response = mercuryApi.query(url)
if (response.success && response.data != null) {
handleMercuryData(response.data!!)
} else {
openInBrowserAfterFailing()
}
} catch (e: Exception) {
openInBrowserAfterFailing()
}
}
}
private fun handleMercuryData(data: MercuryModel.ParsedContent) {
if (data.error == true || data.failed == true) {
openInBrowserAfterFailing()
} else {
binding.titleView.text = data.title.orEmpty()
if (typeface != null) {
binding.titleView.typeface = typeface
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
}
URL(data.url)
url = data.url!!
try {
contentText = response.body()!!.content.orEmpty()
contentText = data.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
handleLeadImage(data?.lead_image_url)
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
}
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
private fun handleLeadImage(lead_image_url: String?) {
if (!lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.load(
lead_image_url,
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {
}
} else {
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
}
}
private fun handleImageLoading() {
binding.webcontent.webViewClient =
object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String,
): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try {
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
}
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
}
}
}
} catch (e: Exception) {
if (context != null) {
}
}
}
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
}
)
}
}
private fun htmlToWebview() {
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
// TODO: Set the color strings programmatically
val (stringTextColor, stringBackgroundColor) = if (appColors.isDarkTheme) {
Pair("#FFFFFF", "#303030")
} else {
Pair("#212121", "#FAFAFA")
}
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
binding.webcontent.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
}
true
} else {
false
}
return true
}
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(
view: WebView,
url: String,
): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US)
.contains(".jpeg")
) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
}catch ( e : ExecutionException) {}
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
)
} catch (e: ExecutionException) {
// Do nothing
}
else if (url.lowercase(Locale.US).contains(".png")) {
} else if (url.lowercase(Locale.US).contains(".png")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
}catch ( e : ExecutionException) {}
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
)
} catch (e: ExecutionException) {
// Do nothing
}
else if (url.lowercase(Locale.US).contains(".webp")) {
} else if (url.lowercase(Locale.US).contains(".webp")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
}catch ( e : ExecutionException) {}
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
)
} catch (e: ExecutionException) {
// Do nothing
}
}
return super.shouldInterceptRequest(view, url)
}
}
}
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
private fun htmlToWebview() {
if (context != null) {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
val gestureDetector =
GestureDetector(
activity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
})
},
)
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
@@ -432,16 +424,25 @@ class ArticleFragment : Fragment(), DIAware {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
}
val fontName = when (font) {
val fontName =
when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
val fontLinkAndStyle =
if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${
fontName.replace(
" ",
"+",
)
}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
@@ -465,10 +466,15 @@ class ArticleFragment : Fragment(), DIAware {
| max-width: 100%;
| }
| a {
| color: $stringColor !important;
| color: ${
String.format(
"#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent),
)
} !important;
| }
| *:not(a) {
| color: $stringTextColor;
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
| }
| * {
| font-size: ${fontSize}px;
@@ -476,11 +482,26 @@ class ArticleFragment : Fragment(), DIAware {
| word-break: break-word;
| overflow:hidden;
| line-height: 1.5em;
| background-color: $stringBackgroundColor;
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| }
| body, html {
| background-color: $stringBackgroundColor !important;
| border-color: $stringBackgroundColor !important;
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| border-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| padding: 0 !important;
| margin: 0 !important;
| }
@@ -490,19 +511,26 @@ class ArticleFragment : Fragment(), DIAware {
| pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: $stringBackgroundColor;
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| }
| </style>
| $fontLinkAndStyle
|</head>
|<body>
| $contentText
|</body>""".trimMargin(),
|</body>
""".trimMargin(),
"text/html",
"utf-8",
null
null,
)
}
}
fun scrollDown() {
val height = binding.nestedScrollView.measuredHeight
@@ -514,21 +542,19 @@ class ArticleFragment : Fragment(), DIAware {
binding.nestedScrollView.smoothScrollBy(0, -height / 2)
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE
requireActivity().openItemUrlInternalBrowser(
url,
customTabsIntent,
requireActivity()
)
if (context != null) {
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item)
} else {
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
}
}
companion object {
private const val ARG_ITEMS = "items"
fun newInstance(
item: SelfossModel.Item
): ArticleFragment {
fun newInstance(item: SelfossModel.Item): ArticleFragment {
val fragment = ArticleFragment()
val args = Bundle()
args.putParcelable(ARG_ITEMS, item.toParcelable())
@@ -538,9 +564,11 @@ class ArticleFragment : Fragment(), DIAware {
}
fun performClick(): Boolean {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
if (allImages != null && (
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
)
) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
@@ -551,6 +579,4 @@ class ArticleFragment : Fragment(), DIAware {
}
return false
}
}
@@ -0,0 +1,195 @@
package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI()
private val repository: Repository by instance()
private var selectedChip: Chip? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding =
FilterFragmentBinding.inflate(
inflater,
container,
false,
)
val context: Context? = context
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch {
handleTagChips(context)
handleSourceChips(context)
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
}
}
binding.floatingActionButton2.setOnClickListener {
(activity as HomeActivity).getElementsAccordingToTab()
(activity as HomeActivity).fetchOnEmptyList()
dismiss()
}
return binding.root
}
private suspend fun handleSourceChips(context: Context) {
val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.into(
object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable?>?,
) {
try {
c.chipIcon = resource
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("sources > onResourceReady")
}
}
},
)
c.text = source.title.getHtmlDecoded()
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setSourceFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setSourceFilter(source)
repository.setTagFilter(null)
}
if (repository.sourceFilter.value?.equals(source) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
c.isEnabled = source.error.isNullOrBlank()
if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
c.tooltipText = source.error
}
sourceGroup.addView(c)
}
}
private suspend fun handleTagChips(context: Context) {
val tagGroup = binding.tagsGroup
val tags = repository.getTags()
tags.forEachIndexed { _, tag ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
c.text = tag.tag
if (tag.color.isNotEmpty()) {
try {
val gd = GradientDrawable()
val gdColor =
try {
Color.parseColor(tag.getColorHexCode())
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color + " / " + tag.getColorHexCode())
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
}
}
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setTagFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
tagGroup.addView(c)
}
}
companion object {
const val TAG = "FilterModalBottomSheet"
}
}
@@ -1,7 +1,9 @@
package bou.amine.apps.readerforselfossv2.android.fragments
import android.os.Bundle
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
import com.bumptech.glide.Glide
@@ -9,7 +11,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
class ImageFragment : Fragment() {
private lateinit var imageUrl: String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null
@@ -21,12 +22,16 @@ class ImageFragment : Fragment() {
imageUrl = requireArguments().getString("imageUrl")!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE
Glide.with(activity)
Glide.with(requireActivity())
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
@@ -43,9 +48,7 @@ class ImageFragment : Fragment() {
companion object {
private const val ARG_IMAGE = "imageUrl"
fun newInstance(
imageUrl : String
): ImageFragment {
fun newInstance(imageUrl: String): ImageFragment {
val fragment = ImageFragment()
val args = Bundle()
args.putString(ARG_IMAGE, imageUrl)
@@ -1,50 +1,19 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context
import android.net.Uri
import android.text.Html
import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import org.jsoup.Jsoup
import java.util.*
/**
* Items extension methods
*/
fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
fun SelfossModel.Item.getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try {
for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) {
@@ -54,72 +23,21 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
}
}
} catch (e: Error) {
e.sendSilentlyWithAcraWithName("preloadImages")
return false
}
return true
}
fun SelfossModel.Item.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun SelfossModel.Item.getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun SelfossModel.Item.getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
/**
* Sources extension methods
*/
fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Source.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
/**
* Common methods
*/
private fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file == null || file == "null" || file.isEmpty()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
fun String.toTextDrawableString(): String {
val textDrawable = StringBuilder()
for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
e.sendSilentlyWithAcraWithName("toTextDrawableString")
}
}
return textDrawable.toString()
}
@@ -1,11 +1,8 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.RequiresApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.google.gson.annotations.SerializedName
import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun SelfossModel.Item.toParcelable(): ParecelableItem =
ParecelableItem(
@@ -19,8 +16,10 @@ fun SelfossModel.Item.toParcelable() : ParecelableItem =
this.icon,
this.link,
this.sourcetitle,
this.tags.joinToString(",")
this.tags.joinToString(","),
this.author,
)
fun ParecelableItem.toModel(): SelfossModel.Item =
SelfossModel.Item(
this.id,
@@ -33,26 +32,30 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.icon,
this.link,
this.sourcetitle,
this.tags.split(",")
this.tags.split(","),
this.author,
)
data class ParecelableItem(
@SerializedName("id") val id: Int,
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: String
) : Parcelable {
data class ParecelableItem(
val id: Int,
val datetime: String,
val title: String,
val content: String,
var unread: Boolean,
var starred: Boolean,
val thumbnail: String?,
val icon: String?,
val link: String,
val sourcetitle: String,
val tags: String,
val author: String?,
) : Parcelable {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<ParecelableItem> = object : Parcelable.Creator<ParecelableItem> {
val CREATOR: Parcelable.Creator<ParecelableItem> =
object : Parcelable.Creator<ParecelableItem> {
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
}
}
@@ -68,12 +71,16 @@ data class ParecelableItem(
icon = source.readString(),
link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(),
tags = source.readString().orEmpty()
tags = source.readString().orEmpty(),
author = source.readString().orEmpty(),
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
override fun writeToParcel(
dest: Parcel,
flags: Int,
) {
dest.writeInt(id)
dest.writeString(datetime)
dest.writeString(title)
@@ -85,5 +92,6 @@ data class ParecelableItem(
dest.writeString(link)
dest.writeString(sourcetitle)
dest.writeString(tags)
dest.writeString(author)
}
}
@@ -1,28 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import android.content.Context
import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase
class AndroidDeviceDatabase(applicationContext: Context): DeviceDatabase<AndroidItemEntity> {
var db: AppDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
override suspend fun items(): List<AndroidItemEntity> = db.itemsDao().items()
override suspend fun insertAllItems(vararg items: AndroidItemEntity) = db.itemsDao().insertAllItems(*items)
override suspend fun deleteAllItems() = db.itemsDao().deleteAllItems()
override suspend fun delete(item: AndroidItemEntity) = db.itemsDao().delete(item)
override suspend fun updateItem(item: AndroidItemEntity) = db.itemsDao().updateItem(item)
}
@@ -1,40 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.DeviceDataBaseService
import bou.amine.apps.readerforselfossv2.service.SearchService
class AndroidDeviceDatabaseService(db: AndroidDeviceDatabase, searchService: SearchService) :
DeviceDataBaseService<AndroidItemEntity>(db, searchService) {
override suspend fun updateDatabase() {
if (itemsCaching) {
if (items.isEmpty()) {
getFromDB()
}
db.deleteAllItems()
db.insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
}
}
override suspend fun clearDBItems() {
db.deleteAllItems()
}
override fun appendNewItems(newItems: List<SelfossModel.Item>) {
var oldItems = items
if (oldItems != newItems) {
oldItems = oldItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<SelfossModel.Item>
oldItems.addAll(newItems)
items = oldItems
sortItems()
getFocusedItems()
}
}
override fun getFromDB() {
TODO("Not yet implemented")
}
}
@@ -1,23 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
@Dao
interface ActionsDao {
@Query("SELECT * FROM actions order by id asc")
suspend fun actions(): List<ActionEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllActions(vararg actions: ActionEntity)
@Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
fun deleteReadActionForArticle(article_id: String)
@Delete
fun delete(action: ActionEntity)
}
@@ -1,36 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Delete
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
@Dao
interface DrawerDataDao {
@Query("SELECT * FROM tags")
fun tags(): List<TagEntity>
@Query("SELECT * FROM sources")
fun sources(): List<SourceEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllTags(vararg tags: TagEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllSources(vararg sources: SourceEntity)
@Query("DELETE FROM tags")
fun deleteAllTags()
@Query("DELETE FROM sources")
fun deleteAllSources()
@Delete
fun deleteTag(tag: TagEntity)
@Delete
fun deleteSource(source: SourceEntity)
}
@@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import androidx.room.Update
@Dao
interface ItemsDao {
@Query("SELECT * FROM items order by id desc")
suspend fun items(): List<AndroidItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllItems(vararg items: AndroidItemEntity)
@Query("DELETE FROM items")
suspend fun deleteAllItems()
@Delete
suspend fun delete(item: AndroidItemEntity)
@Update
suspend fun updateItem(item: AndroidItemEntity)
}
@@ -1,20 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.database
import androidx.room.RoomDatabase
import androidx.room.Database
import bou.amine.apps.readerforselfossv2.android.persistence.dao.ActionsDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.DrawerDataDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.ItemsDao
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
@Database(entities = [TagEntity::class, SourceEntity::class, AndroidItemEntity::class, ActionEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun drawerDataDao(): DrawerDataDao
abstract fun itemsDao(): ItemsDao
abstract fun actionsDao(): ActionsDao
}
@@ -1,22 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "actions")
data class ActionEntity(
@ColumnInfo(name = "articleid")
val articleId: String,
@ColumnInfo(name = "read")
val read: Boolean,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "unstarred")
var unstarred: Boolean
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}
@@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
@Entity(tableName = "items")
data class AndroidItemEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "datetime")
val datetime: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "thumbnail")
val thumbnail: String?,
@ColumnInfo(name = "icon")
val icon: String?,
@ColumnInfo(name = "link")
val link: String,
@ColumnInfo(name = "sourcetitle")
val sourcetitle: String,
@ColumnInfo(name = "tags")
val tags: String
)
@@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey
@ColumnInfo(name = "tag")
val tag: String,
@ColumnInfo(name = "color")
val color: String,
@ColumnInfo(name = "unread")
val unread: Int
)
@Entity(tableName = "sources")
data class SourceEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "tags")
val tags: String,
@ColumnInfo(name = "spout")
val spout: String,
@ColumnInfo(name = "error")
val error: String,
@ColumnInfo(name = "icon")
val icon: String
)
@@ -1,34 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.persistence.migrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.room.migration.Migration
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
}
}
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
// @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database
// Create the new table
database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
// Copy the data
database.execSQL(
"INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items")
// Remove the old table
database.execSQL("DROP TABLE items")
// Change the table name to the correct one
database.execSQL("ALTER TABLE itemstmp RENAME TO items")
}
}
@@ -3,39 +3,37 @@ package bou.amine.apps.readerforselfossv2.android.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.*
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceManager
import android.view.*
import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.TextWatcher
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.widget.addTextChangedListener
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import java.lang.NumberFormatException
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
class SettingsActivity :
AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
DIAware {
override val di by closestDI()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) {
setTheme(R.style.NoBarDark)
}
val binding = ActivitySettingsBinding.inflate(layoutInflater)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
setContentView(binding.root)
if (savedInstanceState == null) {
supportFragmentManager
@@ -65,22 +63,25 @@ class SettingsActivity : AppCompatActivity(),
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings)
return true
false
} else {
super.onBackPressed()
true
}
return super.onSupportNavigateUp()
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
pref: Preference,
): Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
val fragment =
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment
pref.fragment.toString(),
).apply {
arguments = args
setTargetFragment(caller, 0)
@@ -96,93 +97,134 @@ class SettingsActivity : AppCompatActivity(),
}
class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { _ ->
context?.let {
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(it)
}
true
}
}
}
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = arrayOf(
editText.filters =
arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcraWithName("GeneralPreferenceFragment")
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
}
""
}
},
)
}
}
}
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
editText.addTextChangedListener {
object : TextWatcher {
override fun beforeTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing
}
override fun onTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing
}
override fun afterTextChanged(editable: Editable) {
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
}
}
} }
editText.filters = arrayOf(
}
}
editText.filters =
arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input = (dest.toString() + source.toString()).toInt()
if (input > 0) return@InputFilter null
} catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
}
""
}
},
)
}
}
}
class OfflinePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_offline, rootKey)
}
}
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.settings_theme, menu)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.clear) {
val settings = Settings()
settings.remove("color_primary")
settings.remove("color_primary_dark")
settings.remove("color_accent")
settings.remove("color_accent_dark")
settings.remove("dark_theme")
requireActivity().recreate()
}
return super.onOptionsItemSelected(item)
}
}
@@ -192,28 +234,37 @@ class SettingsActivity : AppCompatActivity(),
startActivity(browserIntent)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.trackerUrl))
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.trackerUrl))
true
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.sourceUrl))
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.sourceUrl))
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.translationUrl))
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.translationUrl))
false
}
}
}
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_experimental, rootKey)
}
}
@@ -1,61 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.themes
import android.app.Activity
import androidx.annotation.ColorInt
import bou.amine.apps.readerforselfossv2.android.R
import com.russhwolf.settings.Settings
class AppColors(a: Activity) {
@ColorInt val colorPrimary: Int
@ColorInt val colorPrimaryDark: Int
@ColorInt val colorAccent: Int
@ColorInt val colorAccentDark: Int
@ColorInt val colorBackground: Int
@ColorInt val textColor: Int
val isDarkTheme: Boolean
init {
val settings = Settings()
colorPrimary =
settings.getInt(
"color_primary",
a.resources.getColor(R.color.colorPrimary)
)
colorPrimaryDark =
settings.getInt(
"color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark)
)
colorAccent =
settings.getInt(
"color_accent",
a.resources.getColor(R.color.colorAccent)
)
colorAccentDark =
settings.getInt(
"color_accent_dark",
a.resources.getColor(R.color.colorAccentDark)
)
isDarkTheme =
settings.getBoolean(
"dark_theme",
false
)
colorBackground = if (isDarkTheme) {
a.setTheme(R.style.NoBarDark)
R.color.darkBackground
} else {
a.setTheme(R.style.NoBar)
R.color.grey_50
}
textColor = if (isDarkTheme) {
R.color.white
} else {
R.color.grey_900
}
}
}
@@ -1,8 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.themes
enum class Toppings(val value: Int) {
PRIMARY(1),
PRIMARY_DARK(2),
ACCENT(3),
ACCENT_DARK(4)
}
@@ -5,7 +5,10 @@ import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink(itemUrl: String, itemTitle: String) {
fun Context.shareLink(
itemUrl: String,
itemTitle: String,
) {
val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND
@@ -15,7 +18,7 @@ fun Context.shareLink(itemUrl: String, itemTitle: String) {
startActivity(
Intent.createChooser(
sendIntent,
getString(R.string.share)
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
@@ -0,0 +1,65 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.RelativeLayout
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import com.google.android.material.imageview.ShapeableImageView
import kotlin.math.abs
class CircleImageView
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View
val imageView: ShapeableImageView
val textView: TextView
private val colorScheme =
listOf(
-0x1a8c8d,
-0xf9d6e,
-0x459738,
-0x6a8a33,
-0x867935,
-0x9b4a0a,
-0xb03c09,
-0xb22f1f,
-0xb24954,
-0x7e387c,
-0x512a7f,
-0x759b,
-0x2b1ea9,
-0x2ab1,
-0x48b3,
-0x5e7781,
-0x6f5b52,
)
init {
view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
imageView = view.findViewById(R.id.circleImage)
textView = view.findViewById(R.id.circleText)
}
fun setBackgroundAndText(text: String) {
val circleDrawable = GradientDrawable()
val color = colorFromIdentifier(text)
circleDrawable.setColor(color)
imageView.setImageDrawable(circleDrawable)
textView.text = text.toTextDrawableString()
}
private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
}
@@ -1,62 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import com.russhwolf.settings.Settings
class Config {
val settings = Settings()
val baseUrl: String
get() = settings.getString("url", "")
val userLogin: String
get() = settings.getString("login", "")
val userPassword: String
get() = settings.getString("password", "")
val httpUserLogin: String
get() = settings.getString("httpUserName", "")
val httpUserPassword: String
get() = settings.getString("httpPassword", "")
companion object {
const val settingsName = "paramsselfoss"
const val feedbackEmail = "aminecmi@gmail.com"
const val translationUrl = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss"
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
const val syncChannelId = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
var apiVersion = 0
/* Execute logout and clear all settings to default */
fun logoutAndRedirect(
c: Context,
callingActivity: Activity,
baseUrlFail: Boolean = false
): Boolean {
val settings = Settings()
settings.clear()
val intent = Intent(c, LoginActivity::class.java)
if (baseUrlFail) {
intent.putExtra("baseUrlFail", baseUrlFail)
}
c.startActivity(intent)
callingActivity.finish()
return true
}
}
}
@@ -1,43 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.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)
}
@@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.parseRelativeDate
fun String.toTextDrawableString(c: Context): String {
val textDrawable = StringBuilder()
for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
}
}
return textDrawable.toString()
}
fun SelfossModel.Item.sourceAndDateText(dateUtils: DateUtils): String {
val formattedDate = parseRelativeDate(dateUtils)
return getSourceTitle() + formattedDate
}
fun SelfossModel.Item.toggleStar(): SelfossModel.Item {
this.starred = !this.starred
return this
}
@@ -1,13 +1,9 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.Patterns
@@ -15,156 +11,41 @@ import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.ReaderActivity
import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
val actionIntent = Intent(Intent.ACTION_SEND)
actionIntent.type = "text/plain"
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
this,
0,
actionIntent,
pflags
)
val intentBuilder = CustomTabsIntent.Builder()
// TODO: change to primary when it's possible to customize custom tabs title color
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
intentBuilder.setShowTitle(true)
intentBuilder.setStartAnimations(
this,
R.anim.slide_in_right,
R.anim.slide_out_left
)
intentBuilder.setExitAnimations(
this,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right
)
val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
intentBuilder.setCloseButtonIcon(closeicon)
val shareLabel = this.getString(R.string.label_share)
val icon = BitmapFactory.decodeResource(
resources,
R.drawable.ic_share_white_24dp
)
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
return intentBuilder.build()
}
fun Context.openItemUrlInternally(
allItems: ArrayList<SelfossModel.Item>,
currentItem: Int,
linkDecoded: String,
customTabsIntent: CustomTabsIntent,
articleViewer: Boolean,
app: Activity
) {
if (articleViewer) {
ReaderActivity.allItems = allItems
val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem)
app.startActivity(intent)
} else {
this.openItemUrlInternalBrowser(
linkDecoded,
customTabsIntent,
app)
}
}
fun Context.openItemUrlInternalBrowser(
linkDecoded: String,
customTabsIntent: CustomTabsIntent,
app: Activity
) {
try {
CustomTabActivityHelper.openCustomTab(
app,
customTabsIntent,
Uri.parse(linkDecoded)
) { _, uri ->
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
} catch (e: Exception) {
openInBrowser(linkDecoded, app)
}
}
fun Context.openItemUrl(
allItems: ArrayList<SelfossModel.Item>,
currentItem: Int,
linkDecoded: String,
customTabsIntent: CustomTabsIntent,
internalBrowser: Boolean,
articleViewer: Boolean,
app: Activity
app: Activity,
) {
if (!linkDecoded.isUrlValid()) {
Toast.makeText(
this,
this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG
Toast.LENGTH_LONG,
).show()
} else {
if (!internalBrowser) {
openInBrowser(linkDecoded, app)
} else if (articleViewer) {
this.openItemUrlInternally(
allItems,
currentItem,
linkDecoded,
customTabsIntent,
articleViewer,
app
)
} else {
this.openItemUrlInternalBrowser(
linkDecoded,
customTabsIntent,
app
)
}
}
}
private fun openInBrowser(linkDecoded: String, app: Activity) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(linkDecoded)
try {
if (articleViewer) {
val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem)
app.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
} else {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(linkDecoded.toStringUriWithHttp())
startActivity(intent)
}
}
}
fun String.isUrlValid(): Boolean =
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlValid(ctx: Context): Boolean {
fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull()
var existsAndEndsWithSlash = false
if (baseUrl != null) {
@@ -172,7 +53,7 @@ fun String.isBaseUrlValid(ctx: Context): Boolean {
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
}
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash)
}
fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
@@ -183,7 +64,10 @@ fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
}
class LinkOnTouchListener : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
override fun onTouch(
v: View?,
event: MotionEvent?,
): Boolean {
var ret = false
val widget: TextView = v as TextView
val text: CharSequence = widget.text
@@ -192,7 +76,8 @@ class LinkOnTouchListener: View.OnTouchListener {
val action = event!!.action
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
action == MotionEvent.ACTION_DOWN
) {
var x: Float = event.x
var y: Float = event.y
@@ -1,9 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.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()
@@ -8,5 +8,4 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
return this
}
fun TextBadgeItem.maybeShow(): TextBadgeItem =
if (this.isHidden) this.show() else this
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
@@ -1,153 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import java.util.List;
/**
* This is a helper class to manage the connection to the Custom Tabs Service.
*/
public class CustomTabActivityHelper implements ServiceConnectionCallback {
private CustomTabsSession mCustomTabsSession;
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;
private ConnectionCallback mConnectionCallback;
/**
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
*
* @param activity The host activity.
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
* @param uri the Uri to be opened.
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
*/
public static void openCustomTab(Activity activity,
CustomTabsIntent customTabsIntent,
Uri uri,
CustomTabFallback fallback) {
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
//If we cant find a package name, it means theres no browser that supports
//Chrome Custom Tabs installed. So, we fallback to the webview
if (packageName == null) {
if (fallback != null) {
fallback.openUri(activity, uri);
}
} else {
customTabsIntent.intent.setPackage(packageName);
customTabsIntent.launchUrl(activity, uri);
}
}
/**
* Unbinds the Activity from the Custom Tabs Service.
*
* @param activity the activity that is connected to the service.
*/
public void unbindCustomTabsService(Activity activity) {
if (mConnection == null) return;
activity.unbindService(mConnection);
mClient = null;
mCustomTabsSession = null;
mConnection = null;
}
/**
* Creates or retrieves an exiting CustomTabsSession.
*
* @return a CustomTabsSession.
*/
public CustomTabsSession getSession() {
if (mClient == null) {
mCustomTabsSession = null;
} else if (mCustomTabsSession == null) {
mCustomTabsSession = mClient.newSession(null);
}
return mCustomTabsSession;
}
/**
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
*
* @param connectionCallback
*/
public void setConnectionCallback(ConnectionCallback connectionCallback) {
this.mConnectionCallback = connectionCallback;
}
/**
* Binds the Activity to the Custom Tabs Service.
*
* @param activity the activity to be binded to the service.
*/
public void bindCustomTabsService(Activity activity) {
if (mClient != null) return;
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
if (packageName == null) return;
mConnection = new ServiceConnection(this);
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
}
/**
* @return true if call to mayLaunchUrl was accepted.
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
*/
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
if (mClient == null) return false;
CustomTabsSession session = getSession();
return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
}
@Override
public void onServiceConnected(CustomTabsClient client) {
mClient = client;
mClient.warmup(0L);
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
}
@Override
public void onServiceDisconnected() {
mClient = null;
mCustomTabsSession = null;
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
}
/**
* A Callback for when the service is connected or disconnected. Use those callbacks to
* handle UI changes when the service is connected or disconnected.
*/
public interface ConnectionCallback {
/**
* Called when the service is connected.
*/
void onCustomTabsConnected();
/**
* Called when the service is disconnected.
*/
void onCustomTabsDisconnected();
}
/**
* To be used as a fallback to open the Uri when Custom Tabs is not available.
*/
public interface CustomTabFallback {
/**
* @param activity The Activity that wants to open the Uri.
* @param uri The uri to be opened by the fallback.
*/
void openUri(Activity activity, Uri uri);
}
}
@@ -1,130 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import androidx.browser.customtabs.CustomTabsService;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.helpers.KeepAliveService;
@SuppressWarnings("ALL")
class CustomTabsHelper {
private static final String TAG = "CustomTabsHelper";
private static final String STABLE_PACKAGE = "com.android.chrome";
private static final String BETA_PACKAGE = "com.chrome.beta";
private static final String DEV_PACKAGE = "com.chrome.dev";
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse;
private CustomTabsHelper() {
}
public static void addKeepAliveExtra(Context context, Intent intent) {
Intent keepAliveIntent = new Intent().setClassName(
context.getPackageName(), KeepAliveService.class.getCanonicalName());
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
}
/**
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
* the one chosen by the user if there is one, otherwise makes a best effort to return a
* valid package name.
* <p>
* This is <strong>not</strong> threadsafe.
*
* @param context {@link Context} to use for accessing {@link PackageManager}.
* @return The package name recommended to use for connecting to custom tabs related components.
*/
public static String getPackageNameToUse(Context context) {
if (sPackageNameToUse != null) return sPackageNameToUse;
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
String defaultViewHandlerPackageName = null;
if (defaultViewHandlerInfo != null) {
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
}
// Get all apps that can handle VIEW intents.
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
List<String> packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);
if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
}
}
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
// and service calls.
if (packagesSupportingCustomTabs.isEmpty()) {
sPackageNameToUse = null;
} else if (packagesSupportingCustomTabs.size() == 1) {
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
sPackageNameToUse = defaultViewHandlerPackageName;
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
sPackageNameToUse = STABLE_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
sPackageNameToUse = BETA_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
sPackageNameToUse = DEV_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
sPackageNameToUse = LOCAL_PACKAGE;
}
return sPackageNameToUse;
}
/**
* Used to check whether there is a specialized handler for a given intent.
*
* @param intent The intent to check with.
* @return Whether there is a specialized handler for the given intent.
*/
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
try {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(
intent,
PackageManager.GET_RESOLVED_FILTER);
if (handlers == null || handlers.isEmpty()) {
return false;
}
for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;
if (filter == null) continue;
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
if (resolveInfo.activityInfo == null) continue;
return true;
}
} catch (RuntimeException e) {
Log.e(TAG, "Runtime exception while getting specialized handlers");
}
return false;
}
/**
* @return All possible chrome package names that provide custom tabs feature.
*/
public static String[] getPackages() {
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
}
}
@@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
import android.content.ComponentName;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import java.lang.ref.WeakReference;
/**
* Implementation for the CustomTabsServiceConnection that avoids leaking the
* ServiceConnectionCallback
*/
public class ServiceConnection extends CustomTabsServiceConnection {
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
mConnectionCallback = new WeakReference<>(connectionCallback);
}
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
}
@Override
public void onServiceDisconnected(ComponentName name) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
}
}
@@ -1,19 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
import androidx.browser.customtabs.CustomTabsClient;
public interface ServiceConnectionCallback {
/**
* Called when the service is connected.
*
* @param client a CustomTabsClient
*/
void onServiceConnected(CustomTabsClient client);
/**
* Called when the service is disconnected.
*/
void onServiceDisconnected();
}
@@ -1,15 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.customtabs.helpers;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
public class KeepAliveService extends Service {
private static final Binder sBinder = new Binder();
@Override
public IBinder onBind(Intent intent) {
return sBinder;
}
}
@@ -1,15 +0,0 @@
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
package bou.amine.apps.readerforselfossv2.android.utils.drawer
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.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)
}
@@ -2,66 +2,38 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import android.widget.ImageView
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.BitmapImageViewTarget
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) =
Glide.with(this)
fun Context.bitmapCenterCrop(
url: String,
iv: ImageView,
) = Glide.with(this)
.asBitmap()
.loadMaybeBasicAuth(config, url)
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(iv)
fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
fun Context.circularDrawable(
url: String,
view: CircleImageView,
) {
view.textView.text = ""
Glide.with(this)
.asBitmap()
.loadMaybeBasicAuth(config, url)
.apply(RequestOptions.centerCropTransform())
.into(object : BitmapImageViewTarget(iv) {
override fun setResource(resource: Bitmap?) {
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
resources,
resource
)
circularBitmapDrawable.isCircular = true
iv.setImageDrawable(circularBitmapDrawable)
}
})
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
.load(url)
.into(view.imageView)
}
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
fun getBitmapInputStream(
bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat,
): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
@@ -1,33 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context
import bou.amine.apps.readerforselfossv2.android.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 com.russhwolf.settings.Settings
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 settings = Settings()
if (settings.getBoolean("isSelfSignedCert", false)) {
val client = getUnsafeHttpClient().build()
registry?.append(
GlideUrl::class.java,
InputStream::class.java,
com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
)
}
}
}
}
@@ -1,52 +1,14 @@
package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.view.View
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.R
import com.google.android.material.snackbar.Snackbar
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar
fun Context.isNetworkAvailable(
v: View? = null,
overrideOffline: Boolean = false
): Boolean {
val networkIsAccessible = isNetworkAccessible(this)
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v
s = Snackbar
.make(
v,
R.string.no_network_connectivity,
Snackbar.LENGTH_INDEFINITE
)
s.setAction(android.R.string.ok) {
snackBarShown = false
s.dismiss()
}
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
snackBarShown = true
}
if (snackBarShown && networkIsAccessible && !overrideOffline) {
s.dismiss()
}
return if(overrideOffline) overrideOffline else networkIsAccessible
}
private fun isNetworkAccessible(context: Context): Boolean {
fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -1,72 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils.persistence
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
fun TagEntity.toView(): SelfossModel.Tag =
SelfossModel.Tag(
this.tag,
this.color,
this.unread
)
fun SourceEntity.toView(): SelfossModel.Source =
SelfossModel.Source(
this.id.toInt(),
this.title,
this.tags.split(","),
this.spout,
this.error,
this.icon
)
fun SelfossModel.Source.toEntity(): SourceEntity =
SourceEntity(
this.id.toString(),
this.getTitleDecoded(),
this.tags.joinToString(","),
this.spout,
this.error,
this.icon.orEmpty()
)
fun SelfossModel.Tag.toEntity(): TagEntity =
TagEntity(
this.tag,
this.color,
this.unread
)
fun AndroidItemEntity.toView(): SelfossModel.Item =
SelfossModel.Item(
this.id.toInt(),
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags.split(",")
)
fun SelfossModel.Item.toEntity(): AndroidItemEntity =
AndroidItemEntity(
this.id.toString(),
this.datetime,
this.getTitleDecoded(),
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.getSourceTitle(),
this.tags.joinToString(",")
)
@@ -0,0 +1,31 @@
package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class AppViewModel(private val repository: Repository) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
if (repository.connectionMonitored) {
if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true)
wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored)
{
_networkAvailableProvider.emit(false)
wasConnected = false
}
}
}
}
}
}
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/ic_launcher_background"/>
<item>
<shape android:shape="rectangle" >
<solid android:color="?attr/colorSurface" />
</shape>
</item>
<item>
<bitmap
Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

@@ -0,0 +1,5 @@
<bitmap
xmlns:android="http://schemas.android.com/apk/res/android"
android:dither="true"
android:src="@drawable/checktile"
android:tileMode="repeat"/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Open Sans"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Roboto"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity"
android:fitsSystemWindows="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordLayout"
@@ -32,8 +31,10 @@
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
android:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme"
/>
</com.google.android.material.appbar.AppBarLayout>
@@ -45,19 +46,19 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/windowBackground">
android:background="?android:attr/windowBackground"
android:orientation="vertical">
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:gravity="center_horizontal"
android:paddingTop="100dp"
android:text="@string/nothing_here"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:background="@android:color/transparent"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
@@ -77,17 +78,13 @@
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.ashokvarma.bottomnavigation.BottomNavigationBar
android:layout_gravity="bottom"
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="60dp"/>
android:layout_height="60dp"
android:layout_gravity="bottom"
app:bnbActiveColor="@color/colorAccent"
app:bnbBackgroundColor="?attr/bottomBarBackground" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
android:id="@+id/mainDrawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" />
</androidx.drawerlayout.widget.DrawerLayout>
@@ -1,33 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
app:layoutDescription="@xml/image_close_scene">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
<androidx.appcompat.widget.Toolbar android:theme="@style/ToolBarStyle"
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="?attr/toolbarPopupTheme"
app:theme="@style/ToolBarStyle" />
/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
app:layout_constraintStart_toStartOf="parent" />
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
@@ -1,11 +1,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context="bou.amine.apps.readerforselfossv2.android.LoginActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -14,18 +15,16 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
android:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
android:padding="@dimen/activity_horizontal_margin">
<!-- Login progress -->
<ProgressBar
android:id="@+id/loginProgress"
@@ -35,12 +34,8 @@
android:layout_marginBottom="8dp"
android:visibility="gone" />
<ScrollView
android:id="@+id/loginForm"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/loginForm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -53,14 +48,22 @@
android:imeOptions="actionUnspecified"
android:importantForAutofill="no"
android:inputType="textUri"
android:maxLines="1" />
android:maxLines="1"
android:minHeight="48dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:text="@string/withLoginSwitch"
android:id="@+id/selfSigned"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:text="@string/disable_ssl"
android:textAlignment="viewStart" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withLogin"
android:layout_weight="1"/>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/withLoginSwitch"
android:textAlignment="viewStart" />
<EditText
android:id="@+id/loginView"
@@ -70,6 +73,7 @@
android:hint="@string/prompt_login"
android:inputType="text"
android:maxLines="1"
android:minHeight="48dp"
android:visibility="gone" />
<EditText
@@ -80,45 +84,7 @@
android:hint="@string/prompt_password"
android:inputType="textPassword"
android:maxLines="1"
android:visibility="gone" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withHttpLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/withHttpLoginSwitch" />
<EditText
android:id="@+id/httpLoginView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/prompt_http_login"
android:inputType="text"
android:visibility="gone" />
<EditText
android:id="@+id/httpPasswordView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/prompt_http_password"
android:inputType="textPassword"
android:visibility="gone" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withSelfhostedCert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/self_hosted_cert_switch" />
<TextView
android:id="@+id/warningText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/self_signed_cert_warning"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:minHeight="48dp"
android:visibility="gone" />
<Button
@@ -132,7 +98,6 @@
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</LinearLayout>
@@ -17,8 +17,10 @@
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme"
app:theme="@style/ToolBarStyle" />
/>
</com.google.android.material.appbar.AppBarLayout>
@@ -1,4 +1,5 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -8,11 +9,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ToolBarStyle" />
/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
@@ -10,12 +10,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
/>
</com.google.android.material.appbar.AppBarLayout>
@@ -24,7 +24,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/source_list_item">
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="bou.amine.apps.readerforselfossv2.android.AddSourceActivity">
tools:context="bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -14,120 +14,86 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/formContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:visibility="gone"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintVertical_bias="0.0">
<TextView
android:text="@string/add_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView2"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textAlignment="center"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="16dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="16dp"
android:gravity="center_horizontal" />
app:layout_constraintTop_toTopOf="parent">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/nameInput"
android:layout_marginTop="32dp"
app:layout_constraintTop_toBottomOf="@+id/textView2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_name"
android:textColorHint="?android:textColorPrimary"
android:autofillHints="false" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:ems="10"
android:id="@+id/sourceUri"
android:hint="@string/add_source_hint_url"
android:textColorHint="?android:textColorPrimary"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/nameInput"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:autofillHints="false" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/tags"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/sourceUri"
android:hint="@string/add_source_hint_tags"
android:textColorHint="?android:textColorPrimary"
android:inputType="text"
android:autofillHints="false" />
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/sourceUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_url"
android:inputType="textUri"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/nameInput" />
<EditText
android:id="@+id/tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_tags"
android:inputType="text"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceUri" />
<Spinner
android:layout_width="match_parent"
android:id="@+id/spoutsSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/tags"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_height="40dp"
android:theme="@style/App.Spinner"/>
app:layout_constraintTop_toBottomOf="@+id/tags" />
<Button
android:text="@string/add_source_save"
android:id="@+id/saveBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/saveBtn"
android:elevation="5dp"
android:textColor="?attr/colorAccent"
android:layout_marginEnd="16dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner"
android:elevation="5dp"
android:text="@string/add_source_save"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="0.0"/>
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -136,8 +102,6 @@
style="?android:attr/progressBarStyleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

Some files were not shown because too many files have changed in this diff Show More