Compare commits

...
190 Commits
Author SHA1 Message Date
aminecmi dd81fd95f8 Source update screen.
continuous-integration/drone/push Build is failing
2022-12-28 22:09:31 +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
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
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
124 changed files with 3821 additions and 3132 deletions
+34 -12
View File
@@ -3,28 +3,38 @@ type: docker
name: test
steps:
- name: AnylyseBuildTest
- name: BuildAndTest
image: mingc/android-build-box:latest
failure: ignore
commands:
- echo "---------------------------------------------------------"
- echo "Analysing..."
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Building..."
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
- ./gradlew build -x test
- echo "---------------------------------------------------------"
- echo "Testing..."
- echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport
environment:
TZ: Europe/Paris
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
- name: Analyse
image: kytay/sonar-node-plugin
settings:
sonar_host:
from_secret: sonarScannerHostUrl
sonar_token:
from_secret: sonarScannerLogin
use_node_version: 16.18.1
sonar_debug: true
sonar_project_settings: ./sonar-project.properties
trigger:
event:
- push
- pull_request
---
kind: pipeline
@@ -32,14 +42,21 @@ type: docker
name: Publish
steps:
- name: createTag
- name: createTagAndChangelog
image: ubuntu:latest
commands:
- apt-get update && apt-get install -y git
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $VER..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"
- ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- git push pushing master
- git push pushing --tags
environment:
TZ: Europe/Paris
GITEA_USR:
from_secret: giteaUsr
GITEA_PASS:
@@ -65,10 +82,7 @@ steps:
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/
- 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:
@@ -85,8 +99,15 @@ steps:
- name: build
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Fetch tags..."
- git fetch --tags
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\npushCache=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 -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false
- echo "---------------------------------------------------------"
- echo "Get Key"
- wget https://amine-louveau.fr/key
@@ -100,6 +121,7 @@ steps:
- echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment:
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass
YOUR_KEY_ALIAS:
-25
View File
@@ -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"
```
+56 -45
View File
@@ -1,11 +1,14 @@
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") version "0.6.1"
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@@ -30,7 +33,7 @@ fun gitVersion(): String {
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 {
@@ -54,11 +57,15 @@ fun versionNameFromGit(): String {
android {
compileOptions {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileSdk = 31
// For Kotlin projects
kotlinOptions {
jvmTarget = "11"
}
compileSdk = 33
buildToolsVersion = "31.0.0"
buildFeatures {
viewBinding = true
@@ -66,7 +73,7 @@ android {
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21
targetSdk = 31
targetSdk = 33
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@@ -79,6 +86,11 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
@@ -86,9 +98,6 @@ 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")
@@ -98,9 +107,6 @@ android {
dimension = "build"
}
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "bou.amine.apps.readerforselfossv2.android"
}
@@ -114,13 +120,6 @@ dependencies {
implementation("androidx.preference:preference-ktx:1.1.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
@@ -129,46 +128,28 @@ dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
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.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")
//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.14.2")
implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2")
// Themes
implementation("com.52inc:scoops:1.0.0")
implementation("com.jaredrummler:colorpicker:1.1.0")
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager
@@ -191,14 +172,44 @@ dependencies {
implementation("androidx.core:core-ktx:1.8.0")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
// implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
// implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.3")
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.6.0")
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
}
+2 -7
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
@@ -90,3 +83,5 @@
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontwarn io.mockk.**
-keep class io.mockk.** { *; }
+3 -5
View File
@@ -15,7 +15,8 @@
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar"
android:dataExtractionRules="@xml/data_extraction_rules">
android:dataExtractionRules="@xml/data_extraction_rules"
android:configChanges="uiMode">
<activity
android:name=".MainActivity"
android:theme="@style/SplashTheme"
@@ -51,7 +52,7 @@
android:value=".HomeActivity" />
</activity>
<activity
android:name=".AddSourceActivity"
android:name=".UpsertSourceActivity"
android:parentActivityName=".SourcesActivity"
android:exported="true">
<meta-data
@@ -78,8 +79,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,23 +1,17 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.*
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
@@ -28,50 +22,29 @@ import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter
import bou.amine.apps.readerforselfossv2.android.adapters.ItemsAdapter
import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.*
import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.materialdrawer.holder.BadgeStyle
import com.mikepenz.materialdrawer.holder.ColorHolder
import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.addStickyFooterItem
import com.mikepenz.materialdrawer.util.updateBadge
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
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private val DRAWER_ID_TAGS = 100101L
private val DRAWER_ID_HIDDEN_TAGS = 101100L
private val DRAWER_ID_SOURCES = 100110L
private val DRAWER_ID_FILTERS = 100111L
private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD
@@ -80,8 +53,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private lateinit var tabNewBadge: TextBadgeItem
private lateinit var tabArchiveBadge: TextBadgeItem
private lateinit var tabStarredBadge: TextBadgeItem
private lateinit var customTabActivityHelper: CustomTabActivityHelper
private lateinit var appColors: AppColors
private var offset: Int = 0
private var firstVisible: Int = 0
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
@@ -91,8 +62,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var fromTabShortcut: Boolean = false
private lateinit var tagsBadge: Map<Long, Int>
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings()
}
@@ -101,18 +70,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
override fun onStart() {
super.onStart()
customTabActivityHelper.bindCustomTabsService(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
// Add appcolors to DI
appColors = AppColors(this@HomeActivity)
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
@@ -126,27 +85,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
setContentView(view)
handleThemeBinding()
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
val mDrawerToggle = ActionBarDrawerToggle(this, binding.drawerContainer, binding.toolBar, R.string.material_drawer_open, R.string.material_drawer_close)
binding.drawerContainer.addDrawerListener(mDrawerToggle)
mDrawerToggle.syncState()
customTabActivityHelper = CustomTabActivityHelper()
handleBottomBar()
initDrawer()
handleSwipeRefreshLayout()
getElementsAccordingToTab()
CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isItemCachingEnabled()) {
CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes()
}
}
}
@@ -159,7 +108,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false
lastFetchDone = false
handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
@@ -199,18 +147,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
adapter.handleItemAtIndex(position)
reloadBadgeContent()
val tagHashes = i.tags.map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
(it.key to (it.value - 1))
} else {
(it.key to it.value)
}
}.toMap()
reloadTagsBadges()
// Just load everythin
if (items.size <= 0) {
getElementsAccordingToTab()
@@ -228,32 +164,61 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
if (count > 0) {
badge
.setText(count.toString())
.maybeShow()
} else {
badge.removeBadge()
}
}
private fun handleBottomBar() {
tabNewBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setBackgroundColor(appColors.colorPrimary)
tabArchiveBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setBackgroundColor(appColors.colorPrimary)
tabStarredBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setBackgroundColor(appColors.colorPrimary)
if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch {
repository.badgeUnread.collect {
updateBottomBarBadgeCount(tabNewBadge, it)
}
}
}
if (appSettingsService.isDisplayAllCountEnabled()) {
lifecycleScope.launch {
repository.badgeAll.collect {
updateBottomBarBadgeCount(tabArchiveBadge, it)
}
}
lifecycleScope.launch {
repository.badgeStarred.collect {
updateBottomBarBadgeCount(tabStarredBadge, it)
}
}
}
val tabNew =
BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new)
).setActiveColor(appColors.colorAccent)
)
.setBadgeItem(tabNewBadge)
val tabArchive =
BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read)
).setActiveColor(appColors.colorAccentDark)
)
.setBadgeItem(tabArchiveBadge)
val tabStarred =
BottomNavigationItem(
@@ -269,7 +234,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.setFirstSelectedPosition(0)
.initialise()
binding.bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
if (fromTabShortcut) {
binding.bottomBar.selectTab(elementsShown.position - 1)
@@ -279,12 +243,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onResume() {
super.onResume()
appColors = AppColors(this@HomeActivity)
handleDrawerItems()
handleThemeUpdate()
reloadLayoutManager()
if (appSettingsService.isInfiniteLoadingEnabled()) {
@@ -295,6 +253,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleBottomBarActions()
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask()
CoroutineScope(Dispatchers.Main).launch {
@@ -304,276 +264,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab()
}
override fun onStop() {
super.onStop()
customTabActivityHelper.unbindCustomTabsService(this)
}
private fun handleThemeBinding() {
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
private fun handleThemeUpdate() {
val scoop = Scoop.getInstance()
scoop.update(Toppings.PRIMARY.value, appColors.colorPrimary)
scoop.update(Toppings.PRIMARY_DARK.value, appColors.colorPrimaryDark)
}
private fun initDrawer() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(this@HomeActivity)
.asBitmap()
.load(uri)
.apply(RequestOptions()
.placeholder(R.mipmap.ic_launcher)
.fallback(R.mipmap.ic_launcher)
.fitCenter())
.into(imageView)
private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss()
}
override fun cancel(imageView: ImageView) {
Glide.with(this@HomeActivity).clear(imageView)
}
})
val drawerListener = object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
// We do nothing
}
override fun onDrawerOpened(drawerView: View) {
binding.bottomBar.hide()
}
override fun onDrawerClosed(drawerView: View) {
binding.bottomBar.show()
}
override fun onDrawerStateChanged(newState: Int) {
// We do nothing
}
}
binding.drawerContainer.addDrawerListener(drawerListener)
// Sticky items
addStickyPrimaryItem(R.string.drawer_report_bug, R.drawable.ic_bug_report_black_24dp) { _, _, _ ->
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
startActivity(browserIntent)
false
}
addStickyPrimaryItem(R.string.title_activity_settings, R.drawable.ic_settings_black_24dp) { _, _, _ ->
settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
false
}
}
private fun addStickyPrimaryItem(name: Int, icon: Int, clickListener: ((v: View?, item: IDrawerItem<*>, position: Int) -> Boolean)?) {
binding.mainDrawer.addStickyFooterItem(
PrimaryDrawerItem().apply {
nameRes = name
iconRes = icon
isIconTinted = true
onDrawerItemClickListener = clickListener
})
}
private fun handleDrawerItems() {
tagsBadge = emptyMap()
binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply {
nameRes = R.string.drawer_loading
isSelectable = false
}
)
CoroutineScope(Dispatchers.IO).launch {
val drawerData = DrawerData(repository.getDBTags().map { it.toView() },
repository.getDBSources().map { it.toView() })
runOnUiThread {
// Only refresh if there is no data in the DB, or if the `UpdateSources` setting is enabled
if (drawerData.sources?.isEmpty() == true || appSettingsService.isUpdateSourcesEnabled()) {
drawerApiCalls(drawerData)
} else {
handleDrawerData(drawerData, loadedFromCache = true)
}
}
}
}
private fun drawerApiCalls(drawerData: DrawerData) {
CoroutineScope(Dispatchers.Main).launch {
val apiDrawerData = DrawerData(repository.getTags(), repository.getSources())
handleDrawerData(if (drawerData != apiDrawerData) apiDrawerData else drawerData)
}
}
private fun handleDrawerData(drawerData: DrawerData, loadedFromCache: Boolean = false) {
binding.mainDrawer.itemAdapter.clear()
// Filters title with clear action
secondaryItem(withDivider = false, R.string.drawer_item_filters, DRAWER_ID_FILTERS, R.string.drawer_action_clear) { _,_,_ ->
repository.sourceFilter = null
repository.tagFilter = null
binding.mainDrawer.setSelectionAtPosition(-1)
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
// Hidden tags
if (drawerData.tags != null && drawerData.tags.isNotEmpty() && appSettingsService.getHiddenTags().isNotEmpty()) {
secondaryItem(
withDivider = true,
R.string.drawer_item_hidden_tags,
DRAWER_ID_HIDDEN_TAGS
)
handleHiddenTags(drawerData.tags)
}
// Tags
secondaryItem(withDivider = true, R.string.drawer_item_tags, DRAWER_ID_TAGS)
if (drawerData.tags == null && !loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem()
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
)
} else {
handleTags(drawerData.tags!!)
}
// Sources
secondaryItem(withDivider = true, R.string.drawer_item_sources, DRAWER_ID_SOURCES, R.string.drawer_action_edit) { v, _, _ ->
startActivity(Intent(v!!.context, SourcesActivity::class.java))
false
}
if (drawerData.sources == null && !loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_tags
isSelectable = false
}
)
} else {
handleSources(drawerData.sources!!)
}
// About action
binding.mainDrawer.itemAdapter.add(
DividerDrawerItem(),
PrimaryDrawerItem().apply {
nameRes = R.string.action_about
isSelectable = false
iconRes = R.drawable.ic_info_outline_white_24dp
isIconTinted = true
onDrawerItemClickListener = { _,_,_ ->
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(this@HomeActivity)
false
}
}
)
}
private fun secondaryItem(withDivider: Boolean, name: Int, id: Long, badgeId: Int? = null, clickListener: ((v: View?, item: IDrawerItem<*>, position: Int) -> Boolean)? = null) {
if (withDivider) {
binding.mainDrawer.itemAdapter.add(DividerDrawerItem())
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = name
identifier = id
isSelectable = false
if (badgeId != null) {
badgeRes = badgeId
}
onDrawerItemClickListener = clickListener
}
)
}
private fun createDrawerItem(it: SelfossModel.Tag) {
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(it.color)
} catch (e: IllegalArgumentException) {
appColors.colorPrimary
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
val drawerItem = PrimaryDrawerItem()
.apply {
nameText = it.tag.getHtmlDecoded()
identifier = it.tag.longHash()
iconDrawable = gd
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent)
}
onDrawerItemClickListener = { _, _, _ ->
repository.tagFilter = it
repository.sourceFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
}
if (it.unread > 0) {
drawerItem.badgeText = it.unread.toString()
}
binding.mainDrawer.itemAdapter.add(drawerItem)
}
private fun handleTags(tags: List<SelfossModel.Tag>) {
val filteredTags = tags
.filterNot { appSettingsService.getHiddenTags().contains(it.tag) }
.sortedBy { it.tag }
createTagItems(filteredTags)
}
private fun handleHiddenTags(tags: List<SelfossModel.Tag>) {
val filteredHiddenTags: List<SelfossModel.Tag> =
tags.filter { appSettingsService.getHiddenTags().contains(it.tag) }
createTagItems(filteredHiddenTags)
}
private fun createTagItems(tags: List<SelfossModel.Tag>) {
tagsBadge = tags.associate {
createDrawerItem(it)
(it.tag.longHash() to it.unread)
}
}
private fun handleSources(sources: List<SelfossModel.Source>) {
for (source in sources) {
val item = PrimaryDrawerItem().apply {
nameText = source.title.getHtmlDecoded()
identifier = source.id.toLong()
iconUrl = source.getIcon(repository.baseUrl)
onDrawerItemClickListener = { _,_,_ ->
repository.sourceFilter = source
repository.tagFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
}
binding.mainDrawer.itemAdapter.add(item)
alertDialog.show()
}
}
@@ -658,7 +364,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
})
}
private fun fetchOnEmptyList() {
fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout {
// TODO: do if last element (or is empty ?)
getElementsAccordingToTab(true)
@@ -699,7 +405,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.emptyText.visibility = View.GONE
}
private fun getElementsAccordingToTab(
fun getElementsAccordingToTab(
appendResults: Boolean = false
) {
offset = if (appendResults && items.size > 0) {
@@ -744,8 +450,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemCardAdapter(
this,
items,
customTabActivityHelper,
appColors,
) {
updateItems(it)
}
@@ -754,8 +458,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemListAdapter(
this,
items,
customTabActivityHelper,
appColors,
) {
updateItems(it)
}
@@ -778,36 +480,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges()
reloadBadgeContent()
}
}
}
private fun reloadBadgeContent() {
if (appSettingsService.isDisplayUnreadCountEnabled()) {
tabNewBadge
.setText(repository.badgeUnread.toString())
.maybeShow()
}
if (appSettingsService.isDisplayAllCountEnabled()) {
tabArchiveBadge
.setText(repository.badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(repository.badgeStarred.toString())
.maybeShow()
}
}
private fun reloadTagsBadges() {
tagsBadge.forEach {
binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString()))
}
binding.mainDrawer.resetDrawerContent()
}
private fun calculateNoOfColumns(): Int {
val displayMetrics = resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
@@ -853,6 +531,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_filter -> {
val filterSheetFragment = FilterSheetFragment()
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
return true
}
R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
@@ -892,8 +575,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
).show()
tabNewBadge.removeBadge()
handleDrawerItems()
getElementsAccordingToTab()
} else {
Toast.makeText(
@@ -910,10 +591,20 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true
}
R.id.action_disconnect -> {
appSettingsService.clearAll()
CoroutineScope(Dispatchers.Main).launch {
repository.logout()
}
this@HomeActivity.finish()
val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent)
this@HomeActivity.finish()
return true
}
R.id.action_settings -> {
settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
return true
}
R.id.action_sources -> {
startActivity(Intent(this, SourcesActivity::class.java))
return true
}
else -> return super.onOptionsItemSelected(item)
@@ -922,10 +613,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun maxItemNumber(): Int =
when (elementsShown) {
ItemType.UNREAD -> repository.badgeUnread
ItemType.ALL -> repository.badgeAll
ItemType.STARRED -> repository.badgeStarred
else -> repository.badgeUnread // if !elementsShown then unread are fetched.
ItemType.UNREAD -> repository.badgeUnread.value
ItemType.ALL -> repository.badgeAll.value
ItemType.STARRED -> repository.badgeStarred.value
else -> repository.badgeUnread.value // if !elementsShown then unread are fetched.
}
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
@@ -10,10 +11,11 @@ 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.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -21,26 +23,30 @@ import com.mikepenz.aboutlibraries.LibsBuilder
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
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0
private var isWithLogin = false
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()
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
@@ -51,12 +57,40 @@ class LoginActivity : AppCompatActivity(), DIAware {
handleBaseUrlFail()
if (appSettingsService.getBaseUrl().isNotEmpty()) {
goToMain()
showProgress(true)
// This should be reverted when "old" users connected with a non-selfoss rss
// are handled. Revert to "simple" way.
CoroutineScope(Dispatchers.Main).launch {
try {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
showProgress(false)
if (displaySelfossOnly) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG
).show()
}
repository.logout()
}
} catch (e: Throwable) {
repository.logout()
showProgress(false)
}
}
}
handleActions()
}
@SuppressLint("WrongConstant") // Constant is fetched from the settings
private fun handleTheme() {
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
}
private fun handleActions() {
binding.passwordView.setOnEditorActionListener(
@@ -94,12 +128,16 @@ class LoginActivity : AppCompatActivity(), DIAware {
}
private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
}
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}
private fun preferenceError(t: Throwable) {
private fun preferenceError() {
appSettingsService.resetLoginInformation()
binding.urlView.error = getString(R.string.wrong_infos)
@@ -122,7 +160,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
var cancel = false
var focusView: View? = null
if (url.isBaseUrlInvalid(this@LoginActivity)) {
if (url.isBaseUrlInvalid()) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
cancel = true
@@ -161,18 +199,27 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.Main).launch {
val result = repository.login()
if (result) {
repository.updateApiVersion()
goToMain()
} else {
CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success"))
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
if (displaySelfossOnly) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG
).show()
}
preferenceError()
}
} else {
preferenceError()
}
showProgress(false)
}
showProgress(false)
}
}
@@ -186,11 +233,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
.alpha(
if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
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
binding.loginProgress
@@ -199,11 +246,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
.alpha(
if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
}
}
}
)
)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -3,33 +3,32 @@ 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.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.github.ln_12.library.ConnectivityStatus
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
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 {
@@ -38,7 +37,7 @@ class MyApp : MultiDexApplication(), DIAware {
import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) }
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
}
@@ -48,37 +47,66 @@ class MyApp : MultiDexApplication(), DIAware {
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()
Napier.base(DebugAntilog())
initDrawerImageLoader()
if (!ACRA.isACRASenderServiceProcess()) {
tryToHandleBug()
initTheme()
handleNotificationChannels()
tryToHandleBug()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
handleNotificationChannels()
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
}
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()
}
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-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "LMTlLZuazADohTCm"
basicAuthPassword = "he6ghHp83F0PYPfh"
httpMethod = HttpSender.Method.POST
}
}
}
private fun handleNotificationChannels() {
@@ -98,41 +126,13 @@ class MyApp : MultiDexApplication(), DIAware {
}
}
private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(imageView.context)
.load(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
} else {
oldHandler.uncaughtException(thread, e)
}
@@ -12,12 +12,9 @@ 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.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -28,21 +25,22 @@ import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware {
private var currentItem: Int = 0
private lateinit var appColors: AppColors
private lateinit var toolbarMenu: Menu
private lateinit var binding: ActivityReaderBinding
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)
}
}
@@ -56,26 +54,23 @@ class ReaderActivity : AppCompatActivity(), DIAware {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appColors = AppColors(this)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
if (allItems.isEmpty()) {
currentItem = intent.getIntExtra("currentItem", 0)
allItems = repository.getReaderItems()
if (allItems.isEmpty() || currentItem > allItems.size) {
finish()
}
currentItem = intent.getIntExtra("currentItem", 0)
readItem(allItems[currentItem])
binding.pager.adapter = ScreenSlidePagerAdapter(this)
@@ -223,8 +218,4 @@ class ReaderActivity : AppCompatActivity(), DIAware {
startActivity(intent)
overridePendingTransition(0, 0)
}
companion object {
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
}
}
@@ -8,11 +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.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,21 +19,15 @@ 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)
@@ -45,8 +36,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() {
@@ -65,20 +56,13 @@ class SourcesActivity : AppCompatActivity(), DIAware {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
if (response.isNotEmpty()) {
items = response
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,
@@ -89,7 +73,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
}
binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
}
}
}
@@ -3,17 +3,17 @@ package bou.amine.apps.readerforselfossv2.android
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import android.widget.Toast
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.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 com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -22,82 +22,73 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance
class AddSourceActivity : AppCompatActivity(), DIAware {
class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.Source? = null
private var mSpoutsValue: String? = null
private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding
private lateinit var binding: ActivityUpsertSourceBinding
override val di by closestDI()
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity)
super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater)
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)
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)
supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
binding.saveBtn.setTextColor(appColors.colorAccent)
maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener {
handleSaveSource(
binding.tags,
binding.nameInput.text.toString(),
binding.sourceUri.text.toString()
)
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(this@AddSourceActivity)) {
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer)
handleSpoutsSpinner()
}
}
private fun handleSpoutsSpinner(
spoutsSpinner: Spinner,
mProgress: ProgressBar,
formContainer: ConstraintLayout
) {
private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>()
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
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()
@@ -113,33 +104,37 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText(
this@AddSourceActivity,
this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT
).show()
mProgress.visibility = View.GONE
binding.progress.visibility = View.GONE
}
CoroutineScope(Dispatchers.Main).launch {
try {
val items = repository.getSpouts()
if (items != null) {
if (items.isNotEmpty()) {
val itemsStrings = items.map { it.value.name }
for ((key, value) in items) {
spoutsKV[value.name] = key
}
mProgress.visibility = View.GONE
formContainer.visibility = View.VISIBLE
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter =
ArrayAdapter(
this@AddSourceActivity,
this@UpsertSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter
binding.spoutsSpinner.adapter = spinnerArrayAdapter
if (existingSource != null) {
initFields(items)
}
} else {
handleSpoutFailure()
}
@@ -150,13 +145,11 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
}
private fun maybeGetDetailsFromIntentSharing(
intent: Intent,
sourceUri: EditText,
nameInput: EditText
intent: Intent
) {
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
}
}
@@ -167,7 +160,8 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
finish()
}
private fun handleSaveSource(tags: EditText, title: String, url: String) {
private fun handleSaveSource() {
val url = binding.sourceUri.text.toString()
val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
@@ -178,18 +172,27 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
}
else -> {
CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = repository.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
"",
)
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@AddSourceActivity,
this@UpsertSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
@@ -198,4 +201,9 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
}
}
}
override fun onDestroy() {
super.onDestroy()
repository.unsetSelectedSource()
}
}
@@ -10,11 +10,12 @@ 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.toTextDrawableString
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.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.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
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.service.AppSettingsService
@@ -34,8 +35,6 @@ import org.kodein.di.instance
class ItemCardAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
private val helper: CustomTabActivityHelper,
override val appColors: AppColors,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
@@ -44,8 +43,8 @@ class ItemCardAdapter(
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 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)
@@ -61,9 +60,9 @@ class ItemCardAdapter(
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 = itm.sourceAuthorAndDate()
if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight
@@ -83,10 +82,10 @@ class ItemCardAdapter(
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
TextDrawable
.builder()
.round()
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
@@ -101,7 +100,7 @@ class ItemCardAdapter(
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleClickListeners()
handleCustomTabActions()
handleLinkOpening()
}
private fun handleClickListeners() {
@@ -111,41 +110,32 @@ class ItemCardAdapter(
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
// TODO: Handle failure
}
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
}
}
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
appSettingsService.isInternalBrowserEnabled(),
appSettingsService.isArticleViewerEnabled(),
app
)
@@ -5,12 +5,10 @@ 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.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.buildCustomTabsIntent
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.utils.openItemUrl
@@ -29,8 +27,6 @@ import org.kodein.di.instance
class ItemListAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
private val helper: CustomTabActivityHelper,
override val appColors: AppColors,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
@@ -53,9 +49,9 @@ class ItemListAdapter(
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 = itm.sourceAuthorAndDate()
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
@@ -83,20 +79,15 @@ class ItemListAdapter(
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleCustomTabActions()
handleLinkOpening()
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
appSettingsService.isInternalBrowserEnabled(),
appSettingsService.isArticleViewerEnabled(),
app
)
@@ -5,7 +5,6 @@ import android.graphics.Color
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -21,7 +20,6 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
abstract val repository: Repository
abstract val appSettingsService: AppSettingsService
abstract val app: Activity
abstract val appColors: AppColors
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
@@ -30,7 +28,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
updateItems(this.items)
}
private fun unmarkSnackbar(i: SelfossModel.Item, position: Int) {
private fun unmarkSnackbar(item: SelfossModel.Item, position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
@@ -38,9 +36,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG
)
.setAction(R.string.undo_string) {
CoroutineScope(Dispatchers.IO).launch {
unreadItemAtIndex(position, false)
}
unreadItemAtIndex(item, position, false)
}
val view = s.view
@@ -49,7 +45,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}
private fun markSnackbar(position: Int) {
private fun markSnackbar(item: SelfossModel.Item, position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
@@ -57,7 +53,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG
)
.setAction(R.string.undo_string) {
readItemAtIndex(position)
readItemAtIndex(item, position, false)
}
val view = s.view
@@ -68,37 +64,36 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
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)
repository.markAsRead(item)
}
if (repository.displayedItems == ItemType.UNREAD) {
items.remove(i)
items.remove(item)
notifyItemRemoved(position)
updateItems(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])
repository.unmarkAsRead(item)
}
notifyItemChanged(position)
if (showSnackbar) {
markSnackbar(position)
markSnackbar(item, position)
}
}
@@ -2,12 +2,14 @@ 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.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.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
@@ -61,9 +63,13 @@ class SourcesListAdapter(
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(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
init {
handleClickListeners()
@@ -74,13 +80,13 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
val (id) = items[adapterPosition]
val (id, title) = items[bindingAdapterPosition]
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(bindingAdapterPosition)
notifyItemRemoved(bindingAdapterPosition)
notifyItemRangeChanged(bindingAdapterPosition, itemCount)
} else {
Toast.makeText(
app,
@@ -90,6 +96,14 @@ class SourcesListAdapter(
}
}
}
mView.setOnClickListener {
val source = items[bindingAdapterPosition]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
}
}
}
@@ -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-louveau.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>
}
@@ -52,11 +52,13 @@ override fun doWork(): Result {
repository.handleDBActions()
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isNotifyNewItemsEnabled()) {
launch {
handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notificationManager)
handleNewItemsNotification(apiItems, notificationManager)
}
}
apiItems.map { it.preloadImages(context) }
}
}
return Result.success()
@@ -66,6 +68,7 @@ override fun doWork(): Result {
newItems: List<SelfossModel.Item>?,
notificationManager: NotificationManager
) {
// TODO: Check if this coroutine is actually required
CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty()
@@ -102,7 +105,6 @@ override fun doWork(): Result {
notificationManager.notify(2, newItemsNotification.build())
}
}
apiItems.map { it.preloadImages(context) }
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
@@ -8,6 +8,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,27 +16,21 @@ 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 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.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.buildCustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInternalBrowser
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.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
@@ -53,18 +48,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.SocketTimeoutException
import java.net.URL
import java.util.*
import java.util.concurrent.ExecutionException
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
@@ -72,10 +65,9 @@ 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 textAlignment: String
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private val binding get() = _binding
override val di : DI by closestDI()
private val repository: Repository by instance()
@@ -86,16 +78,10 @@ class ArticleFragment : Fragment(), DIAware {
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())
super.onCreate(savedInstanceState)
val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!!
@@ -115,46 +101,30 @@ class ArticleFragment : Fragment(), DIAware {
contentText = item.content
contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText(repository.dateUtils)
contentSource = item.sourceAuthorAndDate()
allImages = item.getImages()
fontSize = appSettingsService.getFontSize()
staticBar = appSettingsService.isStaticBarEnabled()
font = appSettingsService.getFont()
if (font.isNotEmpty()) {
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
// Just to be sure
null
}
}
refreshAlignment()
fab = binding.fab
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
val floatingToolbar: FloatingToolbar = binding!!.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = requireActivity().buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
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) {
@@ -194,35 +164,35 @@ class ArticleFragment : Fragment(), DIAware {
floatingToolbar.show()
}
binding.source.text = contentSource
binding!!.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
binding!!.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent)
getContentFromMercury()
} else {
binding.titleView.text = contentTitle
binding!!.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
binding!!.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
binding!!.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
.into(binding!!.imageView)
} else {
binding.imageView.visibility = View.GONE
binding!!.imageView.visibility = View.GONE
}
}
binding.nestedScrollView.setOnScrollChangeListener(
binding!!.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
@@ -238,6 +208,7 @@ class ArticleFragment : Fragment(), DIAware {
)
} catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available")
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
@@ -250,7 +221,7 @@ class ArticleFragment : Fragment(), DIAware {
.show()
}
return binding.root
return binding!!.root
}
override fun onDestroyView() {
@@ -266,142 +237,134 @@ class ArticleFragment : Fragment(), DIAware {
}
}
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
binding!!.progressBar.visibility = View.VISIBLE
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 {
val response = mercuryApi.query(url)
if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) {
binding!!.titleView.text = response.data!!.title.orEmpty()
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
}
try {
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.load(
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {
}
} else {
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
}
}
try {
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
}
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
}
}
if (typeface != null) {
binding!!.titleView.typeface = typeface
}
} catch (e: Exception) {
if (context != null) {
}
e.sendSilentlyWithAcraWithName("getContentFromMercury > typeface")
}
}
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.data!!.url)
url = response.data!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url
e.sendSilentlyWithAcraWithName("getContentFromMercury > malformedurlexception")
}
try {
contentText = response.data!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html")
}
if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) {
try {
binding!!.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.load(
response.data!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding!!.imageView)
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image")
}
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image")
}
} else {
binding!!.imageView.visibility = View.GONE
}
try {
binding!!.nestedScrollView.scrollTo(0, 0)
binding!!.progressBar.visibility = View.GONE
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview")
}
} else {
openInBrowserAfterFailing()
}
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing")
openInBrowserAfterFailing()
}
)
}
}
}
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
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")
}
val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding.webcontent.webViewClient = object : WebViewClient() {
binding!!.webcontent.settings.useWideViewPort = true
binding!!.webcontent.settings.loadWithOverviewMode = true
binding!!.webcontent.settings.javaScriptEnabled = false
binding!!.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
if (binding!!.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
return true
}
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
@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")) {
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) {}
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
}
}
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) {}
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url")
}
}
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) {}
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
}
}
return super.shouldInterceptRequest(view, url)
@@ -409,14 +372,14 @@ class ArticleFragment : Fragment(), DIAware {
}
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
})
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
binding!!.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
binding.webcontent.settings.layoutAlgorithm =
binding!!.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null
@@ -425,11 +388,13 @@ class ArticleFragment : Fragment(), DIAware {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > item url")
}
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 -> ""
}
@@ -445,7 +410,7 @@ class ArticleFragment : Fragment(), DIAware {
""
}
binding.webcontent.loadDataWithBaseURL(
binding!!.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
|<head>
@@ -458,10 +423,10 @@ 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;
@@ -469,11 +434,11 @@ 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;
| }
@@ -483,7 +448,7 @@ 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
@@ -498,22 +463,18 @@ class ArticleFragment : Fragment(), DIAware {
}
fun scrollDown() {
val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, height/2)
val height = binding!!.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, height/2)
}
fun scrollUp() {
val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, -height/2)
val height = binding!!.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, -height/2)
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
binding.progressBar.visibility = View.GONE
requireActivity().openItemUrlInternalBrowser(
url,
customTabsIntent,
requireActivity()
)
private fun openInBrowserAfterFailing() {
binding!!.progressBar.visibility = View.GONE
requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
}
companion object {
@@ -531,10 +492,10 @@ 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 (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 position : Int = allImages.indexOf(binding!!.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages)
@@ -0,0 +1,181 @@
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.Bundle
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.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
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 {
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 {
val binding =
bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate(
inflater,
container,
false
)
val context: Context? = context
val tagGroup = binding.tagsGroup
val sourceGroup = binding.sourcesGroup
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags()
tags.forEach { tag ->
val c = Chip(context)
c.text = tag.tag
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
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)
}
repository.getSources().forEach { source ->
val c = Chip(context)
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.listener(object : RequestListener<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
c.chipIcon = resource
return false
}
}).preload()
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
}
sourceGroup.addView(c)
}
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
}
}
binding.floatingActionButton2.setOnClickListener {
(activity as HomeActivity).getElementsAccordingToTab()
(activity as HomeActivity).fetchOnEmptyList()
dismiss()
}
return binding.root
}
companion object {
const val TAG = "FilterModalBottomSheet"
}
}
@@ -28,7 +28,7 @@ class ImageFragment : Fragment() {
val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE
Glide.with(activity)
Glide.with(requireActivity())
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
@@ -2,11 +2,13 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context
import android.webkit.URLUtil
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.acra.ktx.sendSilentlyWithAcra
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
@@ -23,6 +25,7 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
}
}
} catch (e : Error) {
e.sendSilentlyWithAcraWithName("preloadImages")
return false
}
@@ -35,7 +38,7 @@ fun String.toTextDrawableString(): String {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
// We do nothing
e.sendSilentlyWithAcraWithName("toTextDrawableString")
}
}
return textDrawable.toString()
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.os.Parcel
import android.os.Parcelable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem(
@@ -17,7 +16,8 @@ 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(
@@ -31,20 +31,22 @@ 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
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 {
@@ -66,7 +68,8 @@ 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
@@ -83,5 +86,6 @@ data class ParecelableItem(
dest.writeString(link)
dest.writeString(sourcetitle)
dest.writeString(tags)
dest.writeString(author)
}
}
@@ -7,39 +7,32 @@ import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.TextWatcher
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
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 androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.ftinc.scoop.Scoop
import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
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
@@ -104,6 +97,21 @@ class SettingsActivity : AppCompatActivity(),
class MainPreferenceFragment : PreferenceFragmentCompat() {
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
}
}
}
@@ -120,6 +128,7 @@ class SettingsActivity : AppCompatActivity(),
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()
}
""
@@ -143,6 +152,7 @@ class SettingsActivity : AppCompatActivity(),
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
}
}
} }
@@ -152,6 +162,7 @@ class SettingsActivity : AppCompatActivity(),
val input = (dest.toString() + source.toString()).toInt()
if (input > 0) return@InputFilter null
} catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
}
""
}
@@ -169,21 +180,11 @@ class SettingsActivity : AppCompatActivity(),
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.settings_theme, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.clear) {
AppColors.resetColors()
requireActivity().recreate()
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
return super.onOptionsItemSelected(item)
}
}
@@ -1,72 +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)
a.resources.getColor(R.color.darkBackground)
} else {
a.setTheme(R.style.NoBar)
a.resources.getColor(R.color.grey_50)
}
textColor = if (isDarkTheme) {
a.resources.getColor(R.color.white)
} else {
a.resources.getColor(R.color.grey_900)
}
}
companion object {
fun resetColors() {
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")
}
}
}
@@ -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)
}
@@ -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,109 +11,15 @@ 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.utils.customtabs.CustomTabActivityHelper
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
) {
@@ -129,41 +31,23 @@ fun Context.openItemUrl(
Toast.LENGTH_LONG
).show()
} else {
if (!internalBrowser) {
openInBrowser(linkDecoded, app)
} else if (articleViewer) {
this.openItemUrlInternally(
allItems,
currentItem,
linkDecoded,
customTabsIntent,
articleViewer,
app
)
if (articleViewer) {
val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem)
app.startActivity(intent)
} else {
this.openItemUrlInternalBrowser(
linkDecoded,
customTabsIntent,
app
)
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(linkDecoded.toStringUriWithHttp())
startActivity(intent)
}
}
}
private fun openInBrowser(linkDecoded: String, app: Activity) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(linkDecoded)
try {
app.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
}
}
fun String.isUrlValid(): Boolean =
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlInvalid(ctx: Context): Boolean {
fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull()
var existsAndEndsWithSlash = false
if (baseUrl != null) {
@@ -181,7 +65,7 @@ fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
startActivity(intent)
}
class LinkOnTouchListener: View.OnTouchListener {
class LinkOnTouchListener : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
var ret = false
val widget: TextView = v as TextView
@@ -191,7 +75,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,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,129 +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 android.text.TextUtils;
import android.util.Log;
import androidx.browser.customtabs.CustomTabsService;
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.helpers.KeepAliveService;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("ALL")
class CustomTabsHelper {
private static final String TAG = "CustomTabsHelper";
private static final String STABLE_PACKAGE = "com.android.chrome";
private static final String BETA_PACKAGE = "com.chrome.beta";
private static final String DEV_PACKAGE = "com.chrome.dev";
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse;
private CustomTabsHelper() {
}
public static void addKeepAliveExtra(Context context, Intent intent) {
Intent keepAliveIntent = new Intent().setClassName(
context.getPackageName(), KeepAliveService.class.getCanonicalName());
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
}
/**
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
* the one chosen by the user if there is one, otherwise makes a best effort to return a
* valid package name.
* <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,14 +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 android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
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)
}
@@ -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
@@ -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: Internal Server Error - Amine's git

500 Internal Server Error

Gitea Version: 1.28.0+dev-106-g5b078f72aa