Compare commits

...

102 Commits

Author SHA1 Message Date
ca4b424174 Fix login activity layout 2021-11-22 00:32:39 +01:00
721a15ec21 Upgrade minsdk to 21 (#378)
* Upgrade minSdk

* Add changelog entry about the upgrade

* Remove obsolete version checks
2021-11-15 21:17:48 +01:00
a2933ac763 Use java.date to parse dates (#377)
* Use java.date to parse dates

* Enable core library desugaring
2021-11-12 21:31:17 +01:00
e1efe9643c Remove unused icons (#374)
* Remove unused icons

* Add back ic_launcher_background
2021-10-29 11:53:38 +02:00
90242ae801 Migrate the preferences to androidx (#373)
* Switch to androidx for settings

* Implement all settings

* Added all pages to settings

* Add toolbar to settings

* Left align all preferences

* Migrate to androidx preference manager

* Migrate settings functions

* Implement dark theme in settings page

* Remove old settings files

* Remove unused resources

Co-authored-by: Amine Bou <510304+aminecmi@users.noreply.github.com>
2021-10-28 20:40:22 +02:00
0c88f33981 Correct text and icon colors in the side drawer (#372) 2021-10-28 20:17:08 +02:00
5e13a8f20f New Crowdin updates (#370)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (Galician)

* New translations strings.xml (Chinese Simplified)

Co-authored-by: Amine Bou <aminecmi@gmail.com>
2021-10-28 20:16:51 +02:00
ca4b7ada97 Upgrade dependencies (#371) 2021-10-27 16:47:50 +02:00
62a82b01b8 New Crowdin updates (#369)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

Co-authored-by: Amine Bou <aminecmi@gmail.com>
2021-10-26 21:03:03 +02:00
1994fa2f7d Add option to always show the bottom bar in the article reader (#368) 2021-10-26 20:16:38 +02:00
ae32cbfb6f Add missing imports (#367) 2021-10-26 12:54:57 +02:00
2dff3d9191 Use FLAG_IMMUTABLE as required by android 12 (#366) 2021-10-26 12:46:30 +02:00
c0ae0466c2 Remove the LikeButton library (#363)
* Add functions to add and remove favorites from SharedItems

* Remove the LikeButton dependency

* Use the theme colors for the icon tint
2021-10-22 20:06:39 +02:00
58b0574cf9 Use the drawer image loader to load images (#364) 2021-10-22 20:04:28 +02:00
5472c607cd Upgrade bottom navigation library (#362) 2021-10-15 20:15:30 +02:00
f95cb20408 Upgrade gradle to latest version (#361) 2021-10-15 20:15:17 +02:00
5640b7e56c Upgrade MaterialDrawer (#359)
* Upgrade material drawer library

* Add footer options to drawer

* Apply styles to drawer

* Added sources and tags to side drawer

* Hide bottom bar when the side drawer is opened

* Display hamburger icon to open the side drawer

* Add information about libraries to the side drawer

* Cleanup

* Implement reloading badges
2021-10-15 20:14:21 +02:00
fa697f1313 Re-enable resource shrinking (#351) 2021-10-07 21:01:27 +02:00
a12623f8e4 Upgrade dependencies (#352)
* Upgrade gradle to version 7.0.2

* Disable deprecated options

* Upgrade to latest SDK

* Upgrade kotlin

* Upgrade dependencies
2021-10-07 21:01:12 +02:00
abba04839a Support adding sources with api 2 (#353)
* Support adding sources with api 2

* Update changelog
2021-10-07 20:59:53 +02:00
2e38639910 Missing test variables configuration (#348)
* Add buildconfig variables for debug testing

* Allow marking articles as read from the article reader (#346)

* Add new items according to the selfoss id, to avoid duplicate items.

* Migrate setting articles as read from ArticleFragment to SharedItems

* Removed unused assertion

* Allow marking articles as read from the article reader

* Added contributors, because they deserve it !

Co-authored-by: Amine Bou <510304+aminecmi@users.noreply.github.com>
2021-10-06 13:23:52 +02:00
db78717eec Added contributors, because they deserve it ! 2021-10-05 20:43:08 +02:00
58a498868d Allow marking articles as read from the article reader (#346)
* Add new items according to the selfoss id, to avoid duplicate items.

* Migrate setting articles as read from ArticleFragment to SharedItems

* Removed unused assertion

* Allow marking articles as read from the article reader
2021-10-05 20:21:44 +02:00
46e723a238 Migration of Item management to SharedItems (#345)
* Refactor Item addition and deletion

* Metods to filter the items according to read and starred status

* Remove displayed items only if displaying unread items

* Remove unnecessary api calls on tab change and delegate item storage to SharedItems

* Store articles in SharedItems when they get fetched

* Add tag filtering

* Mark items as read

* Disable sorting function

* Add function to get the unread status of an element.

* Fetch items on pull gesture

* Move marking as read logic in SharedItems.

* Delegate item status to SharedItems

* Allow changing unread status of items

* Use full article position reference and not the relative one

* Delegate marking items as unread to SharedItems

* Delegate database addition of Items to SharedItems.

* Function to only provide connectivity information

* Better database management

* Sort items by date

* Provide information about item caching to SharedItems

* Add missing imports

* Update database after fetching articles

* Add missing variable

* Remove unused import

* Use coroutines to access database

* Use coroutines to simultaneously fetch articles.

* Update database after fetching articles.

* Don't block thread when accessing the database

* Prevent crash if connectivity is lost while fetching articles

* Show "Not connected" snackbar if there is no connection or connection is lost during download

* Use coroutines in the background sync

* Added function to get only new items

* Introduced function to filter articles

* Don't execute background sync if the option is disabled

* Improve item filtering

* Apply filters when they are selected on the UI

* Handle infinite scroll

* Incorrect parameters were passed

* Simplify tab selection logic

* Upgrade kotlin jvm to version 1.8

* On tab change fetch new items if the item list is not completely populated

* Remove redundant assignations.

* Fetch articles when changing tag, source or search if the list is not fully populated

* Fetch only the article in the tab selected

* Correct inconsistent position address

* Disable swiping articles only if favorites are selected

* Delegate badge count to SharedItems

* Clear the database when the app starts in order to avoid accumulation and inconsistencies

* Remove unused functions and variables

* Do not overwrite fetched items with old copies from the database

* Display "There's nothing here" only if there are no articles

* Adapt function to read all articles to the new changes

* Use IO Dispatcher for Database and Network computations

* Adapt Background sync to the usage of SharedItems

* Handle refresh gesture appropriately by refreshing the whole items list

* Remove unused imports
2021-09-25 13:45:51 +02:00
304b6c3761 New Crowdin updates (#344)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

Co-authored-by: Amine Bou <aminecmi@gmail.com>
2021-09-20 19:57:29 +02:00
33fb04956c Store items in a public object (#343)
* Added Object SharedItems to store all the articles in one class and allow sharing the data among activities

* Introduced functions to set articles as read globally

* Start migration of items into SharedItems
2021-09-12 21:12:45 +02:00
6e3381fb61 Refresh the articles if using Mark On Scroll. (#342) 2021-04-08 14:10:58 +02:00
56720659ee Date format (#340)
* Set date format according to api version.

* Check new date format in items.

* Global date formatter.

* Ensure api version has been fetched before checking.

* Move api check to MyApp.

* Store api version and select correct date formatter when offline.

* Check api in Home, allow null values.
2021-03-22 19:00:42 +01:00
626c9e2797 Migrate to View Binding. (#338) 2021-03-17 17:50:44 +01:00
05cd96afc0 New Crowdin updates (#333)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)
2021-03-14 09:10:25 +01:00
c8faa8984f Load settings when the home activity is created. (#336) 2021-03-14 09:09:12 +01:00
a025efbf3b Fetch articles only when the home activity is created. (#335) 2021-03-11 22:04:10 +01:00
e62e04e13b Added option to prevent updating sources and tags. (#332)
* Added option to prevent updating sources and tags.

* Delete removed sources and tags from database.
2021-03-06 13:03:45 +01:00
e6b5ea4e67 Download read and starred items (#331)
* Save read and starred articles during background sync

* Use getItems

* Cache images of read articles

* Remove unused function

* Refactor functions
2021-01-12 03:55:47 +01:00
c3148c6744 Sort articles by date when loading from db (#330) 2021-01-11 22:04:25 +01:00
193f538d29 Trying fastlane for f-droid repository. Fixes #321. 2021-01-10 21:40:17 +01:00
7f45db0473 Bakcground image caching issues should be fixed. 2021-01-10 21:20:33 +01:00
d89423b9ac Fix crash when loading image that doesn't exist (#329)
* Wait until the image is downloaded

* Use timeout
2021-01-10 21:14:23 +01:00
25fd869c01 Closes #322. 2021-01-10 12:13:19 +01:00
d1d956b77a Improve image caching (#327)
* Update to support rebase

* Prevent Glide from opening svg images
2021-01-10 05:08:40 +01:00
41c14362a8 Fixes #323. 2021-01-09 15:01:59 +01:00
6fa8c901fc Decode the title of sources containing special html characters (#326)
#325
2021-01-07 21:22:01 +01:00
db124ab9de Changelog. 2020-12-24 13:49:32 +01:00
953940690d New Crowdin updates (#317)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)
2020-12-23 09:34:21 +01:00
918661be2d Expand images on tap (#315)
* Detect click on images in WebView

* First stub of the fragment to show the image in full screen

* Scale image dimension to fit the display

* Hide toolbar from Image view

* Add back button to the Image view

* Open one image on tap

* Allow zooming on images

* Revert to using Toolbar for navigation.

* Remove vibration when opening the Image view

* Do not open links associated with images

* Send all images in the webpage to the Image fragment.

* Change image on swipe

* Store article images in cache in background.

* Use PhotoView in place of WebView to display images. Implemented a pager to swipe through images.

* Removed debugging logging.
2020-12-22 20:06:38 +01:00
7b8a5c9a56 Fixed migration issue. 2020-12-22 18:02:03 +01:00
2d5ab7bf0c Fix crash when a feed has no icon (#318) 2020-12-12 19:06:30 +01:00
9ba281befb Position of the elements in list view fixed in place. (#314)
Closes #287
2020-12-08 20:13:33 +01:00
00c8eed034 Fix back button closing settings from submenu (#313)
* Prevent back button from closing the activity.

* Remove redundant overrides

* Do not close the settings after resetting the themes.

Closes #290
2020-12-08 15:36:12 +01:00
a1e4f89cd1 Decode title of articles from html (#312)
* Decode the title of the articles from html.

* Remove unused imports.

Closes #300
2020-12-07 22:22:54 +01:00
36a43b3861 Allow the value of the thumbnail to be null. (#311)
Closes #310
2020-12-07 15:48:58 +01:00
aa6d470f40 Update README.md 2020-11-14 14:06:09 +01:00
0046a8a477 New Crowdin updates (#304)
* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)
2020-07-13 13:49:40 +02:00
43ff9d186a Update README.md 2020-07-13 11:49:57 +02:00
73dae304be Added screenshots. 2020-07-13 08:59:34 +02:00
66103a451b Update README.md 2020-04-29 10:38:05 +02:00
600c62316d Update README.md 2020-04-28 21:49:07 +02:00
d370ddc4d1 Fixed dark mode webview background. 2020-02-01 14:30:48 +01:00
4049f6a5c7 Removed acra because the issues wern't sent anyway. 2020-02-01 14:30:03 +01:00
3e96ac207e Created new LoggingInterceptor that doesn't log any errors related to… (#291)
* Created new LoggingInterceptor that doesn't log any errors related to SocketTimeoutException.

* Formatted with ktlint

* Intercept timeoutException to bypass the logging but still throwing an exception.
2019-10-12 19:36:55 +02:00
84f1ab12cf Finally fixed issue with crash on launch. 2019-05-17 20:28:21 +02:00
f48f6ed788 Trying to fix crashes. 2019-05-17 19:05:11 +02:00
e517803bd8 Fixed a bug with theme settings page. 2019-05-12 21:42:56 +02:00
3eaf390790 Background thing issue. 2019-05-12 21:38:10 +02:00
6de54d63e6 Updates. 2019-05-12 21:17:45 +02:00
dd7a2f476b Fixed an issue with compilation not working. 2019-05-12 21:07:05 +02:00
1485cc05f4 Fixing dark background webview. 2019-04-06 12:50:30 +02:00
d1dad3e61a Cleaning and update. 2019-04-06 12:42:25 +02:00
e5024b0420 Update README.md 2019-04-04 10:17:51 +02:00
9b01692c55 Update README.md 2019-03-16 19:19:29 +01:00
33aa587d36 Update index.html 2019-03-16 11:26:57 +01:00
12e0766803 No more playstore. 2019-03-16 09:06:06 +01:00
a8721ad7a4 Should intercept socket timeout errors. 2019-01-30 21:11:24 +01:00
bc5e882894 New translations strings.xml (Chinese Traditional) (#280) 2019-01-29 08:39:28 +01:00
e3460322b1 Closes #279. 2019-01-27 13:59:28 +01:00
7e3288a076 For now not sending issue report for #278. 2019-01-27 13:47:41 +01:00
ddc754ec25 Closes #270. 2019-01-15 19:54:46 +01:00
134a0766d6 Updates. 2019-01-14 20:56:12 +01:00
69da932ab5 Just to be sure. 2019-01-14 19:39:54 +01:00
592fb6328a Workaround for #275. 2019-01-14 19:37:43 +01:00
a0aead6491 Closes #271. 2019-01-13 15:59:38 +01:00
722b6cc06d Fixed issue with read/unread cont when swiping. 2019-01-13 15:59:38 +01:00
6d7c4b40f6 Wip fixing the badges reloading when reading all the items. 2019-01-13 15:59:38 +01:00
f538ed39fc New translations strings.xml (Chinese Traditional) (#274) 2019-01-11 14:20:35 +01:00
65821492ad New Crowdin translations (#273)
* New translations strings.xml (Catalan)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)

* New translations strings.xml (French)
2019-01-10 11:19:46 +01:00
6ede718a9f Closes #269. 2019-01-09 21:28:55 +01:00
f1757937a4 Disabled resources shrinking after #262. 2019-01-09 13:33:02 +01:00
2bd2e0a953 Vector assets (#262) 2019-01-09 10:43:49 +01:00
b5aef28af0 New Crowdin translations (#268)
* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)
2019-01-08 10:00:28 +01:00
45747a1506 New Crowdin translations (#267)
* New translations strings.xml (Catalan)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

* New translations strings.xml (French)
2019-01-06 18:46:38 +01:00
c6e2e08bcb Merge pull request #265 from aminecmi/feature/261
WIP: Webview font
2019-01-06 18:25:26 +01:00
25bf18661e Adde a bigger line-height. 2019-01-06 18:17:10 +01:00
6b088dcd24 Font family. 2019-01-06 18:12:18 +01:00
d2b18e1880 Removed lazy loaded fonts. 2019-01-06 16:26:40 +01:00
eec7c94e98 WIP. 2019-01-06 16:16:35 +01:00
d1f8fcacc0 Changelog. 2019-01-05 21:47:45 +01:00
07e4a33cbd Closes #266. 2019-01-05 21:43:09 +01:00
f6317f566e Same change for hidden tags. 2019-01-02 21:30:26 +01:00
9f51e4e6a5 Closes #264 2019-01-02 21:21:32 +01:00
750604a31f Languages cleaning. (#260) 2018-12-12 21:49:38 +01:00
392eee0ad4 New Crowdin translations (#258)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

* New translations strings.xml (Serbian (Cyrillic))

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-12-10 19:26:48 +01:00
37e7b987ee Changed color of alignment icons. 2018-12-10 19:25:18 +01:00
287 changed files with 3627 additions and 5765 deletions

View File

@ -1,5 +1,13 @@
**1.7.x** **1.7.x**
- Hiding tags with 0 articles
- Fixed issue with basic auth and images loading
- Added the ability to justify or left align the reader text
- Fixed #251
- Added experimental issue to set a default timeout. Should work for #238. - Added experimental issue to set a default timeout. Should work for #238.
- Closing #220. - Closing #220.
@ -18,6 +26,26 @@
- Closing #38. Only doing api calls on network available. - Closing #38. Only doing api calls on network available.
- Closing #298 and #287. Issues with Listview rendering
- Closing #290. Fixing back button issue in Settings
- Closing #300. Fixing issues when displaying some special characters.
- Closing #310. Some feeds don't have icons nor thumbnails.
- Closing #178. Expending images on tap.
- Closing #323. Old issue with textview not having the right color.
- Closing #324. Svg images loading crashes the app.
- Closing #322. App crashed because of svg images.
- Closing #236. New sources can be added in Selfoss 2.19.
- Dropped support for android 4, the last version supporting it is v1721030811
**1.6.x** **1.6.x**
- Handling hidden tags. - Handling hidden tags.
@ -38,6 +66,8 @@
- Fixes #215, #208. - Fixes #215, #208.
- Fixes #328.
**1.5.7.x** **1.5.7.x**
- Added confirmation to the mark as read and update menues. - Added confirmation to the mark as read and update menues.

View File

@ -1,20 +1,26 @@
# ReaderForSelfoss # ReaderForSelfoss **(Only available from F-Droid)**
[![Slack Channel](https://img.shields.io/badge/chat-slack-green.svg)](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ) [![Build Status](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/badge/icon)](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/) [![Code Triagers Badge](https://www.codetriage.com/aminecmi/readerforselfoss/badges/users.svg)](https://www.codetriage.com/aminecmi/readerforselfoss) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss)
It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/) It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/)
<a href='https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="100"/></a> <a href="https://f-droid.org/packages/apps.amine.bou.readerforselfoss"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a> **The project is not dead at all.**
Also, the last APK built from source is available [here](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/lastSuccessfulBuild/artifact/SignApksBuilder-out/selfoss-key/selfoss/app-githubConfig-release-unsigned.apk/app-githubConfig-release.apk). I still want to work on it, but for the last few months, I didn't have that much time to do so.
## Join the alpha channel If you are a developer, don't hesitate to help with PRs.
**Keep in mind, it could be instable, but you'll have the new updates faster** If you are a user, you can still create new issues. I'll fix them when I can.
- First, join the google [group](https://groups.google.com/d/forum/reader-for-selfoss-alpha-testing). <a href="https://f-droid.org/packages/apps.amine.bou.readerforselfoss"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
- Then, join the [alpha channel](https://play.google.com/apps/testing/apps.amine.bou.readerforselfoss) of the app.
- You'll be able to update the app for the current alpha version. ## Screen captures
<img src="res//fr-card.png?raw=true" alt="card view" width="400"/> <img src="res//fr-list.png?raw=true" alt="list view" width="400"/>
## Like my app ?
<a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
## Want to help ? ## Want to help ?
@ -30,4 +36,12 @@ Also, the last APK built from source is available [here](https://jenkins.amine-b
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1) - [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues) - [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss) - [Help translation the app](https://crowdin.com/project/readerforselfoss)
- [Ask for help](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ)
## Contributors (Alphabetical order) ❤️
- [@aancel](https://github.com/aancel)
- [@Binnette](https://github.com/Binnette)
- [@davidoskky](https://github.com/davidoskky)
- [@hectorgabucio](https://github.com/hectorgabucio)
- [@licaon-kter](https://github.com/licaon-kter)
- [@sergey-babkin](https://github.com/sergey-babkin)

View File

@ -24,25 +24,29 @@ def versionNameFromGit() {
return gitVersion() return gitVersion()
} }
apply plugin: 'kotlin-kapt'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt'
android { android {
compileOptions { compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
compileSdkVersion 28 compileSdkVersion 31
buildToolsVersion '28.0.3' buildToolsVersion '31.0.0'
buildFeatures {
viewBinding true
}
defaultConfig { defaultConfig {
applicationId "apps.amine.bou.readerforselfoss" applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 31
versionCode versionCodeFromGit() versionCode versionCodeFromGit()
versionName versionNameFromGit() versionName versionNameFromGit()
@ -73,8 +77,8 @@ android {
} }
debug { debug {
buildConfigField "String", "LOGIN_URL", appLoginUrl buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
} }
} }
flavorDimensions "build" flavorDimensions "build"
@ -83,99 +87,85 @@ android {
versionNameSuffix '-github' versionNameSuffix '-github'
dimension "build" dimension "build"
} }
storeConfig { }
// As jenkins publishes to alpha first, this is the default suffix now. kotlinOptions {
versionNameSuffix '-store' jvmTarget = '1.8'
dimension "build"
}
} }
} }
dependencies { dependencies {
implementation 'androidx.preference:preference-ktx:1.1.1'
// Testing // Testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
androidTestImplementation 'androidx.test:runner:1.1.0-beta02' androidTestImplementation 'androidx.test:runner:1.3.1-alpha02'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02'
// Espresso-intents for validation and stubbing of Intents // Espresso-intents for validation and stubbing of Intents
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-beta02' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02'
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support // Android Support
implementation "androidx.appcompat:appcompat:$android_version" implementation "androidx.appcompat:appcompat:1.4.0-beta01"
implementation "com.google.android.material:material:$android_version" implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation "androidx.recyclerview:recyclerview:$android_version" implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
implementation "androidx.legacy:legacy-support-v4:$android_version" implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation "androidx.vectordrawable:vectordrawable:$android_version" implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
implementation "androidx.browser:browser:$android_version" implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:$android_version" implementation "androidx.cardview:cardview:$android_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation "androidx.annotation:annotation:1.2.0"
implementation 'androidx.work:work-runtime-ktx:2.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'org.jsoup:jsoup:1.14.3'
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
//multidex //multidex
implementation 'androidx.multidex:multidex:2.0.0' implementation 'androidx.multidex:multidex:2.0.1'
// About // About
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') { implementation 'com.mikepenz:aboutlibraries-core:8.9.4'
transitive = true implementation 'com.mikepenz:aboutlibraries:8.9.4'
} implementation "com.mikepenz:aboutlibraries-definitions:8.9.4"
// Async
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
// Retrofit + http logging + okhttp // Retrofit + http logging + okhttp
implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.burgstaller:okhttp-digest:1.12' implementation 'com.burgstaller:okhttp-digest:2.5'
// Material-ish things // Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5' implementation 'com.ashokvarma.android:bottom-navigation-bar:2.2.0'
implementation 'com.github.jd-alexander:LikeButton:0.2.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
// glide // glide
implementation 'com.github.bumptech.glide:glide:4.1.1' kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1' implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
// Asking politely users to rate the app
implementation 'com.github.stkent:amplify:2.2.0'
// Drawer // Drawer
implementation 'co.zsmb:materialdrawer-kt:2.0.1' implementation 'com.mikepenz:materialdrawer:8.4.4'
// Themes // Themes
implementation 'com.52inc:scoops:1.0.0' implementation 'com.52inc:scoops:1.0.0'
implementation 'com.jaredrummler:colorpicker:1.0.2' implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1' implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
// Pager // Pager
implementation 'me.relex:circleindicator:2.0.0@aar' implementation 'me.relex:circleindicator:2.1.6'
implementation 'androidx.core:core-ktx:1.0.0' //PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
// Crash implementation 'androidx.core:core-ktx:1.7.0-rc01'
implementation 'ch.acra:acra-http:5.2.1'
implementation 'ch.acra:acra-dialog:5.2.1'
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:2.4.0-beta01"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:2.4.0-beta01"
implementation "android.arch.work:work-runtime-ktx:$work_version" implementation "android.arch.work:work-runtime-ktx:$work_version"
} }
afterEvaluate {
initAppLoginPropertiesIfNeeded()
}
def initAppLoginPropertiesIfNeeded() {
def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties')
if (!propertiesFile.exists()) {
def commentMessage = "This is autogenerated local property from system environment to prevent key to be committed to source control."
ant.propertyfile(file: System.getProperty("user.home") + "/.gradle/gradle.properties", comment: commentMessage) {
entry(key: "appLoginUrl", value: System.getProperty("appLoginUrl"))
entry(key: "appLoginUsername", value: System.getProperty("appLoginUsername"))
entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
}
}
}

View File

@ -0,0 +1,226 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "9cf8b03d32f80dfd58160599a1df197d",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnail",
"columnName": "thumbnail",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "link",
"columnName": "link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourcetitle",
"columnName": "sourcetitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "actions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "articleId",
"columnName": "articleid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "read",
"columnName": "read",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unstarred",
"columnName": "unstarred",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9cf8b03d32f80dfd58160599a1df197d\")"
]
}
}

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apps.amine.bou.readerforselfoss" package="apps.amine.bou.readerforselfoss">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -16,14 +15,15 @@
android:theme="@style/NoBar"> android:theme="@style/NoBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts" <meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
@ -38,7 +38,7 @@
android:parentActivityName=".HomeActivity"> android:parentActivityName=".HomeActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="apps.amine.bou.readerforselfoss.HomeActivity" /> android:value=".HomeActivity" />
</activity> </activity>
<activity <activity
android:name=".SourcesActivity" android:name=".SourcesActivity"
@ -49,22 +49,24 @@
</activity> </activity>
<activity <activity
android:name=".AddSourceActivity" android:name=".AddSourceActivity"
android:parentActivityName=".SourcesActivity"> android:parentActivityName=".SourcesActivity"
android:exported="true">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" /> android:value=".SourcesActivity" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ReaderActivity"> android:name=".ReaderActivity">
</activity> </activity>
<activity
android:name=".ImageActivity">
</activity>
<meta-data <meta-data
android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule" android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
@ -77,6 +79,9 @@
android:value="true" /> android:value="true" />
<meta-data android:name="android.max_aspect" android:value="2.1" /> <meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application> </application>
</manifest> </manifest>

View File

@ -1,9 +1,9 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.view.View import android.view.View
@ -22,11 +22,11 @@ import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_add_source.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import android.graphics.PorterDuff import android.graphics.PorterDuff
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
@ -36,72 +36,62 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var api: SelfossApi private lateinit var api: SelfossApi
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity) appColors = AppColors(this@AddSourceActivity)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_add_source) setContentView(view)
val scoop = Scoop.getInstance() val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolbar) scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
val drawable = nameInput.background val drawable = binding.nameInput.background
drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
// TODO: clean // TODO: clean
if(Build.VERSION.SDK_INT > 16) { binding.nameInput.background = drawable
nameInput.background = drawable
} else{
nameInput.setBackgroundDrawable(drawable)
}
val drawable1 = sourceUri.background val drawable1 = binding.sourceUri.background
drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) { binding.sourceUri.background = drawable1
sourceUri.background = drawable1
} else{
sourceUri.setBackgroundDrawable(drawable1)
}
val drawable2 = tags.background val drawable2 = binding.tags.background
drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP) drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) { binding.tags.background = drawable2
tags.background = drawable2
} else{
tags.setBackgroundDrawable(drawable2)
}
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
try { try {
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
api = SelfossApi( api = SelfossApi(
this, this,
this@AddSourceActivity, this@AddSourceActivity,
prefs.getBoolean("isSelfSignedCert", false), settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(), prefs.getString("api_timeout", "-1")!!.toLong()
prefs.getBoolean("should_log_everything", false)
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
mustLoginToAddSource() mustLoginToAddSource()
} }
maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput) maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
saveBtn.setTextColor(appColors.colorAccent) binding.saveBtn.setTextColor(appColors.colorAccent)
saveBtn.setOnClickListener { binding.saveBtn.setOnClickListener {
handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!) handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api)
} }
} }
@ -109,10 +99,10 @@ class AddSourceActivity : AppCompatActivity() {
super.onResume() super.onResume()
val config = Config(this) val config = Config(this)
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false, this@AddSourceActivity)) { if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource() mustLoginToAddSource()
} else { } else {
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer) handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer)
} }
} }
@ -201,42 +191,78 @@ class AddSourceActivity : AppCompatActivity() {
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) { private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
val sourceDetailsAvailable = val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
if (sourceDetailsAvailable) { when {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() sourceDetailsUnavailable -> {
} else { Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
api.createSource( }
title, PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
url, val tagList = tags.text.toString().split(",").map { it.trim() }
mSpoutsValue!!, api.createSourceApi2(
tags.text.toString(), title,
"" url,
).enqueue(object : Callback<SuccessResponse> { mSpoutsValue!!,
override fun onResponse( tagList,
call: Call<SuccessResponse>, ""
response: Response<SuccessResponse> ).enqueue(object : Callback<SuccessResponse> {
) { override fun onResponse(
if (response.body() != null && response.body()!!.isSuccess) { call: Call<SuccessResponse>,
finish() response: Response<SuccessResponse>
} else { ) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} })
}
else -> {
api.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
""
).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) })
}
} }
} }
} }

View File

@ -0,0 +1,56 @@
package apps.amine.bou.readerforselfoss
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.fragments.ImageFragment
class ImageActivity : AppCompatActivity() {
private lateinit var allImages : ArrayList<String>
private var position : Int = 0
private lateinit var binding: ActivityImageBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityImageBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
position = intent.getIntExtra("position", 0)
binding.pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager)
binding.pager.currentItem = position
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getCount(): Int {
return allImages.size
}
override fun getItem(position: Int): ImageFragment {
return ImageFragment.newInstance(allImages[position])
}
}
}

View File

@ -14,18 +14,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityLoginBinding
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.activity_login.*
import org.acra.ACRA
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -40,27 +36,28 @@ class LoginActivity : AppCompatActivity() {
private lateinit var settings: SharedPreferences private lateinit var settings: SharedPreferences
private lateinit var editor: SharedPreferences.Editor private lateinit var editor: SharedPreferences.Editor
private lateinit var userIdentifier: String private lateinit var userIdentifier: String
private var logErrors: Boolean = false
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@LoginActivity) appColors = AppColors(this@LoginActivity)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_login) setContentView(view)
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
handleBaseUrlFail() handleBaseUrlFail()
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
userIdentifier = settings.getString("unique_id", "") userIdentifier = settings.getString("unique_id", "")!!
logErrors = settings.getBoolean("login_debug", false)
editor = settings.edit() editor = settings.edit()
if (settings.getString("url", "").isNotEmpty()) { if (settings.getString("url", "")!!.isNotEmpty()) {
goToMain() goToMain()
} }
@ -69,14 +66,14 @@ class LoginActivity : AppCompatActivity() {
private fun handleActions() { private fun handleActions() {
withSelfhostedCert.setOnCheckedChangeListener { _, b -> binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
isWithSelfSignedCert = !isWithSelfSignedCert isWithSelfSignedCert = !isWithSelfSignedCert
val visi: Int = if (b) View.VISIBLE else View.GONE val visi: Int = if (b) View.VISIBLE else View.GONE
warningText.visibility = visi binding.warningText.visibility = visi
} }
passwordView.setOnEditorActionListener( binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ -> TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) { if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
attemptLogin() attemptLogin()
@ -86,22 +83,22 @@ class LoginActivity : AppCompatActivity() {
} }
) )
signInButton.setOnClickListener { attemptLogin() } binding.signInButton.setOnClickListener { attemptLogin() }
withLogin.setOnCheckedChangeListener { _, b -> binding.withLogin.setOnCheckedChangeListener { _, b ->
isWithLogin = !isWithLogin isWithLogin = !isWithLogin
val visi: Int = if (b) View.VISIBLE else View.GONE val visi: Int = if (b) View.VISIBLE else View.GONE
loginLayout.visibility = visi binding.loginView.visibility = visi
passwordLayout.visibility = visi binding.passwordView.visibility = visi
} }
withHttpLogin.setOnCheckedChangeListener { _, b -> binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
isWithHTTPLogin = !isWithHTTPLogin isWithHTTPLogin = !isWithHTTPLogin
val visi: Int = if (b) View.VISIBLE else View.GONE val visi: Int = if (b) View.VISIBLE else View.GONE
httpLoginInput.visibility = visi binding.httpLoginView.visibility = visi
httpPasswordInput.visibility = visi binding.httpPasswordView.visibility = visi
} }
} }
@ -112,9 +109,8 @@ class LoginActivity : AppCompatActivity() {
alertDialog.setMessage(getString(R.string.base_url_error)) alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
{ dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
)
alertDialog.show() alertDialog.show()
} }
} }
@ -128,25 +124,25 @@ class LoginActivity : AppCompatActivity() {
private fun attemptLogin() { private fun attemptLogin() {
// Reset errors. // Reset errors.
urlView.error = null binding.urlView.error = null
loginView.error = null binding.loginView.error = null
httpLoginView.error = null binding.httpLoginView.error = null
passwordView.error = null binding.passwordView.error = null
httpPasswordView.error = null binding.httpPasswordView.error = null
// Store values at the time of the login attempt. // Store values at the time of the login attempt.
val url = urlView.text.toString() val url = binding.urlView.text.toString()
val login = loginView.text.toString() val login = binding.loginView.text.toString()
val httpLogin = httpLoginView.text.toString() val httpLogin = binding.httpLoginView.text.toString()
val password = passwordView.text.toString() val password = binding.passwordView.text.toString()
val httpPassword = httpPasswordView.text.toString() val httpPassword = binding.httpPasswordView.text.toString()
var cancel = false var cancel = false
var focusView: View? = null var focusView: View? = null
if (!url.isBaseUrlValid(logErrors, this@LoginActivity)) { if (!url.isBaseUrlValid(this@LoginActivity)) {
urlView.error = getString(R.string.login_url_problem) binding.urlView.error = getString(R.string.login_url_problem)
focusView = urlView focusView = binding.urlView
cancel = true cancel = true
inValidCount++ inValidCount++
if (inValidCount == 3) { if (inValidCount == 3) {
@ -155,9 +151,8 @@ class LoginActivity : AppCompatActivity() {
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
{ dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
)
alertDialog.show() alertDialog.show()
inValidCount = 0 inValidCount = 0
} }
@ -165,28 +160,28 @@ class LoginActivity : AppCompatActivity() {
if (isWithLogin) { if (isWithLogin) {
if (TextUtils.isEmpty(password)) { if (TextUtils.isEmpty(password)) {
passwordView.error = getString(R.string.error_invalid_password) binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = passwordView focusView = binding.passwordView
cancel = true cancel = true
} }
if (TextUtils.isEmpty(login)) { if (TextUtils.isEmpty(login)) {
loginView.error = getString(R.string.error_field_required) binding.loginView.error = getString(R.string.error_field_required)
focusView = loginView focusView = binding.loginView
cancel = true cancel = true
} }
} }
if (isWithHTTPLogin) { if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) { if (TextUtils.isEmpty(httpPassword)) {
httpPasswordView.error = getString(R.string.error_invalid_password) binding.httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = httpPasswordView focusView = binding.httpPasswordView
cancel = true cancel = true
} }
if (TextUtils.isEmpty(httpLogin)) { if (TextUtils.isEmpty(httpLogin)) {
httpLoginView.error = getString(R.string.error_field_required) binding.httpLoginView.error = getString(R.string.error_field_required)
focusView = httpLoginView focusView = binding.httpLoginView
cancel = true cancel = true
} }
} }
@ -208,8 +203,7 @@ class LoginActivity : AppCompatActivity() {
this, this,
this@LoginActivity, this@LoginActivity,
isWithSelfSignedCert, isWithSelfSignedCert,
-1L, -1L
isWithSelfSignedCert
) )
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) { if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
@ -221,19 +215,11 @@ class LoginActivity : AppCompatActivity() {
editor.remove("password") editor.remove("password")
editor.remove("httpPassword") editor.remove("httpPassword")
editor.apply() editor.apply()
urlView.error = getString(R.string.wrong_infos) binding.urlView.error = getString(R.string.wrong_infos)
loginView.error = getString(R.string.wrong_infos) binding.loginView.error = getString(R.string.wrong_infos)
passwordView.error = getString(R.string.wrong_infos) binding.passwordView.error = getString(R.string.wrong_infos)
httpLoginView.error = getString(R.string.wrong_infos) binding.httpLoginView.error = getString(R.string.wrong_infos)
httpPasswordView.error = getString(R.string.wrong_infos) binding.httpPasswordView.error = getString(R.string.wrong_infos)
if (logErrors) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity)
Toast.makeText(
this@LoginActivity,
t.message,
Toast.LENGTH_LONG
).show()
}
showProgress(false) showProgress(false)
} }
@ -261,28 +247,28 @@ class LoginActivity : AppCompatActivity() {
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime) val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
loginForm.visibility = if (show) View.GONE else View.VISIBLE binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
loginForm binding.loginForm
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 0F else 1F if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() { ).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
loginForm.visibility = if (show) View.GONE else View.VISIBLE binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
} }
} }
) )
loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
loginProgress binding.loginProgress
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 1F else 0F if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() { ).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
} }
} }
) )
@ -290,29 +276,19 @@ class LoginActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.login_menu, menu) menuInflater.inflate(R.menu.login_menu, menu)
menu.findItem(R.id.login_debug).isChecked = logErrors
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { return when (item.itemId) {
R.id.about -> { R.id.about -> {
LibsBuilder() LibsBuilder()
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.start(this) .start(this)
return true true
} }
R.id.login_debug -> { else -> super.onOptionsItemSelected(item)
val newState = !item.isChecked
item.isChecked = newState
logErrors = newState
editor.putBoolean("login_debug", newState)
editor.apply()
return true
}
else -> return super.onOptionsItemSelected(item)
} }
} }
} }

View File

@ -2,14 +2,18 @@ package apps.amine.bou.readerforselfoss
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import apps.amine.bou.readerforselfoss.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val intent = Intent(this, LoginActivity::class.java) val intent = Intent(this, LoginActivity::class.java)

View File

@ -6,50 +6,27 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.multidex.MultiDexApplication
import android.widget.ImageView import android.widget.ImageView
import androidx.multidex.MultiDexApplication
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.github.stkent.amplify.feedback.DefaultEmailFeedbackCollector
import com.github.stkent.amplify.feedback.GooglePlayStoreFeedbackCollector
import com.github.stkent.amplify.tracking.Amplify
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader
import org.acra.ACRA
import org.acra.ReportField
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraDialog
import org.acra.annotation.AcraHttpSender
import org.acra.sender.HttpSender
import java.io.IOException
import java.util.UUID.randomUUID import java.util.UUID.randomUUID
@AcraHttpSender(uri = "http://37.187.110.167/amine/acra/simplest-acra.php",
httpMethod = HttpSender.Method.POST)
@AcraDialog(resText = R.string.crash_dialog_text,
resCommentPrompt = R.string.crash_dialog_comment,
resTheme = android.R.style.Theme_DeviceDefault_Dialog)
@AcraCore(reportContent = [ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
buildConfigClass = BuildConfig::class)
class MyApp : MultiDexApplication() { class MyApp : MultiDexApplication() {
private lateinit var config: Config
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
config = Config(baseContext)
initAmplify()
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "").isEmpty()) { if (prefs.getString("unique_id", "")!!.isEmpty()) {
val editor = prefs.edit() val editor = prefs.edit()
editor.putString("unique_id", randomUUID().toString()) editor.putString("unique_id", randomUUID().toString())
editor.apply() editor.apply()
@ -81,40 +58,20 @@ class MyApp : MultiDexApplication() {
} }
} }
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
ACRA.init(this)
ACRA.getErrorReporter().putCustomData("unique_id", prefs.getString("unique_id", ""))
}
private fun initAmplify() {
Amplify.initSharedInstance(this)
.setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
.setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
.applyAllDefaultRules()
}
private fun initDrawerImageLoader() { private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set( override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
imageView: ImageView?,
uri: Uri?,
placeholder: Drawable?,
tag: String?
) {
Glide.with(imageView?.context) Glide.with(imageView?.context)
.load(uri) .loadMaybeBasicAuth(config, uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder)) .apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView) .into(imageView)
} }
override fun cancel(imageView: ImageView?) { override fun cancel(imageView: ImageView) {
Glide.with(imageView?.context).clear(imageView) Glide.with(imageView?.context).clear(imageView)
} }
override fun placeholder(ctx: Context?, tag: String?): Drawable { override fun placeholder(ctx: Context, tag: String?): Drawable {
return baseContext.resources.getDrawable(R.mipmap.ic_launcher) return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
} }
}) })

View File

@ -1,10 +1,14 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -13,38 +17,27 @@ import androidx.appcompat.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.room.Room import androidx.room.Room
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_reader.*
import me.relex.circleindicator.CircleIndicator import me.relex.circleindicator.CircleIndicator
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
private var markOnScroll: Boolean = false private var markOnScroll: Boolean = false
private var debugReadingItems: Boolean = false
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var userIdentifier: String private lateinit var userIdentifier: String
@ -54,14 +47,18 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var db: AppDatabase private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1 private var activeAlignment: Int = 1
val JUSTIFY = 1 val JUSTIFY = 1
val ALIGN_LEFT = 2 val ALIGN_LEFT = 2
private fun showMenuItem(willAddToFavorite: Boolean) { private fun showMenuItem(willAddToFavorite: Boolean) {
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite if (willAddToFavorite) {
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
} else {
toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)
}
} }
private fun canFavorite() { private fun canFavorite() {
@ -76,38 +73,39 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_reader) setContentView(view)
db = Room.databaseBuilder( db = Room.databaseBuilder(
applicationContext, applicationContext,
AppDatabase::class.java, "selfoss-database" AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build() ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance() val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar) scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(toolBar) setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
prefs = PreferenceManager.getDefaultSharedPreferences(this) prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit() editor = prefs.edit()
debugReadingItems = prefs.getBoolean("read_debug", false) userIdentifier = prefs.getString("unique_id", "")!!
userIdentifier = prefs.getString("unique_id", "")
markOnScroll = prefs.getBoolean("mark_on_scroll", false) markOnScroll = prefs.getBoolean("mark_on_scroll", false)
activeAlignment = prefs.getInt("text_align", JUSTIFY) activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi( api = SelfossApi(
this, this,
this@ReaderActivity, this@ReaderActivity,
prefs.getBoolean("isSelfSignedCert", false), settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(), prefs.getString("api_timeout", "-1")!!.toLong()
prefs.getBoolean("should_log_everything", false)
) )
if (allItems.isEmpty()) { if (allItems.isEmpty()) {
@ -118,9 +116,9 @@ class ReaderActivity : AppCompatActivity() {
readItem(allItems[currentItem]) readItem(allItems[currentItem])
pager.adapter = binding.pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity)) ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem binding.pager.currentItem = currentItem
} }
override fun onResume() { override fun onResume() {
@ -128,10 +126,10 @@ class ReaderActivity : AppCompatActivity() {
notifyAdapter() notifyAdapter()
pager.setPageTransformer(true, DepthPageTransformer()) binding.pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager) (binding.indicator as CircleIndicator).setViewPager(binding.pager)
pager.addOnPageChangeListener( binding.pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() { object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
@ -141,74 +139,32 @@ class ReaderActivity : AppCompatActivity() {
} else { } else {
canFavorite() canFavorite()
} }
readItem(allItems[pager.currentItem]) readItem(allItems[position])
} }
} }
) )
} }
fun readItem(item: Item) { private fun readItem(item: Item) {
if (markOnScroll) { if (markOnScroll) {
thread { SharedItems.readItem(applicationContext, api, db, item)
db.itemsDao().delete(item.toEntity())
} }
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
api.markItem(item.id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter()
.maybeHandleSilentException(Exception(message), this@ReaderActivity)
}
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
thread {
db.itemsDao().insertAllItems(item.toEntity())
}
if (debugReadingItems) {
ACRA.getErrorReporter()
.maybeHandleSilentException(t, this@ReaderActivity)
}
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
}
}
}
} }
private fun notifyAdapter() { private fun notifyAdapter() {
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged() (binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (markOnScroll) { if (markOnScroll) {
pager.clearOnPageChangeListeners() binding.pager.clearOnPageChangeListeners()
} }
} }
override fun onSaveInstanceState(oldInstanceState: Bundle?) { override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState) super.onSaveInstanceState(oldInstanceState)
oldInstanceState!!.clear() oldInstanceState.clear()
} }
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) : private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
@ -260,14 +216,14 @@ class ReaderActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() { fun afterSave() {
allItems[pager.currentItem] = allItems[binding.pager.currentItem] =
allItems[pager.currentItem].toggleStar() allItems[binding.pager.currentItem].toggleStar()
notifyAdapter() notifyAdapter()
canRemoveFromFavorite() canRemoveFromFavorite()
} }
fun afterUnsave() { fun afterUnsave() {
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar() allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
notifyAdapter() notifyAdapter()
canFavorite() canFavorite()
} }
@ -277,62 +233,23 @@ class ReaderActivity : AppCompatActivity() {
onBackPressed() onBackPressed()
return true return true
} }
R.id.save -> { R.id.star -> {
if (this@ReaderActivity.isNetworkAccessible(null)) { if (allItems[binding.pager.currentItem].starred) {
api.starrItem(allItems[pager.currentItem].id) SharedItems.unstarItem(
.enqueue(object : Callback<SuccessResponse> { this@ReaderActivity,
override fun onResponse( api,
call: Call<SuccessResponse>, db,
response: Response<SuccessResponse> allItems[binding.pager.currentItem]
) { )
afterSave() afterUnsave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else { } else {
thread { SharedItems.starItem(
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false)) this@ReaderActivity,
afterSave() api,
} db,
} allItems[binding.pager.currentItem]
} )
R.id.unsave -> { afterSave()
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.unstarrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
afterUnsave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
afterUnsave()
}
} }
} }
R.id.align_left -> { R.id.align_left -> {

View File

@ -1,21 +1,22 @@
package apps.amine.bou.readerforselfoss package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.Toast import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -23,50 +24,52 @@ import retrofit2.Response
class SourcesActivity : AppCompatActivity() { class SourcesActivity : AppCompatActivity() {
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity) appColors = AppColors(this@SourcesActivity)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySourcesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_sources) setContentView(view)
val scoop = Scoop.getInstance() val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolbar) scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
fab.rippleColor = appColors.colorAccentDark binding.fab.rippleColor = appColors.colorAccentDark
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
recyclerView.clearOnScrollListeners() binding.recyclerView.clearOnScrollListeners()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val mLayoutManager = LinearLayoutManager(this) val mLayoutManager = LinearLayoutManager(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val api = SelfossApi( val api = SelfossApi(
this, this,
this@SourcesActivity, this@SourcesActivity,
prefs.getBoolean("isSelfSignedCert", false), settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(), prefs.getString("api_timeout", "-1")!!.toLong()
prefs.getBoolean("should_log_everything", false)
) )
var items: ArrayList<Source> = ArrayList() var items: ArrayList<Source> = ArrayList()
recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) { if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
api.sources.enqueue(object : Callback<List<Source>> { api.sources.enqueue(object : Callback<List<Source>> {
@ -78,7 +81,7 @@ class SourcesActivity : AppCompatActivity() {
items = response.body() as ArrayList<Source> items = response.body() as ArrayList<Source>
} }
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api) val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
if (items.isEmpty()) { if (items.isEmpty()) {
Toast.makeText( Toast.makeText(
@ -99,7 +102,7 @@ class SourcesActivity : AppCompatActivity() {
}) })
} }
fab.setOnClickListener { binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
} }
} }

View File

@ -2,27 +2,26 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.Html
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView.ScaleType import android.widget.ImageView.ScaleType
import android.widget.Toast import androidx.core.content.ContextCompat
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink import apps.amine.bou.readerforselfoss.utils.shareLink
@ -31,13 +30,6 @@ import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.card_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
@ -49,8 +41,8 @@ class ItemCardAdapter(
private val articleViewer: Boolean, private val articleViewer: Boolean,
private val fullHeightCards: Boolean, private val fullHeightCards: Boolean,
override val appColors: AppColors, override val appColors: AppColors,
override val debugReadingItems: Boolean,
override val userIdentifier: String, override val userIdentifier: String,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
@ -59,144 +51,108 @@ class ItemCardAdapter(
c.resources.getDimension(R.dimen.card_image_max_height).toInt() c.resources.getDimension(R.dimen.card_image_max_height).toInt()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(v) return ViewHolder(binding)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position] with(holder) {
val itm = items[position]
binding.favButton.isSelected = itm.starred
binding.title.text = itm.getTitleDecoded()
binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.favButton.isLiked = itm.starred binding.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent) binding.title.setLinkTextColor(appColors.colorAccent)
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAndDateText()
if (!fullHeightCards) { binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
holder.mView.itemImage.maxHeight = imageMaxHeight c,
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP appColors.textColor
))
if (!fullHeightCards) {
binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP
}
if (itm.getThumbnail(c).isEmpty()) {
binding.itemImage.visibility = View.GONE
Glide.with(c).clear(binding.itemImage)
binding.itemImage.setImageDrawable(null)
} else {
binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
}
} }
if (itm.getThumbnail(c).isEmpty()) {
holder.mView.itemImage.visibility = View.GONE
Glide.with(c).clear(holder.mView.itemImage)
holder.mView.itemImage.setImageDrawable(null)
} else {
holder.mView.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.sourcetitle)
val drawable =
TextDrawable
.builder()
.round()
.build(itm.sourcetitle.toTextDrawableString(c), color)
holder.mView.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
}
holder.mView.favButton.isLiked = itm.starred
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return items.size return items.size
} }
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) { inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init { init {
mView.setCardBackgroundColor(appColors.cardBackgroundColor) binding.root.setCardBackgroundColor(appColors.cardBackgroundColor)
handleClickListeners() handleClickListeners()
handleCustomTabActions() handleCustomTabActions()
} }
private fun handleClickListeners() { private fun handleClickListeners() {
mView.favButton.setOnLikeListener(object : OnLikeListener { binding.favButton.setOnClickListener {
override fun liked(likeButton: LikeButton) { val item = items[bindingAdapterPosition]
val (id) = items[adapterPosition] if (isNetworkAvailable(c)) {
if (c.isNetworkAccessible(null)) { if (item.starred) {
api.starrItem(id).enqueue(object : Callback<SuccessResponse> { SharedItems.unstarItem(c, api, db, item)
override fun onResponse( item.starred = false
call: Call<SuccessResponse>, binding.favButton.isSelected = false
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else { } else {
thread { SharedItems.starItem(c, api, db, item)
db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false)) item.starred = true
} binding.favButton.isSelected = true
} }
} }
}
override fun unLiked(likeButton: LikeButton) { binding.shareBtn.setOnClickListener {
val (id) = items[adapterPosition] val item = items[bindingAdapterPosition]
if (c.isNetworkAccessible(null)) { c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
}
}
} }
})
mView.shareBtn.setOnClickListener { binding.browserBtn.setOnClickListener {
val item = items[adapterPosition] c.openInBrowserAsNewTask(items[bindingAdapterPosition])
c.shareLink(item.getLinkDecoded(), item.title) }
} }
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
}
}
private fun handleCustomTabActions() { private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent() val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app) helper.bindCustomTabsService(app)
mView.setOnClickListener { binding.root.setOnClickListener {
c.openItemUrl( c.openItemUrl(
items, items,
adapterPosition, bindingAdapterPosition,
items[adapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,

View File

@ -2,43 +2,26 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.Html
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import androidx.core.content.ContextCompat
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.list_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class ItemListAdapter( class ItemListAdapter(
@ -49,75 +32,66 @@ class ItemListAdapter(
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
private val articleViewer: Boolean, private val articleViewer: Boolean,
override val debugReadingItems: Boolean,
override val userIdentifier: String, override val userIdentifier: String,
override val appColors: AppColors, override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext private val c: Context = app.baseContext
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate( val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
R.layout.list_item, return ViewHolder(binding)
parent,
false
) as ConstraintLayout
return ViewHolder(v)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position] with(holder) {
val itm = items[position]
holder.mView.title.text = Html.fromHtml(itm.title) binding.title.text = itm.getTitleDecoded()
holder.mView.title.setOnTouchListener(LinkOnTouchListener()) binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.title.setLinkTextColor(appColors.colorAccent) binding.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText() binding.title.setLinkTextColor(appColors.colorAccent)
if (itm.getThumbnail(c).isEmpty()) { binding.sourceTitleAndDate.text = itm.sourceAndDateText()
val sizeInInt = 46
val sizeInDp = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
.displayMetrics
).toInt()
val marginInInt = 16 binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
val marginInDp = TypedValue.applyDimension( c,
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources appColors.textColor
.displayMetrics ))
).toInt()
val params = holder.mView.itemImage.layoutParams as ViewGroup.MarginLayoutParams if (itm.getThumbnail(c).isEmpty()) {
params.height = sizeInDp
params.width = sizeInDp
params.setMargins(marginInDp, 0, 0, 0)
holder.mView.itemImage.layoutParams = params
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.sourcetitle) val color = generator.getColor(itm.getSourceTitle())
val drawable = val drawable =
TextDrawable TextDrawable
.builder() .builder()
.round() .round()
.build(itm.sourcetitle.toTextDrawableString(c), color) .build(itm.getSourceTitle().toTextDrawableString(c), color)
holder.mView.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
}
} else { } else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage) c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
} }
} else {
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
} }
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init { init {
handleCustomTabActions() handleCustomTabActions()
@ -127,11 +101,11 @@ class ItemListAdapter(
val customTabsIntent = c.buildCustomTabsIntent() val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app) helper.bindCustomTabsService(app)
mView.setOnClickListener { binding.root.setOnClickListener {
c.openItemUrl( c.openItemUrl(
items, items,
adapterPosition, bindingAdapterPosition,
items[adapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,

View File

@ -2,44 +2,34 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import androidx.recyclerview.widget.RecyclerView
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import com.google.android.material.snackbar.Snackbar
import apps.amine.bou.readerforselfoss.utils.succeeded
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item> abstract var items: ArrayList<Item>
abstract val api: SelfossApi abstract val api: SelfossApi
abstract val db: AppDatabase abstract val db: AppDatabase
abstract val debugReadingItems: Boolean
abstract val userIdentifier: String abstract val userIdentifier: String
abstract val app: Activity abstract val app: Activity
abstract val appColors: AppColors abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) { fun updateAllItems() {
items = newItems items = SharedItems.focusedItems
notifyDataSetChanged() notifyDataSetChanged()
updateItems(items) updateItems(items)
} }
private fun doUnmark(i: Item, position: Int) { private fun unmarkSnackbar(i: Item, position: Int) {
val s = Snackbar val s = Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
@ -47,35 +37,11 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) SharedItems.unreadItem(app, api, db, i)
thread { if (SharedItems.displayedItems == "unread") {
db.itemsDao().insertAllItems(i.toEntity()) addItemAtIndex(i, position)
}
notifyItemInserted(position)
updateItems(items)
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().deleteReadActionForArticle(i.id)
}
} }
} }
@ -85,65 +51,57 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
fun removeItemAtIndex(position: Int) { private fun markSnackbar(position: Int) {
val i = items[position] val s = Snackbar
items.remove(i) .make(
notifyItemRemoved(position) app.findViewById(R.id.coordLayout),
updateItems(items) R.string.marked_as_unread,
Snackbar.LENGTH_LONG
thread { )
db.itemsDao().delete(i.toEntity()) .setAction(R.string.undo_string) {
} SharedItems.readItem(app, api, db, items[position])
items = SharedItems.focusedItems
if (app.isNetworkAccessible(null)) { if (SharedItems.displayedItems == "unread") {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> { notifyItemRemoved(position)
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
}
doUnmark(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items) updateItems(items)
} else {
thread { notifyItemChanged(position)
db.itemsDao().insertAllItems(i.toEntity())
}
} }
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
doUnmark(i, position)
} }
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
}
fun handleItemAtIndex(position: Int) {
if (SharedItems.unreadItemStatusAtIndex(position)) {
readItemAtIndex(position)
} else {
unreadItemAtIndex(position)
} }
} }
private fun readItemAtIndex(position: Int) {
val i = items[position]
SharedItems.readItem(app, api, db, i)
if (SharedItems.displayedItems == "unread") {
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
} else {
notifyItemChanged(position)
}
unmarkSnackbar(i, position)
}
private fun unreadItemAtIndex(position: Int) {
SharedItems.unreadItem(app, api, db, items[position])
notifyItemChanged(position)
markSnackbar(position)
}
fun addItemAtIndex(item: Item, position: Int) { fun addItemAtIndex(item: Item, position: Int) {
items.add(position, item) items.add(position, item)
notifyItemInserted(position) notifyItemInserted(position)

View File

@ -12,12 +12,13 @@ import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.SourceListItemBinding
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlinx.android.synthetic.main.source_list_item.view.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -29,33 +30,32 @@ class SourcesListAdapter(
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
private lateinit var binding: SourceListItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate( binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
R.layout.source_list_item, return ViewHolder(binding.root)
parent,
false
) as ConstraintLayout
return ViewHolder(v)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position] val itm = items[position]
config = Config(c)
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.title) val color = generator.getColor(itm.getTitleDecoded())
val drawable = val drawable =
TextDrawable TextDrawable
.builder() .builder()
.round() .round()
.build(itm.title.toTextDrawableString(c), color) .build(itm.getTitleDecoded().toTextDrawableString(c), color)
holder.mView.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage) c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
} }
holder.mView.sourceTitle.text = itm.title binding.sourceTitle.text = itm.getTitleDecoded()
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size

View File

@ -7,17 +7,13 @@ import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
class MercuryApi(shouldLog: Boolean) { class MercuryApi() {
private val service: MercuryService private val service: MercuryService
init { init {
val interceptor = HttpLoggingInterceptor() val interceptor = HttpLoggingInterceptor()
interceptor.level = if (shouldLog) { interceptor.level = HttpLoggingInterceptor.Level.NONE
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
val client = OkHttpClient.Builder().addInterceptor(interceptor).build() val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
val gson = GsonBuilder() val gson = GsonBuilder()

View File

@ -28,17 +28,17 @@ class ParsedContent(
} }
constructor(source: Parcel) : this( constructor(source: Parcel) : this(
title = source.readString(), title = source.readString().orEmpty(),
content = source.readString(), content = source.readString(),
date_published = source.readString(), date_published = source.readString().orEmpty(),
lead_image_url = source.readString(), lead_image_url = source.readString(),
dek = source.readString(), dek = source.readString().orEmpty(),
url = source.readString(), url = source.readString().orEmpty(),
domain = source.readString(), domain = source.readString().orEmpty(),
excerpt = source.readString(), excerpt = source.readString().orEmpty(),
total_pages = source.readInt(), total_pages = source.readInt(),
rendered_pages = source.readInt(), rendered_pages = source.readInt(),
next_page_url = source.readString() next_page_url = source.readString().orEmpty()
) )
override fun describeContents() = 0 override fun describeContents() = 0

View File

@ -3,6 +3,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
import com.burgstaller.okhttp.AuthenticationCacheInterceptor import com.burgstaller.okhttp.AuthenticationCacheInterceptor
import com.burgstaller.okhttp.CachingAuthenticatorDecorator import com.burgstaller.okhttp.CachingAuthenticatorDecorator
@ -12,11 +13,14 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator
import com.burgstaller.okhttp.digest.Credentials import com.burgstaller.okhttp.digest.Credentials
import com.burgstaller.okhttp.digest.DigestAuthenticator import com.burgstaller.okhttp.digest.DigestAuthenticator
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -24,8 +28,7 @@ class SelfossApi(
c: Context, c: Context,
callingActivity: Activity?, callingActivity: Activity?,
isWithSelfSignedCert: Boolean, isWithSelfSignedCert: Boolean,
timeout: Long, timeout: Long
shouldLog: Boolean
) { ) {
private lateinit var service: SelfossService private lateinit var service: SelfossService
@ -62,6 +65,17 @@ class SelfossApi(
.maybeWithSelfSigned(isWithSelfSignedCert) .maybeWithSelfSigned(isWithSelfSignedCert)
.authenticator(CachingAuthenticatorDecorator(this, authCache)) .authenticator(CachingAuthenticatorDecorator(this, authCache))
.addInterceptor(AuthenticationCacheInterceptor(authCache)) .addInterceptor(AuthenticationCacheInterceptor(authCache))
.addInterceptor(object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response: Response = chain.proceed(request)
if (response.code == 408) {
return response
}
return response
}
})
} }
init { init {
@ -83,15 +97,34 @@ class SelfossApi(
val logging = HttpLoggingInterceptor() val logging = HttpLoggingInterceptor()
logging.level = if (shouldLog) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
logging.level = HttpLoggingInterceptor.Level.NONE
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout) val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
httpClient.addInterceptor(logging) val timeoutCode = 504
httpClient
.addInterceptor { chain ->
val res = chain.proceed(chain.request())
if (res.code == timeoutCode) {
throw SocketTimeoutException("timeout")
}
res
}
.addInterceptor(logging)
.addInterceptor { chain ->
val request = chain.request()
try {
chain.proceed(request)
} catch (e: SocketTimeoutException) {
Response.Builder()
.code(timeoutCode)
.protocol(Protocol.HTTP_2)
.body("".toResponseBody("text/plain".toMediaTypeOrNull()))
.message("")
.request(request)
.build()
}
}
try { try {
val retrofit = val retrofit =
@ -112,45 +145,50 @@ class SelfossApi(
fun login(): Call<SuccessResponse> = fun login(): Call<SuccessResponse> =
service.loginToSelfoss(config.userLogin, config.userPassword) service.loginToSelfoss(config.userLogin, config.userPassword)
fun readItems( suspend fun readItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("read", tag, sourceId, search, itemsNumber, offset) getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun newItems( suspend fun newItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("unread", tag, sourceId, search, itemsNumber, offset) getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun starredItems( suspend fun starredItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset) getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun allItems(): Call<List<Item>> = fun allItems(): Call<List<Item>> =
service.allItems(userName, password) service.allItems(userName, password)
private fun getItems( suspend fun allNewItems(): retrofit2.Response<List<Item>> =
getItems("unread", null, null, null, 200, 0)
suspend fun allReadItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0)
suspend fun allStarredItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0)
private suspend fun getItems(
type: String, type: String,
tag: String?, tag: String?,
sourceId: Long?, sourceId: Long?,
search: String?, search: String?,
items: Int, items: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
service.getItems(type, tag, sourceId, search, userName, password, items, offset) service.getItems(type, tag, sourceId, search, null, userName, password, items, offset)
suspend fun updateItems(
updatedSince: String
): retrofit2.Response<List<Item>> =
service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0)
fun markItem(itemId: String): Call<SuccessResponse> = fun markItem(itemId: String): Call<SuccessResponse> =
service.markAsRead(itemId, userName, password) service.markAsRead(itemId, userName, password)
@ -158,7 +196,7 @@ class SelfossApi(
fun unmarkItem(itemId: String): Call<SuccessResponse> = fun unmarkItem(itemId: String): Call<SuccessResponse> =
service.unmarkAsRead(itemId, userName, password) service.unmarkAsRead(itemId, userName, password)
fun readAll(ids: List<String>): Call<SuccessResponse> = suspend fun readAll(ids: List<String>): SuccessResponse =
service.markAllAsRead(ids, userName, password) service.markAllAsRead(ids, userName, password)
fun starrItem(itemId: String): Call<SuccessResponse> = fun starrItem(itemId: String): Call<SuccessResponse> =
@ -167,8 +205,7 @@ class SelfossApi(
fun unstarrItem(itemId: String): Call<SuccessResponse> = fun unstarrItem(itemId: String): Call<SuccessResponse> =
service.unstarr(itemId, userName, password) service.unstarr(itemId, userName, password)
val stats: Call<Stats> suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
get() = service.stats(userName, password)
val tags: Call<List<Tag>> val tags: Call<List<Tag>>
get() = service.tags(userName, password) get() = service.tags(userName, password)
@ -176,6 +213,9 @@ class SelfossApi(
fun update(): Call<String> = fun update(): Call<String> =
service.update(userName, password) service.update(userName, password)
val apiVersion: Call<ApiVersion>
get() = service.version()
val sources: Call<List<Source>> val sources: Call<List<Source>>
get() = service.sources(userName, password) get() = service.sources(userName, password)
@ -193,4 +233,13 @@ class SelfossApi(
filter: String filter: String
): Call<SuccessResponse> = ): Call<SuccessResponse> =
service.createSource(title, url, spout, tags, filter, userName, password) service.createSource(title, url, spout, tags, filter, userName, password)
fun createSourceApi2(
title: String,
url: String,
spout: String,
tags: List<String>,
filter: String
): Call<SuccessResponse> =
service.createSourceApi2(title, url, spout, tags, filter, userName, password)
} }

View File

@ -0,0 +1,134 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import android.content.Context
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import kotlinx.coroutines.*
import retrofit2.Response
suspend fun getAndStoreAllItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
launch {
try {
enqueueArticles(api.allNewItems(), db, true)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(api.allReadItems(), db, false)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(api.allStarredItems(), db, false)
} catch (e: Throwable) {}
}
} else {
launch { SharedItems.updateDatabase(db) }
}
}
suspend fun updateItems(context: Context, api: SelfossApi, db: AppDatabase) = coroutineScope {
if (isNetworkAvailable(context)) {
launch {
try {
enqueueArticles(api.updateItems(SharedItems.items[0].datetime), db, true)
} catch (e: Throwable) {}
}
}
}
suspend fun refreshFocusedItems(context: Context, api: SelfossApi, db: AppDatabase) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
val response = when (SharedItems.displayedItems) {
"read" -> api.readItems(200, 0)
"unread" -> api.newItems(200, 0)
"starred" -> api.starredItems(200, 0)
else -> api.readItems(200, 0)
}
if (response.isSuccessful) {
SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>)
SharedItems.updateDatabase(db)
}
}
}
suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.readItems( 200, offset), db, false)
SharedItems.fetchedAll = true
SharedItems.updateDatabase(db)
} catch (e: Throwable) {}
}
}
suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
if (!SharedItems.fetchedUnread) {
SharedItems.clearDBItems(db)
}
enqueueArticles(api.newItems(200, offset), db, false)
SharedItems.fetchedUnread = true
} catch (e: Throwable) {}
}
SharedItems.updateDatabase(db)
}
suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.starredItems(200, offset), db, false)
SharedItems.fetchedStarred = true
SharedItems.updateDatabase(db)
} catch (e: Throwable) {
}
}
}
suspend fun readAll(context: Context, api: SelfossApi, db: AppDatabase): Boolean {
var success = false
if (isNetworkAvailable(context)) {
try {
val ids = SharedItems.focusedItems.map { it.id }
if (ids.isNotEmpty()) {
val result = api.readAll(ids)
SharedItems.readItems(db, ids)
success = result.isSuccess
}
} catch (e: Throwable) {}
}
return success
}
suspend fun reloadBadges(context: Context, api: SelfossApi) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
val response = api.stats()
if (response.isSuccessful) {
val badges = response.body()
SharedItems.badgeUnread = badges!!.unread
SharedItems.badgeAll = badges.total
SharedItems.badgeStarred = badges.starred
}
} catch (e: Throwable) {}
} else {
SharedItems.computeBadges()
}
}
private fun enqueueArticles(response: Response<List<Item>>, db: AppDatabase, clearDatabase: Boolean) {
if (response.isSuccessful) {
if (clearDatabase) {
CoroutineScope(Dispatchers.IO).launch {
SharedItems.clearDBItems(db)
}
}
val allItems = response.body() as ArrayList<Item>
SharedItems.appendNewItems(allItems)
}
}

View File

@ -4,9 +4,15 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.text.Html
import android.webkit.URLUtil
import org.jsoup.Jsoup
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
private fun constructUrl(config: Config?, path: String, file: String?): String { private fun constructUrl(config: Config?, path: String, file: String?): String {
@ -24,7 +30,11 @@ data class Tag(
@SerializedName("tag") val tag: String, @SerializedName("tag") val tag: String,
@SerializedName("color") val color: String, @SerializedName("color") val color: String,
@SerializedName("unread") val unread: Int @SerializedName("unread") val unread: Int
) ) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
class SuccessResponse(@SerializedName("success") val success: Boolean) { class SuccessResponse(@SerializedName("success") val success: Boolean) {
val isSuccess: Boolean val isSuccess: Boolean
@ -42,6 +52,19 @@ data class Spout(
@SerializedName("description") val description: String @SerializedName("description") val description: String
) )
data class ApiVersion(
@SerializedName("version") val version: String?,
@SerializedName("apiversion") val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
data class Source( data class Source(
@SerializedName("id") val id: String, @SerializedName("id") val id: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@ -58,6 +81,10 @@ data class Source(
} }
return constructUrl(config, "favicons", icon) return constructUrl(config, "favicons", icon)
} }
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
} }
data class Item( data class Item(
@ -65,10 +92,10 @@ data class Item(
@SerializedName("datetime") val datetime: String, @SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("content") val content: String, @SerializedName("content") val content: String,
@SerializedName("unread") val unread: Boolean, @SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean, @SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String, @SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String, @SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String, @SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String, @SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: SelfossTagType @SerializedName("tags") val tags: SelfossTagType
@ -84,17 +111,17 @@ data class Item(
} }
constructor(source: Parcel) : this( constructor(source: Parcel) : this(
id = source.readString(), id = source.readString().orEmpty(),
datetime = source.readString(), datetime = source.readString().orEmpty(),
title = source.readString(), title = source.readString().orEmpty(),
content = source.readString(), content = source.readString().orEmpty(),
unread = 0.toByte() != source.readByte(), unread = 0.toByte() != source.readByte(),
starred = 0.toByte() != source.readByte(), starred = 0.toByte() != source.readByte(),
thumbnail = source.readString(), thumbnail = source.readString(),
icon = source.readString(), icon = source.readString(),
link = source.readString(), link = source.readString().orEmpty(),
sourcetitle = source.readString(), sourcetitle = source.readString().orEmpty(),
tags = source.readParcelable(ClassLoader.getSystemClassLoader()) tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("")
) )
override fun describeContents() = 0 override fun describeContents() = 0
@ -127,6 +154,51 @@ data class Item(
return constructUrl(config, "thumbnails", thumbnail) return constructUrl(config, "thumbnails", thumbnail)
} }
fun getImages() : ArrayList<String> {
var allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.toLowerCase().contains(".jpg") ||
url.toLowerCase().contains(".jpeg") ||
url.toLowerCase().contains(".png") ||
url.toLowerCase().contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try {
for (url in imageUrls) {
if ( URLUtil.isValidUrl(url)) {
val image = Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
}
}
} catch (e : Error) {
return false
}
return true
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls // TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
var stringUrl: String var stringUrl: String
@ -168,7 +240,7 @@ data class SelfossTagType(val tags: String) : Parcelable {
} }
constructor(source: Parcel) : this( constructor(source: Parcel) : this(
tags = source.readString() tags = source.readString().orEmpty()
) )
override fun describeContents() = 0 override fun describeContents() = 0

View File

@ -1,6 +1,7 @@
package apps.amine.bou.readerforselfoss.api.selfoss package apps.amine.bou.readerforselfoss.api.selfoss
import retrofit2.Call import retrofit2.Call
import retrofit2.Response
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
@ -16,16 +17,17 @@ internal interface SelfossService {
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse> fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
@GET("items") @GET("items")
fun getItems( suspend fun getItems(
@Query("type") type: String, @Query("type") type: String,
@Query("tag") tag: String?, @Query("tag") tag: String?,
@Query("source") source: Long?, @Query("source") source: Long?,
@Query("search") search: String?, @Query("search") search: String?,
@Query("updatedsince") updatedSince: String?,
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String, @Query("password") password: String,
@Query("items") items: Int, @Query("items") items: Int,
@Query("offset") offset: Int @Query("offset") offset: Int
): Call<List<Item>> ): Response<List<Item>>
@GET("items") @GET("items")
fun allItems( fun allItems(
@ -51,11 +53,11 @@ internal interface SelfossService {
@FormUrlEncoded @FormUrlEncoded
@POST("mark") @POST("mark")
fun markAllAsRead( suspend fun markAllAsRead(
@Field("ids[]") ids: List<String>, @Field("ids[]") ids: List<String>,
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<SuccessResponse> ): SuccessResponse
@Headers("Content-Type: application/x-www-form-urlencoded") @Headers("Content-Type: application/x-www-form-urlencoded")
@POST("starr/{id}") @POST("starr/{id}")
@ -74,10 +76,10 @@ internal interface SelfossService {
): Call<SuccessResponse> ): Call<SuccessResponse>
@GET("stats") @GET("stats")
fun stats( suspend fun stats(
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<Stats> ): Response<Stats>
@GET("tags") @GET("tags")
fun tags( fun tags(
@ -103,6 +105,9 @@ internal interface SelfossService {
@Query("password") password: String @Query("password") password: String
): Call<List<Source>> ): Call<List<Source>>
@GET("api/about")
fun version(): Call<ApiVersion>
@DELETE("source/{id}") @DELETE("source/{id}")
fun deleteSource( fun deleteSource(
@Path("id") id: String, @Path("id") id: String,
@ -121,4 +126,16 @@ internal interface SelfossService {
@Query("username") username: String, @Query("username") username: String,
@Query("password") password: String @Query("password") password: String
): Call<SuccessResponse> ): Call<SuccessResponse>
@FormUrlEncoded
@POST("source")
fun createSourceApi2(
@Field("title") title: String,
@Field("url") url: String,
@Field("spout") spout: String,
@Field("tags[]") tags: List<String>,
@Field("filter") filter: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
} }

View File

@ -4,7 +4,8 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.preference.PreferenceManager import android.os.Build
import androidx.preference.PreferenceManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_LOW import androidx.core.app.NotificationCompat.PRIORITY_LOW
@ -13,17 +14,19 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import apps.amine.bou.readerforselfoss.MainActivity import apps.amine.bou.readerforselfoss.MainActivity
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import kotlinx.coroutines.CoroutineScope
import org.acra.ACRA import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -34,103 +37,118 @@ import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
lateinit var db: AppDatabase lateinit var db: AppDatabase
override fun doWork(): Result { override fun doWork(): Result {
if (context.isNetworkAccessible(null)) { val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
val notificationManager = if (isNetworkAvailable(context)) {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(applicationContext, Config.syncChannelId) CoroutineScope(Dispatchers.IO).launch {
.setContentTitle(context.getString(R.string.loading_notification_title)) val notificationManager =
.setContentText(context.getString(R.string.loading_notification_text)) applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_cloud_download)
notificationManager.notify(1, notification.build()) val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val settings = notificationManager.notify(1, notification.build())
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder( val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
val api = SelfossApi( db = Room.databaseBuilder(
this.context, applicationContext,
null, AppDatabase::class.java, "selfoss-database"
settings.getBoolean("isSelfSignedCert", false), ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
sharedPref.getString("api_timeout", "-1").toLong(), .addMigrations(MIGRATION_3_4).build()
sharedPref.getBoolean("should_log_everything", false)
)
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize))
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
})
thread {
val actions = db.actionsDao().actions() val actions = db.actionsDao().actions()
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(api.markItem(action.articleId), action) action.read -> doAndReportOnFail(
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action) api.markItem(action.articleId),
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action) action
)
action.unread -> doAndReportOnFail(
api.unmarkItem(action.articleId),
action
)
action.starred -> doAndReportOnFail(
api.starrItem(action.articleId),
action
)
action.unstarred -> doAndReportOnFail( action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId), api.unstarrItem(action.articleId),
action action
) )
} }
} }
getAndStoreAllItems(context, api, db)
SharedItems.updateDatabase(db)
storeItems(notifyNewItems, notificationManager)
}
}
}
return Result.success()
}
private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
CoroutineScope(Dispatchers.IO).launch {
val apiItems = SharedItems.items
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
apiItems.map { it.preloadImages(context) }
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
} }
} }
return Result.SUCCESS
} }
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) { private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
@ -145,7 +163,6 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
} }
override fun onFailure(call: Call<T>, t: Throwable) { override fun onFailure(call: Call<T>, t: Throwable) {
ACRA.getErrorReporter().maybeHandleSilentException(t, context)
} }
}) })
} }

View File

@ -1,57 +1,55 @@
package apps.amine.bou.readerforselfoss.fragments package apps.amine.bou.readerforselfoss.fragments
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import android.view.InflateException import android.view.*
import android.webkit.*
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.room.Room import androidx.room.Room
import apps.amine.bou.readerforselfoss.ImageActivity
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.*
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.succeeded
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.view.*
import org.acra.ACRA
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import kotlin.concurrent.thread import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList
class ArticleFragment : Fragment() { class ArticleFragment : Fragment() {
private lateinit var pageNumber: Number private lateinit var pageNumber: Number
@ -63,11 +61,22 @@ class ArticleFragment : Fragment() {
private lateinit var contentSource: String private lateinit var contentSource: String
private lateinit var contentImage: String private lateinit var contentImage: String
private lateinit var contentTitle: String private lateinit var contentTitle: String
private lateinit var allImages : ArrayList<String>
private lateinit var editor: SharedPreferences.Editor private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var db: AppDatabase private lateinit var db: AppDatabase
private lateinit var textAlignment: String private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private lateinit var prefs: SharedPreferences
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
private var staticBar = false
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
@ -77,66 +86,75 @@ class ArticleFragment : Fragment() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(activity!!) appColors = AppColors(requireActivity())
config = Config(requireActivity())
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
pageNumber = arguments!!.getInt(ARG_POSITION) pageNumber = requireArguments().getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS) allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
db = Room.databaseBuilder( db = Room.databaseBuilder(
context!!, requireContext(),
AppDatabase::class.java, "selfoss-database" AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build() ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
} }
private var rootView: ViewGroup? = null
private lateinit var prefs: SharedPreferences
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
try { try {
rootView = inflater _binding = FragmentArticleBinding.inflate(inflater, container, false)
.inflate(R.layout.fragment_article, container, false) as ViewGroup
url = allItems[pageNumber.toInt()].getLinkDecoded() url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title contentTitle = allItems[pageNumber.toInt()].getTitleDecoded()
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!) contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity())
contentSource = allItems[pageNumber.toInt()].sourceAndDateText() contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
allImages = allItems[pageNumber.toInt()].getImages()
prefs = PreferenceManager.getDefaultSharedPreferences(activity) prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit() editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16").toInt() fontSize = prefs.getString("reader_font_size", "16")!!.toInt()
staticBar = prefs.getBoolean("reader_static_bar", false)
font = prefs.getString("reader_font", "")!!
if (font.isNotEmpty()) {
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
// Just to be sure
null
}
}
refreshAlignment() refreshAlignment()
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val debugReadingItems = prefs.getBoolean("read_debug", false)
val api = SelfossApi( val api = SelfossApi(
context!!, requireContext(),
activity!!, requireActivity(),
settings.getBoolean("isSelfSignedCert", false), settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(), prefs.getString("api_timeout", "-1")!!.toLong()
prefs.getBoolean("should_log_everything", false)
) )
fab = rootView!!.fab fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark fab.rippleColor = appColors.colorAccentDark
val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar val floatingToolbar: FloatingToolbar = binding.floatingToolbar
floatingToolbar.attachFab(fab) floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent) floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = activity!!.buildCustomTabsIntent() val customTabsIntent = requireActivity().buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper() mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity) mCustomTabActivityHelper!!.bindCustomTabsService(activity)
@ -146,49 +164,43 @@ class ArticleFragment : Fragment() {
override fun onItemClick(item: MenuItem) { override fun onItemClick(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs) R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
R.id.share_action -> activity!!.shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> activity!!.openItemUrl( R.id.open_action -> requireActivity().openItemUrl(
allItems, allItems,
pageNumber.toInt(), pageNumber.toInt(),
url, url,
customTabsIntent, customTabsIntent,
false, false,
false, false,
activity!! requireActivity()
) )
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) { R.id.unread_action -> if (context != null) {
api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue( if (allItems[pageNumber.toInt()].unread) {
object : Callback<SuccessResponse> { SharedItems.readItem(
override fun onResponse( context!!,
call: Call<SuccessResponse>, api,
response: Response<SuccessResponse> db,
) { allItems[pageNumber.toInt()]
if (!response.succeeded() && debugReadingItems) { )
val message = allItems[pageNumber.toInt()].unread = false
"message: ${response.message()} " + Toast.makeText(
"response isSuccess: ${response.isSuccessful} " + context,
"response code: ${response.code()} " + R.string.marked_as_read,
"response message: ${response.message()} " + Toast.LENGTH_LONG
"response errorBody: ${response.errorBody()?.string()} " + ).show()
"body success: ${response.body()?.success} " + } else {
"body isSuccess: ${response.body()?.isSuccess}" SharedItems.unreadItem(
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), activity!!) context!!,
} api,
} db,
allItems[pageNumber.toInt()]
override fun onFailure( )
call: Call<SuccessResponse>, allItems[pageNumber.toInt()].unread = true
t: Throwable Toast.makeText(
) { context,
if (debugReadingItems) { R.string.marked_as_unread,
ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!) Toast.LENGTH_LONG
} ).show()
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
} }
} }
else -> Unit else -> Unit
@ -200,55 +212,76 @@ class ArticleFragment : Fragment() {
} }
) )
rootView!!.source.text = contentSource if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) { if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs) getContentFromMercury(customTabsIntent, prefs)
} else { } else {
rootView!!.titleView.text = contentTitle binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview() htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) { if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
Glide Glide
.with(context!!) .with(requireContext())
.asBitmap() .asBitmap()
.load(contentImage) .loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView) .into(binding.imageView)
} else { } else {
rootView!!.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
} }
rootView!!.nestedScrollView.setOnScrollChangeListener( binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) { if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide() fab.hide()
} else { } else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show() if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
} }
} }
) )
} catch (e: InflateException) { } catch (e: InflateException) {
AlertDialog.Builder(context!!) AlertDialog.Builder(requireContext())
.setMessage(context!!.getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(context!!.getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok .setPositiveButton(android.R.string.ok
) { dialog, which -> ) { dialog, which ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!) val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPref.edit() val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false) editor.putBoolean("prefer_article_viewer", false)
editor.commit() editor.commit()
activity!!.finish() requireActivity().finish()
} }
.create() .create()
.show() .show()
} }
return rootView return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
} }
private fun refreshAlignment() { private fun refreshAlignment() {
@ -263,11 +296,9 @@ class ArticleFragment : Fragment() {
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences prefs: SharedPreferences
) { ) {
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) { if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
rootView!!.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi( val parser = MercuryApi()
prefs.getBoolean("should_log_everything", false)
)
parser.parseUrl(url).enqueue( parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> { object : Callback<ParsedContent> {
@ -279,7 +310,10 @@ class ArticleFragment : Fragment() {
try { try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try { try {
rootView!!.titleView.text = response.body()!!.title binding.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
try { try {
// Note: Mercury may return relative urls... If it does the url val will not be changed. // Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url) URL(response.body()!!.url)
@ -288,49 +322,40 @@ class ArticleFragment : Fragment() {
// Mercury returned a relative url. We do nothing. // Mercury returned a relative url. We do nothing.
} }
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} }
try { try {
contentText = response.body()!!.content.orEmpty() contentText = response.body()!!.content.orEmpty()
htmlToWebview() htmlToWebview()
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} }
try { try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
try { try {
Glide Glide
.with(context!!) .with(requireContext())
.asBitmap() .asBitmap()
.load(response.body()!!.lead_image_url) .loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView) .into(binding.imageView)
} catch (e: Exception) { } catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
} }
} else { } else {
rootView!!.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
} }
} }
try { try {
rootView!!.nestedScrollView.scrollTo(0, 0) binding.nestedScrollView.scrollTo(0, 0)
rootView!!.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
} }
} }
} else { } else {
@ -338,13 +363,11 @@ class ArticleFragment : Fragment() {
openInBrowserAfterFailing(customTabsIntent) openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
if (context != null) { if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
} }
} }
} }
@ -361,28 +384,33 @@ class ArticleFragment : Fragment() {
private fun htmlToWebview() { private fun htmlToWebview() {
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent) val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
rootView!!.webcontent.visibility = View.VISIBLE val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
val (textColor, backgroundColor) = if (appColors.isDarkTheme) { val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
if (context != null) { if (context != null) {
rootView!!.webcontent.setBackgroundColor( binding.webcontent.setBackgroundColor(
ContextCompat.getColor( ContextCompat.getColor(
context!!, requireContext(),
R.color.dark_webview R.color.dark_webview
) )
) )
Pair(ContextCompat.getColor(context!!, R.color.dark_webview_text), ContextCompat.getColor(context!!, R.color.light_webview_text)) Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview))
} else { } else {
Pair(null, null) Pair(null, null)
} }
} else { } else {
if (context != null) { if (context != null) {
rootView!!.webcontent.setBackgroundColor( binding.webcontent.setBackgroundColor(
ContextCompat.getColor( ContextCompat.getColor(
context!!, requireContext(),
R.color.light_webview R.color.light_webview
) )
) )
Pair(ContextCompat.getColor(context!!, R.color.light_webview_text), ContextCompat.getColor(context!!, R.color.dark_webview_text)) Pair(ContextCompat.getColor(requireContext(), R.color.light_webview_text), ContextCompat.getColor(requireContext(), R.color.light_webview))
} else { } else {
Pair(null, null) Pair(null, null)
} }
@ -400,27 +428,81 @@ class ArticleFragment : Fragment() {
"#FFFFFF" "#FFFFFF"
} }
rootView!!.webcontent.settings.useWideViewPort = true binding.webcontent.settings.useWideViewPort = true
rootView!!.webcontent.settings.loadWithOverviewMode = true binding.webcontent.settings.loadWithOverviewMode = true
rootView!!.webcontent.settings.javaScriptEnabled = false binding.webcontent.settings.javaScriptEnabled = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { binding.webcontent.webViewClient = object : WebViewClient() {
rootView!!.webcontent.settings.layoutAlgorithm = override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
} else { requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN }
return true
}
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.toLowerCase().contains(".jpg") || url.toLowerCase().contains(".jpeg")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
}catch ( e : ExecutionException) {}
}
else if (url.toLowerCase().contains(".png")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
}catch ( e : ExecutionException) {}
}
else if (url.toLowerCase().contains(".webp")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
}catch ( e : ExecutionException) {}
}
return super.shouldInterceptRequest(view, url)
}
} }
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return performClick()
}
})
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null var baseUrl: String? = null
try { try {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
} }
rootView!!.webcontent.loadDataWithBaseURL( val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
binding.webcontent.loadDataWithBaseURL(
baseUrl, baseUrl,
"""<html> """<html>
|<head> |<head>
@ -443,6 +525,14 @@ class ArticleFragment : Fragment() {
| text-align: $textAlignment; | text-align: $textAlignment;
| word-break: break-word; | word-break: break-word;
| overflow:hidden; | overflow:hidden;
| line-height: 1.5em;
| background-color: $stringBackgroundColor;
| }
| body, html {
| background-color: $stringBackgroundColor !important;
| border-color: $stringBackgroundColor !important;
| padding: 0 !important;
| margin: 0 !important;
| } | }
| a, pre, code { | a, pre, code {
| text-align: $textAlignment; | text-align: $textAlignment;
@ -453,6 +543,7 @@ class ArticleFragment : Fragment() {
| background-color: $stringBackgroundColor; | background-color: $stringBackgroundColor;
| } | }
| </style> | </style>
| $fontLinkAndStyle
|</head> |</head>
|<body> |<body>
| $contentText | $contentText
@ -464,15 +555,15 @@ class ArticleFragment : Fragment() {
} }
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) { private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
rootView!!.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
activity!!.openItemUrl( requireActivity().openItemUrl(
allItems, allItems,
pageNumber.toInt(), pageNumber.toInt(),
url, url,
customTabsIntent, customTabsIntent,
true, true,
false, false,
activity!! requireActivity()
) )
} }
@ -493,5 +584,20 @@ class ArticleFragment : Fragment() {
} }
} }
fun performClick(): Boolean {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages)
intent.putExtra("position", position)
startActivity(intent)
return false
}
return false
}
} }

View File

@ -0,0 +1,57 @@
package apps.amine.bou.readerforselfoss.fragments
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.databinding.FragmentImageBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
class ImageFragment : Fragment() {
private lateinit var imageUrl : String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null
private val binding get() = _binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
imageUrl = requireArguments().getString("imageUrl")!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE
Glide.with(activity)
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_IMAGE = "imageUrl"
fun newInstance(
imageUrl : String
): ImageFragment {
val fragment = ImageFragment()
val args = Bundle()
args.putString(ARG_IMAGE, imageUrl)
fragment.arguments = args
return fragment
}
}
}

View File

@ -10,7 +10,7 @@ import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
@Dao @Dao
interface ActionsDao { interface ActionsDao {
@Query("SELECT * FROM actions order by id asc") @Query("SELECT * FROM actions order by id asc")
fun actions(): List<ActionEntity> suspend fun actions(): List<ActionEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllActions(vararg actions: ActionEntity) fun insertAllActions(vararg actions: ActionEntity)

View File

@ -13,17 +13,17 @@ import androidx.room.Update
@Dao @Dao
interface ItemsDao { interface ItemsDao {
@Query("SELECT * FROM items order by id desc") @Query("SELECT * FROM items order by id desc")
fun items(): List<ItemEntity> suspend fun items(): List<ItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllItems(vararg items: ItemEntity) suspend fun insertAllItems(vararg items: ItemEntity)
@Query("DELETE FROM items") @Query("DELETE FROM items")
fun deleteAllItems() suspend fun deleteAllItems()
@Delete @Delete
fun delete(item: ItemEntity) suspend fun delete(item: ItemEntity)
@Update @Update
fun updateItem(item: ItemEntity) suspend fun updateItem(item: ItemEntity)
} }

View File

@ -10,7 +10,7 @@ import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
@Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 3) @Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun drawerDataDao(): DrawerDataDao abstract fun drawerDataDao(): DrawerDataDao

View File

@ -20,9 +20,9 @@ data class ItemEntity(
@ColumnInfo(name = "starred") @ColumnInfo(name = "starred")
var starred: Boolean, var starred: Boolean,
@ColumnInfo(name = "thumbnail") @ColumnInfo(name = "thumbnail")
val thumbnail: String, val thumbnail: String?,
@ColumnInfo(name = "icon") @ColumnInfo(name = "icon")
val icon: String, val icon: String?,
@ColumnInfo(name = "link") @ColumnInfo(name = "link")
val link: String, val link: String,
@ColumnInfo(name = "sourcetitle") @ColumnInfo(name = "sourcetitle")

View File

@ -14,3 +14,21 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) {
database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))") database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
} }
} }
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
// @see https://stackoverflow.com/questions/57392015/how-to-migrate-not-null-table-column-into-null-in-android-room-database
// Create the new table
database.execSQL("CREATE TABLE IF NOT EXISTS `itemstmp` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT, `icon` TEXT, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
// Copy the data
database.execSQL(
"INSERT INTO itemstmp (`id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags`) SELECT `id`, `datetime`, `title`, `content`, `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, `tags` FROM items")
// Remove the old table
database.execSQL("DROP TABLE items")
// Change the table name to the correct one
database.execSQL("ALTER TABLE itemstmp RENAME TO items")
}
}

View File

@ -1,142 +0,0 @@
package apps.amine.bou.readerforselfoss.settings;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.appbar.AppBarLayout;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.ftinc.scoop.Scoop;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.themes.AppColors;
import apps.amine.bou.readerforselfoss.themes.Toppings;
/**
* A {@link PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
new AppColors(this);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
AppBarLayout bar = (AppBarLayout) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
Toolbar toolbar = bar.findViewById(R.id.toolbar);
Scoop scoop = Scoop.getInstance();
scoop.bind(this, Toppings.PRIMARY.getValue(), toolbar);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.getValue());
}
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
root.addView(bar, 0);
getDelegate().onPostCreate(savedInstanceState);
}
ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@NonNull
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
@Override
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}

View File

@ -1,418 +0,0 @@
package apps.amine.bou.readerforselfoss.settings;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.SwitchPreference;
import androidx.appcompat.app.ActionBar;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import java.util.List;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.themes.AppColors;
import apps.amine.bou.readerforselfoss.utils.Config;
/**
* A {@link PreferenceActivity} that presents a set of application settings. On
* handset devices, settings are presented as a single list. On tablets,
* settings are split by category, with category headers shown to the left of
* the list of settings.
* <p>
* See <a href="http://developer.android.com/design/patterns/settings.html">
* Android Design: Settings</a> for design guidelines and the <a
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
* API Guide</a> for more information on developing a Settings UI.
*/
public class SettingsActivity extends AppCompatPreferenceActivity {
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
String stringValue = value.toString();
preference.setSummary(stringValue);
return true;
}
};
/**
* Helper method to determine if the device has an extra-large screen. For
* example, 10" tablets are extra-large.
*/
private static boolean isXLargeTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
/**
* Binds a preference's summary to its value. More specifically, when the
* preference's value is changed, its summary (line of text below the
* preference title) is updated to reflect the value. The summary is also
* immediately updated upon calling this method. The exact display format is
* dependent on the type of preference.
*
* @see #sBindPreferenceSummaryToValueListener
*/
private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
new AppColors(this);
super.onCreate(savedInstanceState);
setupActionBar();
}
/**
* Set up the {@link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onIsMultiPane() {
return isXLargeTablet(this);
}
/**
* {@inheritDoc}
*/
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
}
/**
* This method stops fragment injection in malicious applications.
* Make sure to deny any unknown fragments here.
*/
@Override
protected boolean isValidFragment(String fragmentName) {
return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|| OfflinePreferenceFragment.class.getName().equals(fragmentName)
|| ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
|| DebugPreferenceFragment.class.getName().equals(fragmentName)
|| LinksPreferenceFragment.class.getName().equals(fragmentName)
|| ThemePreferenceFragment.class.getName().equals(fragmentName);
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class GeneralPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
itemsNumber.getEditText().setFilters(new InputFilter[]{
new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
try {
int input = Integer.parseInt(dest.toString() + source.toString());
if (input <= 200 && input > 0)
return null;
} catch (NumberFormatException nfe) {
Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show();
}
return "";
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class ArticleViewerPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_viewer);
setHasOptionsMenu(true);
final EditTextPreference fontSize = (EditTextPreference) findPreference("reader_font_size");
fontSize.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
try {
fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
} catch (NumberFormatException e) {}
}
});
fontSize.getEditText().setFilters(new InputFilter[]{
new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
try {
int input = Integer.parseInt(dest.toString() + source.toString());
if (input > 0)
return null;
} catch (NumberFormatException nfe) {}
return "";
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class DebugPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_debug);
setHasOptionsMenu(true);
SharedPreferences pref = getActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE);
final String id = pref.getString("unique_id", "...");
final Preference identifier = findPreference("debug_identifier");
final ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
identifier.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (clipboard != null) {
ClipData clip = ClipData.newPlainText("Selfoss unique id", id);
clipboard.setPrimaryClip(clip);
Toast.makeText(getActivity(), R.string.unique_id_to_clipboard, Toast.LENGTH_LONG).show();
return true;
}
return false;
}
});
identifier.setTitle(id);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class LinksPreferenceFragment extends PreferenceFragment {
public void openUrl(Uri uri) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(browserIntent);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_links);
setHasOptionsMenu(true);
findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
openUrl(Uri.parse(Config.trackerUrl));
return true;
}
});
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
openUrl(Uri.parse(Config.sourceUrl));
return false;
}
});
findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
openUrl(Uri.parse(Config.translationUrl));
return false;
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class ThemePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_theme);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
} else if (id == R.id.clear) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences.Editor editor = pref.edit();
editor.remove("color_primary");
editor.remove("color_primary_dark");
editor.remove("color_accent");
editor.remove("color_accent_dark");
editor.remove("dark_theme");
editor.apply();
getActivity().finish();
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.settings_theme, menu);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class OfflinePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_offline);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class ExperimentalPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_experimental);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -0,0 +1,212 @@
package apps.amine.bou.readerforselfoss.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.*
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceManager
import android.view.*
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.utils.Config
import java.lang.NumberFormatException
private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) {
setTheme(R.style.NoBarDark)
}
setContentView(R.layout.activity_settings)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainPreferenceFragment())
.commit()
} else {
title = savedInstanceState.getCharSequence(TITLE_TAG)
}
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle(R.string.title_activity_settings)
}
}
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = title
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// Save current activity title so we can set it again after a configuration change
outState.putCharSequence(TITLE_TAG, title)
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings)
return true
}
return super.onSupportNavigateUp()
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment
).apply {
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
title = pref.title
supportActionBar?.title = title
return true
}
class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_main, rootKey)
}
}
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) {
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
}
""
}
)
}
}
}
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
}
}
} }
editText.filters = arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input = (dest.toString() + source.toString()).toInt()
if (input > 0) return@InputFilter null
} catch (nfe: NumberFormatException) {
}
""
}
)
}
}
}
class OfflinePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_offline, rootKey)
}
}
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.settings_theme, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.clear) {
val pref = PreferenceManager.getDefaultSharedPreferences(activity)
val editor = pref.edit()
editor.remove("color_primary")
editor.remove("color_primary_dark")
editor.remove("color_accent")
editor.remove("color_accent_dark")
editor.remove("dark_theme")
editor.apply()
requireActivity().recreate()
}
return super.onOptionsItemSelected(item)
}
}
class LinksPreferenceFragment : PreferenceFragmentCompat() {
private fun openUrl(uri: Uri?) {
val browserIntent = Intent(Intent.ACTION_VIEW, uri)
startActivity(browserIntent)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.trackerUrl))
true
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.sourceUrl))
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.translationUrl))
false
}
}
}
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_experimental, rootKey)
}
}
}

View File

@ -2,7 +2,7 @@ package apps.amine.bou.readerforselfoss.themes
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import android.util.TypedValue import android.util.TypedValue
@ -18,6 +18,7 @@ class AppColors(a: Activity) {
@ColorInt val colorAccentDark: Int @ColorInt val colorAccentDark: Int
@ColorInt val cardBackgroundColor: Int @ColorInt val cardBackgroundColor: Int
@ColorInt val colorBackground: Int @ColorInt val colorBackground: Int
@ColorInt val textColor: Int
val isDarkTheme: Boolean val isDarkTheme: Boolean
init { init {
@ -57,6 +58,12 @@ class AppColors(a: Activity) {
android.R.color.background_light android.R.color.background_light
} }
textColor = if (isDarkTheme) {
R.color.white
} else {
R.color.grey_900
}
val wrapper = Context::class.java val wrapper = Context::class.java
val method = wrapper!!.getMethod("getThemeResId") val method = wrapper!!.getMethod("getThemeResId")
method.isAccessible = true method.isAccessible = true

View File

@ -1,22 +0,0 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.preference.PreferenceManager
import android.provider.Settings
import org.acra.ErrorReporter
fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx)
val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") == "true"
if (sharedPref.getBoolean("acra_should_log", false) && !isTestLab) {
this.handleSilentException(throwable)
}
}
fun ErrorReporter.doHandleSilentException(throwable: Throwable, ctx: Context) {
val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") == "true"
if (!isTestLab) {
this.handleSilentException(throwable)
}
}

View File

@ -11,19 +11,19 @@ class Config(c: Context) {
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE) val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
val baseUrl: String val baseUrl: String
get() = settings.getString("url", "") get() = settings.getString("url", "")!!
val userLogin: String val userLogin: String
get() = settings.getString("login", "") get() = settings.getString("login", "")!!
val userPassword: String val userPassword: String
get() = settings.getString("password", "") get() = settings.getString("password", "")!!
val httpUserLogin: String val httpUserLogin: String
get() = settings.getString("httpUserName", "") get() = settings.getString("httpUserName", "")!!
val httpUserPassword: String val httpUserPassword: String
get() = settings.getString("httpPassword", "") get() = settings.getString("httpPassword", "")!!
companion object { companion object {
const val settingsName = "paramsselfoss" const val settingsName = "paramsselfoss"
@ -40,6 +40,8 @@ class Config(c: Context) {
const val newItemsChannelId = "new-items-channel-id" const val newItemsChannelId = "new-items-channel-id"
var apiVersion = 0
fun logoutAndRedirect( fun logoutAndRedirect(
c: Context, c: Context,
callingActivity: Activity, callingActivity: Activity,

View File

@ -0,0 +1,31 @@
package apps.amine.bou.readerforselfoss.utils
import android.text.format.DateUtils
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
fun parseDate(dateString: String): Instant {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (Config.apiVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
}
}
fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString(
date.toEpochMilli(),
Instant.now().toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
}

View File

@ -1,40 +1,24 @@
package apps.amine.bou.readerforselfoss.utils package apps.amine.bou.readerforselfoss.utils
import android.content.Context import android.content.Context
import android.text.format.DateUtils
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import org.acra.ACRA
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun String.toTextDrawableString(c: Context): String { fun String.toTextDrawableString(c: Context): String {
val textDrawable = StringBuilder() val textDrawable = StringBuilder()
for (s in this.split(" ".toRegex()).filter { !it.isEmpty() }.toTypedArray()) { for (s in this.split(" ".toRegex()).filter { it.isNotEmpty() }.toTypedArray()) {
try { try {
textDrawable.append(s[0]) textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) { } catch (e: StringIndexOutOfBoundsException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, c)
} }
} }
return textDrawable.toString() return textDrawable.toString()
} }
fun Item.sourceAndDateText(): String { fun Item.sourceAndDateText(): String {
val formattedDate: String = try { val formattedDate = parseRelativeDate(this.datetime)
" " + DateUtils.getRelativeTimeSpanString(
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
Date().time,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
} catch (e: ParseException) {
e.printStackTrace()
""
}
return this.sourcetitle + formattedDate return this.getSourceTitle() + formattedDate
} }
fun Item.toggleStar(): Item { fun Item.toggleStar(): Item {

View File

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build
import android.text.Spannable import android.text.Spannable
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
@ -20,18 +21,23 @@ import apps.amine.bou.readerforselfoss.ReaderActivity
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.acra.ACRA import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.buildCustomTabsIntent(): CustomTabsIntent { fun Context.buildCustomTabsIntent(): CustomTabsIntent {
val actionIntent = Intent(Intent.ACTION_SEND) val actionIntent = Intent(Intent.ACTION_SEND)
actionIntent.type = "text/plain" actionIntent.type = "text/plain"
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity( val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
this, PendingIntent.FLAG_IMMUTABLE
0, } else {
actionIntent,
0 0
) }
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
this,
0,
actionIntent,
pflags
)
val intentBuilder = CustomTabsIntent.Builder() val intentBuilder = CustomTabsIntent.Builder()
@ -75,6 +81,7 @@ fun Context.openItemUrlInternally(
) { ) {
if (articleViewer) { if (articleViewer) {
ReaderActivity.allItems = allItems ReaderActivity.allItems = allItems
SharedItems.position = currentItem
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
@ -138,13 +145,13 @@ private fun openInBrowser(linkDecoded: String, app: Activity) {
} }
fun String.isUrlValid(): Boolean = fun String.isUrlValid(): Boolean =
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches() this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean { fun String.isBaseUrlValid(ctx: Context): Boolean {
val baseUrl = HttpUrl.parse(this) val baseUrl = this.toHttpUrlOrNull()
var existsAndEndsWithSlash = false var existsAndEndsWithSlash = false
if (baseUrl != null) { if (baseUrl != null) {
val pathSegments = baseUrl.pathSegments() val pathSegments = baseUrl.pathSegments
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1] existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
} }

View File

@ -0,0 +1,404 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.text.SimpleDateFormat
import kotlin.concurrent.thread
/*
* Singleton class that contains the articles fetched from Selfoss, it allows sharing the items list
* between Activities and Fragments
*/
object SharedItems {
var items: ArrayList<Item> = arrayListOf<Item>()
get() {
return ArrayList(field)
}
set(value) {
field = ArrayList(value)
}
var focusedItems: ArrayList<Item> = arrayListOf<Item>()
get() {
return ArrayList(field)
}
set(value) {
field = ArrayList(value)
}
var position = 0
set(value) {
field = when {
value < 0 -> 0
value > items.size -> items.size
else -> value
}
}
var displayedItems: String = "unread"
set(value) {
field = when (value) {
"all" -> "all"
"unread" -> "unread"
"read" -> "read"
"starred" -> "starred"
else -> "all"
}
}
var searchFilter: String? = null
var sourceIDFilter: Long? = null
var sourceFilter: String? = null
var tagFilter: String? = null
var itemsCaching = false
var fetchedUnread = false
var fetchedAll = false
var fetchedStarred = false
var badgeUnread = -1
var badgeAll = -1
var badgeStarred = -1
/**
* Add new items to the SharedItems list
*
* The new items are considered more updated than the ones already in the list.
* The old items present in the new list are discarded and replaced by the new ones.
* Items are compared according to the selfoss id, which should always be unique.
*/
fun appendNewItems(newItems: ArrayList<Item>) {
var tmpItems = items
if (tmpItems != newItems) {
tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<Item>
tmpItems.addAll(newItems)
items = tmpItems
sortItems()
getFocusedItems()
}
}
fun refreshFocusedItems(newItems: ArrayList<Item>) {
val tmpItems = items
tmpItems.removeAll(focusedItems)
appendNewItems(newItems)
}
suspend fun clearDBItems(db: AppDatabase) {
db.itemsDao().deleteAllItems()
}
suspend fun updateDatabase(db: AppDatabase) {
if (itemsCaching) {
if (items.isEmpty()) {
getFromDB(db)
}
db.itemsDao().deleteAllItems()
db.itemsDao().insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
}
}
fun filter() {
fun filterSearch(item: Item): Boolean {
return if (!searchFilter.isEmptyOrNullOrNullString()) {
var matched = item.title.contains(searchFilter.toString(), true)
matched = matched || item.content.contains(searchFilter.toString(), true)
matched = matched || item.sourcetitle.contains(searchFilter.toString(), true)
matched
} else {
true
}
}
var tmpItems = focusedItems
if (tagFilter != null) {
tmpItems = tmpItems.filter { it.tags.tags.contains(tagFilter.toString()) } as ArrayList<Item>
}
if (searchFilter != null) {
tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList<Item>
}
if (sourceFilter != null) {
tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList<Item>
}
focusedItems = tmpItems
}
private fun getFocusedItems() {
when (displayedItems) {
"all" -> getAll()
"unread" -> getUnRead()
"read" -> getRead()
"starred" -> getStarred()
else -> getUnRead()
}
}
fun getUnRead() {
displayedItems = "unread"
focusedItems = items.filter { item -> item.unread } as ArrayList<Item>
filter()
}
fun getRead() {
displayedItems = "read"
focusedItems = items.filter { item -> !item.unread } as ArrayList<Item>
filter()
}
fun getStarred() {
displayedItems = "starred"
focusedItems = items.filter { item -> item.starred } as ArrayList<Item>
filter()
}
fun getAll() {
displayedItems = "all"
focusedItems = items
filter()
}
suspend fun getFromDB(db: AppDatabase) {
if (itemsCaching) {
val dbItems = db.itemsDao().items().map { it.toView() } as ArrayList<Item>
appendNewItems(dbItems)
}
}
private fun removeItemAtIndex(index: Int) {
val i = focusedItems[index]
val tmpItems = focusedItems
tmpItems.remove(i)
focusedItems = tmpItems
}
fun addItemAtIndex(newItem: Item, index: Int) {
val tmpItems = focusedItems
tmpItems.add(index, newItem)
focusedItems = tmpItems
}
fun readItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item)) {
position = items.indexOf(item)
readItemAtPosition(app, api, db)
}
}
fun readItems(db: AppDatabase, ids: List<String>) {
for (id in ids) {
val match = items.filter { it -> it.id == id }
if (match.isNotEmpty() && match.size == 1) {
position = items.indexOf(match[0])
val tmpItems = items
tmpItems[position].unread = false
items = tmpItems
resetDBItem(db)
badgeUnread--
}
}
}
private fun readItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
val tmpItems = items
tmpItems[position].unread = false
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeUnread--
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
}
})
} else if (itemsCaching) {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
}
if (position > items.size) {
position -= 1
}
}
fun unreadItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item) && !item.unread) {
position = items.indexOf(item)
unreadItemAtPosition(app, api, db)
}
}
private fun unreadItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
val tmpItems = items
tmpItems[position].unread = true
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeUnread++
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_unread),
Toast.LENGTH_SHORT
).show()
}
})
} else if (itemsCaching) {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
}
}
fun starItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item) && !item.starred) {
position = items.indexOf(item)
starItemAtPosition(app, api, db)
}
}
private fun starItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) {
api.starrItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
val tmpItems = items
tmpItems[position].starred = true
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeStarred++
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_favortie),
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, true, false))
}
}
}
fun unstarItem(app: Context, api: SelfossApi, db: AppDatabase, item: Item) {
if (items.contains(item) && item.starred) {
position = items.indexOf(item)
unstarItemAtPosition(app, api, db)
}
}
private fun unstarItemAtPosition(app: Context, api: SelfossApi, db: AppDatabase) {
val i = items[position]
if (app.isNetworkAccessible(null)) {
api.unstarrItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
val tmpItems = items
tmpItems[position].starred = false
items = tmpItems
resetDBItem(db)
getFocusedItems()
badgeStarred--
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
app,
app.getString(R.string.cant_unmark_favortie),
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, false, false, true))
}
}
}
private fun resetDBItem(db: AppDatabase) {
if (itemsCaching) {
val i = items[position]
CoroutineScope(Dispatchers.IO).launch {
db.itemsDao().delete(i.toEntity())
db.itemsDao().insertAllItems(i.toEntity())
}
}
}
fun unreadItemStatusAtIndex(position: Int): Boolean {
return focusedItems[position].unread
}
fun computeBadges() {
badgeUnread = items.filter { item -> item.unread }.size
badgeStarred = items.filter { item -> item.starred }.size
badgeAll = items.size
}
private fun sortItems() {
val tmpItems = ArrayList(items.sortedByDescending { parseDate(it.datetime) })
items = tmpItems
}
}

View File

@ -1,112 +0,0 @@
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlBasePrimaryDrawerItem.java */
package apps.amine.bou.readerforselfoss.utils.drawer
import android.net.Uri
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.materialdrawer.holder.ColorHolder
import com.mikepenz.materialdrawer.holder.ImageHolder
import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.BaseDrawerItem
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerUIUtils
import com.mikepenz.materialize.util.UIUtils
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
BaseDrawerItem<T, VH>() {
fun withIcon(url: String): T {
this.icon = ImageHolder(url)
return this as T
}
fun withIcon(uri: Uri): T {
this.icon = ImageHolder(uri)
return this as T
}
var description: StringHolder? = null
private set
var descriptionTextColor: ColorHolder? = null
private set
fun withDescription(description: String): T {
this.description = StringHolder(description)
return this as T
}
fun withDescription(@StringRes descriptionRes: Int): T {
this.description = StringHolder(descriptionRes)
return this as T
}
fun withDescriptionTextColor(@ColorInt color: Int): T {
this.descriptionTextColor = ColorHolder.fromColor(color)
return this as T
}
fun withDescriptionTextColorRes(@ColorRes colorRes: Int): T {
this.descriptionTextColor = ColorHolder.fromColorRes(colorRes)
return this as T
}
/**
* a helper method to have the logic for all secondaryDrawerItems only once
* @param viewHolder
*/
protected fun bindViewHelper(viewHolder: CustomBaseViewHolder) {
val ctx = viewHolder.itemView.context
//set the identifier from the drawerItem here. It can be used to run tests
viewHolder.itemView.id = hashCode()
//set the item selected if it is
viewHolder.itemView.isSelected = isSelected
//get the correct color for the background
val selectedColor = getSelectedColor(ctx)
//get the correct color for the text
val color = getColor(ctx)
val selectedTextColor = getSelectedTextColor(ctx)
//get the correct color for the icon
val iconColor = getIconColor(ctx)
val selectedIconColor = getSelectedIconColor(ctx)
//set the background for the item
UIUtils.setBackground(
viewHolder.view,
UIUtils.getSelectableBackground(ctx, selectedColor, true)
)
//set the text for the name
StringHolder.applyTo(this.getName(), viewHolder.name)
//set the text for the description or hide
StringHolder.applyToOrHide(this.description, viewHolder.description)
//set the colors for textViews
viewHolder.name.setTextColor(getTextColorStateList(color, selectedTextColor))
//set the description text color
ColorHolder.applyToOr(
descriptionTextColor,
viewHolder.description,
getTextColorStateList(color, selectedTextColor)
)
//define the typeface for our textViews
if (getTypeface() != null) {
viewHolder.name.typeface = getTypeface()
viewHolder.description.typeface = getTypeface()
}
//we make sure we reset the image first before setting the new one in case there is an empty one
DrawerImageLoader.getInstance().cancelImage(viewHolder.icon)
viewHolder.icon.setImageBitmap(null)
//get the drawables for our icon and set it
ImageHolder.applyTo(icon, viewHolder.icon, "customUrlItem")
//for android API 17 --> Padding not applied via xml
DrawerUIUtils.setDrawerVerticalPadding(viewHolder.view)
}
}

View File

@ -1,94 +0,0 @@
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlPrimaryDrawerItem.java */
package apps.amine.bou.readerforselfoss.utils.drawer
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import android.view.View
import android.widget.TextView
import apps.amine.bou.readerforselfoss.R
import com.mikepenz.materialdrawer.holder.BadgeStyle
import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable
class CustomUrlPrimaryDrawerItem :
CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(),
ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
protected var mBadge: StringHolder = StringHolder("")
protected var mBadgeStyle = BadgeStyle()
override fun withBadge(badge: StringHolder): CustomUrlPrimaryDrawerItem {
this.mBadge = badge
return this
}
override fun withBadge(badge: String): CustomUrlPrimaryDrawerItem {
this.mBadge = StringHolder(badge)
return this
}
override fun withBadge(@StringRes badgeRes: Int): CustomUrlPrimaryDrawerItem {
this.mBadge = StringHolder(badgeRes)
return this
}
override fun withBadgeStyle(badgeStyle: BadgeStyle): CustomUrlPrimaryDrawerItem {
this.mBadgeStyle = badgeStyle
return this
}
override fun getBadge(): StringHolder {
return mBadge
}
override fun getBadgeStyle(): BadgeStyle {
return mBadgeStyle
}
override fun getType(): Int {
return R.id.material_drawer_item_custom_url_item
}
@LayoutRes
override fun getLayoutRes(): Int {
return R.layout.material_drawer_item_primary
}
override fun bindView(viewHolder: ViewHolder, payloads: List<*>?) {
super.bindView(viewHolder, payloads)
val ctx = viewHolder.itemView.context
//bind the basic view parts
bindViewHelper(viewHolder)
//set the text for the badge or hide
val badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge)
//style the badge if it is visible
if (badgeVisible) {
mBadgeStyle.style(
viewHolder.badge,
getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx))
)
viewHolder.badgeContainer.visibility = View.VISIBLE
} else {
viewHolder.badgeContainer.visibility = View.GONE
}
//define the typeface for our textViews
if (getTypeface() != null) {
viewHolder.badge.typeface = getTypeface()
}
//call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
onPostBindView(this, viewHolder.itemView)
}
override fun getViewHolder(v: View): ViewHolder {
return ViewHolder(v)
}
class ViewHolder(view: View) : CustomBaseViewHolder(view) {
val badgeContainer: View = view.findViewById(R.id.material_drawer_badge_container)
val badge: TextView = view.findViewById(R.id.material_drawer_badge)
}
}

View File

@ -2,30 +2,33 @@ package apps.amine.bou.readerforselfoss.utils.glide
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import android.widget.ImageView import android.widget.ImageView
import apps.amine.bou.readerforselfoss.utils.Config
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.BitmapImageViewTarget import com.bumptech.glide.request.target.BitmapImageViewTarget
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
fun Context.bitmapCenterCrop(url: String, iv: ImageView) = fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) =
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(url) .loadMaybeBasicAuth(config, url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.bitmapFitCenter(url: String, iv: ImageView) = fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(url) .loadMaybeBasicAuth(config, url)
.apply(RequestOptions.fitCenterTransform())
.into(iv)
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
Glide.with(this)
.asBitmap()
.load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(object : BitmapImageViewTarget(iv) { .into(object : BitmapImageViewTarget(iv) {
override fun setResource(resource: Bitmap?) { override fun setResource(resource: Bitmap?) {
@ -36,4 +39,31 @@ fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
circularBitmapDrawable.isCircular = true circularBitmapDrawable.isCircular = true
iv.setImageDrawable(circularBitmapDrawable) iv.setImageDrawable(circularBitmapDrawable)
} }
}) })
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
return ByteArrayInputStream(bitmapData)
}

View File

@ -3,7 +3,8 @@ package apps.amine.bou.readerforselfoss.utils.network
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkInfo import android.net.NetworkCapabilities
import android.os.Build
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
@ -14,9 +15,7 @@ var view: View? = null
lateinit var s: Snackbar lateinit var s: Snackbar
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkIsAccessible = isNetworkAvailable(this)
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v view = v
@ -42,4 +41,24 @@ fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boo
s.dismiss() s.dismiss()
} }
return if(overrideOffline) overrideOffline else networkIsAccessible return if(overrideOffline) overrideOffline else networkIsAccessible
}
fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false
}
} else {
val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting
}
} }

View File

@ -28,7 +28,7 @@ fun SourceEntity.toView(): Source =
fun Source.toEntity(): SourceEntity = fun Source.toEntity(): SourceEntity =
SourceEntity( SourceEntity(
this.id, this.id,
this.title, this.getTitleDecoded(),
this.tags.tags, this.tags.tags,
this.spout, this.spout,
this.error, this.error,
@ -61,13 +61,13 @@ fun Item.toEntity(): ItemEntity =
ItemEntity( ItemEntity(
this.id, this.id,
this.datetime, this.datetime,
this.title, this.getTitleDecoded(),
this.content, this.content,
this.unread, this.unread,
this.starred, this.starred,
this.thumbnail, this.thumbnail,
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.getSourceTitle(),
this.tags.tags this.tags.tags
) )

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:color="@color/red"/>
<item android:state_selected="false"
android:color="?android:attr/textColorPrimary" />
</selector>

View File

@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FFFFFFFF"
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/> android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
</vector> </vector>

View File

@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FFFFFFFF"
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/> android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
</vector> </vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

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