Compare commits

...

280 Commits

Author SHA1 Message Date
edfea45936 Migration 2021-10-27 15:22:35 +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
9eac51e729 Closes #257. 2018-12-09 16:38:35 +01:00
fa9cce6783 Removed some logs. 2018-12-09 14:26:20 +01:00
f0d4b63a97 Merge pull request #256 from aminecmi/fdroid/build
Fixes #254 and #255.
2018-12-02 13:22:37 +01:00
83eeb11388 Fixes #254 and #255. 2018-12-02 13:13:19 +01:00
01f746f33d Merge pull request #255 from aminecmi/fdroid/build
A fix for #254.
2018-12-02 03:54:36 +01:00
200851894b See #254. 2018-12-01 19:32:28 +01:00
862e5cf4ab New Crowdin translations (#253)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)
2018-11-29 06:06:32 +01:00
0b07f2a407 Closes #174. Closes #248. 2018-11-28 21:03:00 +01:00
9ba6feef0b Removed density calculation to solve #248. 2018-11-27 21:44:34 +01:00
63a0638522 Removed throw... 2018-11-27 21:36:12 +01:00
f9a4e6e363 New Crowdin translations (#252)
* 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 (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)
2018-11-27 21:32:12 +01:00
6b40fd4bdc Typos. 2018-11-27 21:25:18 +01:00
04c7776466 Trying to fix an issue with webview not available with the article viewer. Fixed an issue when the browser isn't available. 2018-11-27 21:14:03 +01:00
92c335b4e1 Closes #251. 2018-11-27 20:02:53 +01:00
17251e576b New Crowdin translations (#250)
* New translations strings.xml (Catalan)

* New translations strings.xml (Catalan)

* New translations strings.xml (Catalan)
2018-11-27 15:27:24 +01:00
62ea782429 Update CONTRIBUTING.md 2018-11-26 16:22:34 +01:00
f99474e3c1 Update CONTRIBUTING.md 2018-11-26 16:21:39 +01:00
57ac8f428f Update README.md 2018-11-26 16:14:48 +01:00
9cc1adbf15 Changing acra errors handling. 2018-11-24 09:47:58 +01:00
1d9a440ae7 Do not log for test labs devices. 2018-11-22 19:50:15 +01:00
511553806c No idea why this was changed. 2018-11-21 09:57:44 +01:00
87e7d7c4fe New Crowdin translations (#247)
* New translations strings.xml (German)

* New translations strings.xml (German)

* New translations strings.xml (German)
2018-11-21 08:51:38 +01:00
ec87089310 Fixed issue with Android 9 and CLEARTEXT communication issue. 2018-11-20 21:06:05 +01:00
d8478ebb01 Added loging url validation. 2018-11-20 19:44:10 +01:00
600adc81b5 Merge pull request #246 from Binnette/ExtraSubject
Add a vertical scrollbar to article fragment
2018-11-17 20:51:10 +01:00
ddac2870af Changed git tag sort for it to work on the jenkins server. 2018-11-17 20:48:18 +01:00
8d9c8c1394 Add a vertical scrollbar to article fragment 2018-11-17 20:37:21 +01:00
b59c3bcb23 Merge pull request #245 from Binnette/ExtraSubject
Add EXTRA_SUBJECT when sharing link
2018-11-17 19:46:51 +01:00
7f554adba5 Add EXTRA_SUBJECT when sharing link 2018-11-17 18:07:38 +01:00
21ce061282 Better handling for version code automation. 2018-11-15 21:11:15 +01:00
bdb71e9b14 Note for build. 2018-11-13 22:02:44 +01:00
df22e7de15 Still not working. 2018-11-13 22:01:41 +01:00
6b3550396b Jenkins not executing the rest of the script. 2018-11-13 21:59:28 +01:00
c70f1e31a6 Added fetch to the build script. 2018-11-13 21:57:36 +01:00
695670e944 Still fixing the local publish issue. 2018-11-13 21:47:12 +01:00
1028826788 No more local publish. 2018-11-13 21:45:10 +01:00
82a8977c96 Closes #244. 2018-11-13 20:24:06 +01:00
07d9ce1054 New Crowdin translations (#243)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-11-13 15:51:16 +01:00
7da7d49277 New Crowdin translations (#242)
* 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)
2018-11-11 17:17:37 +01:00
9b45365441 Changelog. Previous commit should close #238. 2018-11-11 12:25:45 +01:00
91a7464bce Added experimental settings with a custom timeout setting. 2018-11-11 12:23:59 +01:00
51add226eb New Crowdin translations (#241)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-11-08 08:59:37 +01:00
332e9f5108 New Crowdin translations (#240)
* 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)
2018-11-07 21:25:14 +01:00
0b91087c07 CHANGELOG. 2018-11-07 21:22:19 +01:00
ebbb1ba0f8 Closes #220. 2018-11-07 21:22:06 +01:00
e9143ae852 Initial changes for #238. 2018-11-07 21:07:29 +01:00
42e8ecee78 Offline shortcut. 2018-11-07 21:05:23 +01:00
4efd76fcbc Tab selection from app shortcut. 2018-11-07 20:45:51 +01:00
fb1614070e Inital app shortcuts. 2018-11-07 20:25:48 +01:00
c473dd7227 Fixes #239. 2018-11-07 19:32:10 +01:00
76bddb195d New Crowdin translations (#237)
* 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)
2018-11-06 20:35:30 +01:00
1e02ad2041 Closes #201. 2018-11-06 20:29:00 +01:00
f6ab909f8b Fixed issue. 2018-11-06 20:11:07 +01:00
7e520e9bed Still fixing selfoss version issues. 2018-11-05 21:11:25 +01:00
32e2d05014 CHANGELOG. 2018-11-05 20:30:58 +01:00
40d9c97f73 Fixes #216. 2018-11-05 20:30:37 +01:00
1aa68d3449 New Crowdin translations (#234)
* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-11-05 13:44:22 +01:00
aeeac8cccd Little fix. 2018-11-04 14:56:48 +01:00
7292edf997 #Closes #179. 2018-11-04 14:37:22 +01:00
f49256c72f Manual sync for read/unread/star/unstar. 2018-11-04 14:33:50 +01:00
d02b28b81f Initial changes for #179. 2018-11-04 14:25:05 +01:00
08117043dd Do not replace the background task. 2018-11-03 21:07:27 +01:00
63496c993e New Crowdin translations (#233)
* 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 (Chinese Simplified)
2018-11-03 19:01:40 +01:00
00ef542e49 Closes #33. 2018-11-03 18:48:50 +01:00
a78c6e6b33 Sync with settings. 2018-11-03 18:47:43 +01:00
363eaf9bf9 Preferences for the background tasks. 2018-11-03 18:14:22 +01:00
fec6683701 Merge branch 'master' of github.com:aminecmi/ReaderforSelfoss 2018-11-03 11:30:13 +01:00
1549edb647 ... 2018-11-03 11:29:53 +01:00
3de48ba162 Some more background tasks. 2018-11-03 11:29:03 +01:00
a2a3d6f1a7 New Crowdin translations (#232)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)

* New translations strings.xml (French)
2018-11-02 10:34:04 +01:00
ccab2c7648 Initial work on background task. 2018-11-01 21:51:31 +01:00
880dd1db5c New Crowdin translations (#231)
* 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)
2018-11-01 21:27:59 +01:00
ed18fea356 Closes #38. 2018-11-01 21:11:54 +01:00
9816b20bf6 Only do api calls on network available. 2018-11-01 21:10:00 +01:00
0bb2195bff Network status on articles loading. 2018-11-01 20:42:49 +01:00
ab2d0c4036 Closes #230. 2018-10-31 20:14:20 +01:00
99fc417109 Fixed #230. 2018-10-29 19:53:41 +01:00
dc304ef8c1 Updated gradle. 2018-10-20 09:29:09 +02:00
c5511880bc Just trying to fix fragment issues. 2018-10-19 05:27:16 +02:00
5fe76d735e Remiving items from the cache on swipe. 2018-10-17 20:19:35 +02:00
3064b3b835 Closes #228 by removing the list action bar. Action buttons are exclusively on the card view from now on. 2018-10-17 19:46:30 +02:00
70dc8af3ce New Crowdin translations (#227)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-10-16 08:33:34 +02:00
53c8c241da Order By id desc for items. 2018-10-14 18:56:07 +02:00
bdc4f5680b Merge branch 'crowdin_translation' 2018-10-14 16:58:15 +02:00
ed290573b2 Merge branch 'master' into crowdin_translation 2018-10-14 16:57:45 +02:00
1616a97a8a New translations strings.xml (French) 2018-10-14 16:32:43 +02:00
d090183007 New translations strings.xml (French) 2018-10-14 16:31:09 +02:00
de337fd260 Moving to version 1.7 with caching. 2018-10-14 15:59:02 +02:00
12dc206323 Build with optional publish. 2018-10-14 15:57:22 +02:00
d47c508dee New translations strings.xml (Galician) 2018-10-14 15:42:10 +02:00
ed75f55437 New translations strings.xml (Arabic) 2018-10-14 15:42:09 +02:00
5ad3ad4a57 New translations strings.xml (Chinese Simplified) 2018-10-14 15:42:07 +02:00
aeac1bd1d4 New translations strings.xml (Chinese Traditional) 2018-10-14 15:42:06 +02:00
4d18085072 New translations strings.xml (Czech) 2018-10-14 15:42:04 +02:00
0c9f8214ca New translations strings.xml (Danish) 2018-10-14 15:42:03 +02:00
a7ce7ce02e New translations strings.xml (Dutch) 2018-10-14 15:42:01 +02:00
820986c7f0 New translations strings.xml (Finnish) 2018-10-14 15:42:00 +02:00
8079cae745 New translations strings.xml (French) 2018-10-14 15:41:59 +02:00
6f067bd258 New translations strings.xml (German) 2018-10-14 15:41:58 +02:00
b6ade0f212 New translations strings.xml (Greek) 2018-10-14 15:41:56 +02:00
27dadc1be3 New translations strings.xml (Hebrew) 2018-10-14 15:41:55 +02:00
95e4162b4c New translations strings.xml (Hungarian) 2018-10-14 15:41:54 +02:00
f75557585e New translations strings.xml (Indonesian) 2018-10-14 15:41:52 +02:00
1b4c26919b New translations strings.xml (Afrikaans) 2018-10-14 15:41:51 +02:00
ad085bf129 New translations strings.xml (Italian) 2018-10-14 15:41:50 +02:00
8fcd551105 New translations strings.xml (Korean) 2018-10-14 15:41:48 +02:00
a0954700e2 New translations strings.xml (Norwegian) 2018-10-14 15:41:47 +02:00
9705560442 New translations strings.xml (Polish) 2018-10-14 15:41:45 +02:00
1f47a13ce5 New translations strings.xml (Portuguese) 2018-10-14 15:41:44 +02:00
6f0ff2c975 New translations strings.xml (Portuguese, Brazilian) 2018-10-14 15:41:43 +02:00
76e5477986 New translations strings.xml (Romanian) 2018-10-14 15:41:41 +02:00
7f308d5be3 New translations strings.xml (Russian) 2018-10-14 15:41:40 +02:00
54a43c83e8 New translations strings.xml (Serbian (Cyrillic)) 2018-10-14 15:41:38 +02:00
8fe7266c84 New translations strings.xml (Spanish) 2018-10-14 15:41:37 +02:00
d7a46b27b7 New translations strings.xml (Swedish) 2018-10-14 15:41:36 +02:00
2257d09fdd New translations strings.xml (Turkish) 2018-10-14 15:41:34 +02:00
047c5481c4 New translations strings.xml (Ukrainian) 2018-10-14 15:41:33 +02:00
8a6719f934 New translations strings.xml (Vietnamese) 2018-10-14 15:41:32 +02:00
51a692f3be New translations strings.xml (Japanese) 2018-10-14 15:41:30 +02:00
b333f93171 New translations strings.xml (Catalan) 2018-10-14 15:41:29 +02:00
89d34a1a71 Closes #1. May need some fine tuning. 2018-10-14 15:38:06 +02:00
8788e920ce More resources cleaning. 2018-10-14 11:25:14 +02:00
d306fb53d3 Migration with new table and schemas. 2018-10-14 11:17:24 +02:00
374537b5c7 Preparing for room migrations. Some cleaning. 2018-10-14 11:07:10 +02:00
598149d4cd Publish version. 2018-10-13 22:13:56 +02:00
50bcf18096 New translations strings.xml (Galician) 2018-10-13 22:11:24 +02:00
a089ced03f New translations strings.xml (Arabic) 2018-10-13 22:11:23 +02:00
1f18dddf8b New translations strings.xml (Chinese Simplified) 2018-10-13 22:11:21 +02:00
f5934e240e New translations strings.xml (Chinese Traditional) 2018-10-13 22:11:20 +02:00
6b8da2eacf New translations strings.xml (Czech) 2018-10-13 22:11:19 +02:00
f4757a67b7 New translations strings.xml (Danish) 2018-10-13 22:11:17 +02:00
6edeb9d840 New translations strings.xml (Dutch) 2018-10-13 22:11:16 +02:00
43ce0fd7bc New translations strings.xml (Finnish) 2018-10-13 22:11:15 +02:00
5599f5a8fc New translations strings.xml (French) 2018-10-13 22:11:13 +02:00
6fd45ceb4f New translations strings.xml (German) 2018-10-13 22:11:12 +02:00
05ad8aac29 New translations strings.xml (Greek) 2018-10-13 22:11:11 +02:00
fa4f2476b7 New translations strings.xml (Hebrew) 2018-10-13 22:11:10 +02:00
00818a94e9 New translations strings.xml (Hungarian) 2018-10-13 22:11:08 +02:00
5d5250e44a New translations strings.xml (Indonesian) 2018-10-13 22:11:07 +02:00
3052b33132 New translations strings.xml (Afrikaans) 2018-10-13 22:11:06 +02:00
50de6f8b5b New translations strings.xml (Italian) 2018-10-13 22:11:04 +02:00
f88a2f415f New translations strings.xml (Norwegian) 2018-10-13 22:11:02 +02:00
96f9813e01 New translations strings.xml (Polish) 2018-10-13 22:11:01 +02:00
fee739cb17 New translations strings.xml (Portuguese) 2018-10-13 22:11:00 +02:00
b1814c63b9 New translations strings.xml (Portuguese, Brazilian) 2018-10-13 22:10:59 +02:00
c1d45678f8 New translations strings.xml (Romanian) 2018-10-13 22:10:58 +02:00
3d34e59a94 New translations strings.xml (Russian) 2018-10-13 22:10:56 +02:00
f1133bea8b New translations strings.xml (Spanish) 2018-10-13 22:10:54 +02:00
ec64c88ff1 New translations strings.xml (Swedish) 2018-10-13 22:10:53 +02:00
be66dbba6c New translations strings.xml (Turkish) 2018-10-13 22:10:52 +02:00
8926cdbbf5 New translations strings.xml (Vietnamese) 2018-10-13 22:10:50 +02:00
a956870dec New translations strings.xml (Japanese) 2018-10-13 22:10:49 +02:00
8ed7951c9b New translations strings.xml (Catalan) 2018-10-13 22:10:48 +02:00
5569a47674 Some cleaning and preparing for items storage in room. 2018-10-13 22:02:13 +02:00
0dc6981913 Removed some unused resources. 2018-10-13 21:14:08 +02:00
4984f2f7ad Removed the intro. It was causing issues. 2018-10-13 20:49:26 +02:00
3b6891c84a App was a little slow with livedata. 2018-10-13 10:24:58 +02:00
4901e7174c Still trying to fix the build issue. 2018-10-13 04:52:55 +02:00
8d70e68fe2 Trying to fix build issue. 2018-10-12 22:50:43 +02:00
d3e1527b70 AS changes gradle version to one that does not exist. 2018-10-12 22:29:33 +02:00
0c201301f2 Replaced reservoir by room. 2018-10-12 22:04:47 +02:00
6090590f24 Imports cleaning. Libraries update. 2018-10-12 21:01:39 +02:00
06b88c783d Auto migration to android x. 2018-10-12 20:36:18 +02:00
bb75ebf635 Merge branch 'master' of github.com:aminecmi/ReaderforSelfoss 2018-10-07 22:30:30 +02:00
7d7d0014be Fixes #83. The issue wasn't selfoss related at all... 2018-10-07 22:30:07 +02:00
b3f8d44794 New Crowdin translations (#224)
* New translations strings.xml (Korean)

* New translations strings.xml (Korean)

* New translations strings.xml (Korean)
2018-10-03 14:35:03 +02:00
29d1e38340 Fixes #223. Fixes internal reader issue on 4.2.2 2018-09-30 09:37:22 +02:00
2be872e61e Fixed version update. 2018-09-25 20:56:25 +02:00
377c5518f7 Minimal versions updates. 2018-09-25 20:47:38 +02:00
21be7357b5 Revert "Updated to androidx."
This reverts commit d47ba2c820.
2018-09-25 20:27:27 +02:00
d47ba2c820 Updated to androidx. 2018-09-25 20:08:51 +02:00
a64b14614a New Crowdin translations (#222)
* New translations strings.xml (Italian)

* New translations strings.xml (Italian)

* New translations strings.xml (Italian)
2018-09-24 08:06:00 +02:00
6a88192e77 Fixes #221 2018-09-21 21:39:37 +02:00
aa7c630818 erge branch 'master' of github.com:aminecmi/ReaderforSelfoss 2018-09-15 16:26:22 +02:00
7fb54f14c7 Fixed bug 2018-09-15 16:24:29 +02:00
3d709c02b7 New Crowdin translations (#219)
* New translations strings.xml (French)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-09-12 13:16:07 +02:00
339d384561 Changelog. 2018-09-10 20:29:22 +02:00
50338d51af New Crowdin translations (#218)
* New translations strings.xml (French)

* New translations strings.xml (French)
2018-09-10 15:17:16 +02:00
92dbabf899 New Crowdin translations (#217)
* 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)
2018-09-09 20:59:34 +02:00
0043021390 Merge branch 'master' of github.com:aminecmi/ReaderforSelfoss 2018-09-09 20:40:13 +02:00
70ba9b20da Items marked as read on open. Closes #208. 2018-09-09 20:39:27 +02:00
7fda0a04a1 Fixes #215. 2018-09-09 19:45:49 +02:00
3db3157dc9 Trying to fix a strange issue with the loading when hiding items.. 2018-09-09 19:44:01 +02:00
2089fe60ca New Crowdin translations (#214)
* New translations strings.xml (Galician)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)
2018-08-06 15:49:50 +02:00
9606d36670 Hiding hidden tags from the "normal" tags section. 2018-08-03 09:49:49 +02:00
303 changed files with 7922 additions and 8275 deletions

View File

@ -41,6 +41,12 @@ Always check if the web version of your instance is working.
* Remember that PR review can take time.
# Install Selfoss (if you don't have an instance)
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
All the details to need are [here](https://selfoss.aditu.de/).
# Build the project
You can directly import this project into IntellIJ/Android Studio.

4
.gitignore vendored
View File

@ -216,6 +216,4 @@ gradle-app.setting
release/
crowdin.properties
publish-version.sh
crowdin.properties

View File

@ -1,3 +1,49 @@
**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.
- Closing #220.
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
- Closes #216. Issue with selfoss version 2.19.
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
- Closes #33. Background sync with settings.
- Closing #1. Initial article caching.
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
- Closing #38. Only doing api calls on network available.
- 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.
**1.6.x**
- Handling hidden tags.
@ -16,6 +62,10 @@
- Versions updates.
- Fixes #215, #208.
- Fixes #328.
**1.5.7.x**
- Added confirmation to the mark as read and update menues.

View File

@ -1,24 +1,34 @@
# 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/)
<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).
- 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.
<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>
## 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 ?
Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md)
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md).
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
## Useful links
@ -26,4 +36,12 @@ Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
- [Ask for help](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ)
## 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

@ -1,17 +1,17 @@
buildscript {
}
ext {
configuration = [
buildDate: new Date()
]
// This will make me able to build multiple times a day. May break thinks. I may forget it.
todaysBuilds = "1"
}
def gitVersion() {
def process = "git describe --abbrev=0 --tags".execute()
return process.text.substring(1).replaceAll("\\.", "").trim()
def process
def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute()
if (maybeTagOfCurrentCommit.text.isEmpty()) {
println "No tag on current commit. Will take the latest one."
process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
} else {
println "Tag found on current commit"
process = 'git describe --contains HEAD'.execute()
}
return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
}
def versionCodeFromGit() {
@ -28,19 +28,22 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 28
buildToolsVersion '28.0.1'
compileSdkVersion 31
buildToolsVersion '31.0.0'
buildFeatures {
viewBinding true
}
defaultConfig {
applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16
targetSdkVersion 27
targetSdkVersion 31
versionCode versionCodeFromGit()
versionName versionNameFromGit()
@ -53,7 +56,14 @@ android {
vectorDrawables.useSupportLibrary = true
// tests
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
buildTypes {
release {
@ -64,8 +74,8 @@ android {
}
debug {
buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
}
}
flavorDimensions "build"
@ -74,95 +84,81 @@ android {
versionNameSuffix '-github'
dimension "build"
}
storeConfig {
// As jenkins publishes to alpha first, this is the default suffix now.
versionNameSuffix '-store'
dimension "build"
}
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
// Testing
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
androidTestImplementation 'androidx.test:runner:1.3.1-alpha02'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02'
// Espresso-intents for validation and stubbing of Intents
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.android.support:support-vector-drawable:27.1.1'
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation "androidx.appcompat:appcompat:1.4.0-beta01"
implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:$android_version"
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.13.1'
//multidex
implementation 'com.android.support:multidex:1.0.3'
// Intro
implementation 'agency.tango.android:material-intro-screen:0.0.5'
implementation 'androidx.multidex:multidex:2.0.1'
// About
implementation('com.mikepenz:aboutlibraries:6.0.0@aar') {
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
transitive = true
}
// Async
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
// Retrofit + http logging + okhttp
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.burgstaller:okhttp-digest:1.12'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.burgstaller:okhttp-digest:2.5'
// Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.3'
implementation 'com.github.jd-alexander:LikeButton:0.2.1'
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.1.0'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
// 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'
// Asking politely users to rate the app
implementation 'com.github.stkent:amplify:2.1.0'
// Drawer
implementation 'co.zsmb:materialdrawer-kt:1.3.5'
implementation 'com.anupcowkur:reservoir:3.1.0'
implementation 'com.mikepenz:materialdrawer:8.4.4'
// Themes
implementation 'com.52inc:scoops:1.0.0'
implementation 'com.jrummyapps:colorpicker:2.1.7'
implementation 'com.jaredrummler:colorpicker:1.0.2'
implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
// Pager
implementation 'me.relex:circleindicator:1.2.2@aar'
implementation 'me.relex:circleindicator:2.0.0@aar'
implementation 'androidx.core:core-ktx:0.3'
//PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
// Crash
implementation 'ch.acra:acra-http:5.1.3'
implementation 'ch.acra:acra-dialog:5.1.3'
implementation 'androidx.core:core-ktx:1.7.0-rc01'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
implementation "androidx.room:room-ktx:2.4.0-beta01"
kapt "androidx.room:room-compiler:2.4.0-beta01"
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

@ -30,22 +30,6 @@
<fields>;
}
##Retrofit
#-keep class com.google.gson.** { *; }
#-keep class com.google.inject.** { *; }
#-keep class org.apache.http.** { *; }
#-keep class org.apache.james.mime4j.** { *; }
#-keep class javax.inject.** { *; }
#-keep class retrofit.** { *; }
#-keepclassmembernames interface * {
# @retrofit.http.* <methods>;
#}
#-keep class retrofit.** { *; }
#-keep class apps.amine.bou.readerforselfoss.api.selfoss.model.** { *; }
#-keepclassmembernames interface * {
# @retrofit.http.* <methods>;
#}
-dontwarn okio.**
-dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; }
@ -75,4 +59,7 @@
-dontwarn javax.annotation.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class android.support.v7.widget.SearchView { *; }
# maybe remove later ?
-keep class * extends androidx.fragment.app.Fragment

View File

@ -0,0 +1,96 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "08ca537d7ac9d4dd216e8e395d70801a",
"entities": [
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
"fields": [
{
"fieldPath": "tag",
"columnName": "tag",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spout",
"columnName": "spout",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")"
]
}
}

View File

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

View File

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

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

@ -2,27 +2,27 @@ package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.support.test.InstrumentationRegistry
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.action.ViewActions.closeSoftKeyboard
import android.support.test.espresso.action.ViewActions.pressBack
import android.support.test.espresso.action.ViewActions.pressKey
import android.support.test.espresso.action.ViewActions.typeText
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.contrib.DrawerActions
import android.support.test.espresso.intent.Intents
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.times
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed
import android.support.test.espresso.matcher.ViewMatchers.isRoot
import android.support.test.espresso.matcher.ViewMatchers.withContentDescription
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.pressKey
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import android.view.KeyEvent
import apps.amine.bou.readerforselfoss.utils.Config
import org.junit.After

View File

@ -1,91 +0,0 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.support.test.InstrumentationRegistry.getInstrumentation
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.intent.Intents
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.times
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import apps.amine.bou.readerforselfoss.utils.Config
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class IntroActivityEspressoTest {
@Rule @JvmField
val rule = ActivityTestRule(IntroActivity::class.java, true, false)
@Before
fun clearData() {
val editor =
getInstrumentation().targetContext
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
.edit()
editor.clear()
editor.commit()
Intents.init()
}
@Test
fun nextEachTimes() {
rule.launchActivity(Intent())
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
intended(hasComponent(IntroActivity::class.java.name), times(1))
intended(hasComponent(LoginActivity::class.java.name), times(1))
}
@Test
fun nextBackRandomTimes() {
val max = 5
val min = 1
val random = (Random().nextInt(max + 1 - min)) + min
rule.launchActivity(Intent())
onView(withText(R.string.intro_hello_title)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
repeat(random) { _ ->
onView(withText(R.string.intro_needs_selfoss_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_back)).perform(click())
}
onView(withId(R.id.button_next)).perform(click())
onView(withText(R.string.intro_all_set_message)).check(matches(isDisplayed()))
onView(withId(R.id.button_next)).perform(click())
intended(hasComponent(IntroActivity::class.java.name), times(1))
intended(hasComponent(LoginActivity::class.java.name), times(1))
}
@After
fun releaseIntents() {
Intents.release()
}
}

View File

@ -2,25 +2,25 @@ package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.support.test.InstrumentationRegistry
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.action.ViewActions.closeSoftKeyboard
import android.support.test.espresso.action.ViewActions.pressBack
import android.support.test.espresso.action.ViewActions.typeText
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.intent.Intents
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.times
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
import android.support.test.espresso.matcher.ViewMatchers
import android.support.test.espresso.matcher.ViewMatchers.isRoot
import android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import apps.amine.bou.readerforselfoss.utils.Config
import com.mikepenz.aboutlibraries.ui.LibsActivity
import org.junit.After

View File

@ -3,13 +3,13 @@ package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.support.test.InstrumentationRegistry.getInstrumentation
import android.support.test.espresso.intent.Intents
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.times
import android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import androidx.test.InstrumentationRegistry.getInstrumentation
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import org.junit.After
import org.junit.Before
@ -45,7 +45,6 @@ class MainActivityEspressoTest {
rule.launchActivity(intent)
intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(IntroActivity::class.java.name))
intended(hasComponent(LoginActivity::class.java.name), times(0))
}
@ -58,7 +57,6 @@ class MainActivityEspressoTest {
intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(LoginActivity::class.java.name))
intended(hasComponent(IntroActivity::class.java.name), times(0))
}
@After

View File

@ -1,7 +1,7 @@
package apps.amine.bou.readerforselfoss
import android.support.design.widget.TextInputLayout
import android.support.test.espresso.matcher.ViewMatchers
import com.google.android.material.textfield.TextInputLayout
import androidx.test.espresso.matcher.ViewMatchers
import android.view.View
import org.hamcrest.Description
import org.hamcrest.Matcher

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apps.amine.bou.readerforselfoss"
xmlns:tools="http://schemas.android.com/tools">
package="apps.amine.bou.readerforselfoss">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -11,19 +11,20 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar">
<activity
android:name=".MainActivity"
android:theme="@style/SplashTheme">
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".IntroActivity"
android:theme="@style/Theme.Intro">
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".LoginActivity"
@ -37,7 +38,7 @@
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
android:value=".HomeActivity" />
</activity>
<activity
android:name=".SourcesActivity"
@ -48,22 +49,24 @@
</activity>
<activity
android:name=".AddSourceActivity"
android:parentActivityName=".SourcesActivity">
android:parentActivityName=".SourcesActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".ReaderActivity">
</activity>
<activity
android:name=".ImageActivity">
</activity>
<meta-data
android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
@ -76,6 +79,9 @@
android:value="true" />
<meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>

View File

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

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

@ -1,70 +0,0 @@
package apps.amine.bou.readerforselfoss
import agency.tango.materialintroscreen.MaterialIntroActivity
import agency.tango.materialintroscreen.MessageButtonBehaviour
import agency.tango.materialintroscreen.SlideFragmentBuilder
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate
import android.view.View
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimary)
.buttonsColor(R.color.colorAccent)
.image(R.drawable.web_hi_res_512)
.title(getString(R.string.intro_hello_title))
.description(getString(R.string.intro_hello_message))
.build()
)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorAccent)
.buttonsColor(R.color.colorPrimary)
.image(R.drawable.ic_info_outline_white_48px)
.title(getString(R.string.intro_needs_selfoss_title))
.description(getString(R.string.intro_needs_selfoss_message))
.build(),
MessageButtonBehaviour(
View.OnClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://selfoss.aditu.de")
)
startActivity(browserIntent)
}, getString(R.string.intro_needs_selfoss_link)
)
)
addSlide(
SlideFragmentBuilder()
.backgroundColor(R.color.colorPrimaryDark)
.buttonsColor(R.color.colorAccentDark)
.image(R.drawable.ic_thumb_up_white_48px)
.title(getString(R.string.intro_all_set_title))
.description(getString(R.string.intro_all_set_message))
.build()
)
}
override fun onFinish() {
super.onFinish()
val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext)
val e = getPrefs.edit()
e.putBoolean("firstStart", false)
e.apply()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -6,8 +6,8 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
@ -17,14 +17,13 @@ import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityLoginBinding
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.activity_login.*
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -39,28 +38,28 @@ class LoginActivity : AppCompatActivity() {
private lateinit var settings: SharedPreferences
private lateinit var editor: SharedPreferences.Editor
private lateinit var userIdentifier: String
private var logErrors: Boolean = false
private lateinit var appColors: AppColors
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@LoginActivity)
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_login)
setContentView(view)
setSupportActionBar(toolbar)
setSupportActionBar(binding.toolbar)
handleBaseUrlFail()
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
userIdentifier = settings.getString("unique_id", "")
logErrors = settings.getBoolean("login_debug", false)
userIdentifier = settings.getString("unique_id", "")!!
editor = settings.edit()
if (settings.getString("url", "").isNotEmpty()) {
if (settings.getString("url", "")!!.isNotEmpty()) {
goToMain()
}
@ -69,14 +68,14 @@ class LoginActivity : AppCompatActivity() {
private fun handleActions() {
withSelfhostedCert.setOnCheckedChangeListener { _, b ->
binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
isWithSelfSignedCert = !isWithSelfSignedCert
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, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
attemptLogin()
@ -86,22 +85,22 @@ class LoginActivity : AppCompatActivity() {
}
)
signInButton.setOnClickListener { attemptLogin() }
binding.signInButton.setOnClickListener { attemptLogin() }
withLogin.setOnCheckedChangeListener { _, b ->
binding.withLogin.setOnCheckedChangeListener { _, b ->
isWithLogin = !isWithLogin
val visi: Int = if (b) View.VISIBLE else View.GONE
loginLayout.visibility = visi
passwordLayout.visibility = visi
binding.loginLayout.visibility = visi
binding.passwordLayout.visibility = visi
}
withHttpLogin.setOnCheckedChangeListener { _, b ->
binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
isWithHTTPLogin = !isWithHTTPLogin
val visi: Int = if (b) View.VISIBLE else View.GONE
httpLoginInput.visibility = visi
httpPasswordInput.visibility = visi
binding.httpLoginInput.visibility = visi
binding.httpPasswordInput.visibility = visi
}
}
@ -128,25 +127,25 @@ class LoginActivity : AppCompatActivity() {
private fun attemptLogin() {
// Reset errors.
urlView.error = null
loginView.error = null
httpLoginView.error = null
passwordView.error = null
httpPasswordView.error = null
binding.urlView.error = null
binding.loginView.error = null
binding.httpLoginView.error = null
binding.passwordView.error = null
binding.httpPasswordView.error = null
// Store values at the time of the login attempt.
val url = urlView.text.toString()
val login = loginView.text.toString()
val httpLogin = httpLoginView.text.toString()
val password = passwordView.text.toString()
val httpPassword = httpPasswordView.text.toString()
val url = binding.urlView.text.toString()
val login = binding.loginView.text.toString()
val httpLogin = binding.httpLoginView.text.toString()
val password = binding.passwordView.text.toString()
val httpPassword = binding.httpPasswordView.text.toString()
var cancel = false
var focusView: View? = null
if (!url.isBaseUrlValid()) {
urlView.error = getString(R.string.login_url_problem)
focusView = urlView
if (!url.isBaseUrlValid(this@LoginActivity)) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
cancel = true
inValidCount++
if (inValidCount == 3) {
@ -163,16 +162,30 @@ class LoginActivity : AppCompatActivity() {
}
}
if (isWithLogin || isWithHTTPLogin) {
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
passwordView.error = getString(R.string.error_invalid_password)
focusView = passwordView
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
loginView.error = getString(R.string.error_field_required)
focusView = loginView
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
cancel = true
}
}
if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) {
binding.httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = binding.httpPasswordView
cancel = true
}
if (TextUtils.isEmpty(httpLogin)) {
binding.httpLoginView.error = getString(R.string.error_field_required)
focusView = binding.httpLoginView
cancel = true
}
}
@ -194,75 +207,72 @@ class LoginActivity : AppCompatActivity() {
this,
this@LoginActivity,
isWithSelfSignedCert,
isWithSelfSignedCert
-1L
)
api.login().enqueue(object : Callback<SuccessResponse> {
private fun preferenceError(t: Throwable) {
editor.remove("url")
editor.remove("login")
editor.remove("httpUserName")
editor.remove("password")
editor.remove("httpPassword")
editor.apply()
urlView.error = getString(R.string.wrong_infos)
loginView.error = getString(R.string.wrong_infos)
passwordView.error = getString(R.string.wrong_infos)
httpLoginView.error = getString(R.string.wrong_infos)
httpPasswordView.error = getString(R.string.wrong_infos)
if (logErrors) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity)
Toast.makeText(
this@LoginActivity,
t.message,
Toast.LENGTH_LONG
).show()
}
showProgress(false)
}
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
goToMain()
} else {
preferenceError(Exception("No response body..."))
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
api.login().enqueue(object : Callback<SuccessResponse> {
private fun preferenceError(t: Throwable) {
editor.remove("url")
editor.remove("login")
editor.remove("httpUserName")
editor.remove("password")
editor.remove("httpPassword")
editor.apply()
binding.urlView.error = getString(R.string.wrong_infos)
binding.loginView.error = getString(R.string.wrong_infos)
binding.passwordView.error = getString(R.string.wrong_infos)
binding.httpLoginView.error = getString(R.string.wrong_infos)
binding.httpPasswordView.error = getString(R.string.wrong_infos)
showProgress(false)
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
preferenceError(t)
}
})
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
goToMain()
} else {
preferenceError(Exception("No response body..."))
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
preferenceError(t)
}
})
} else {
showProgress(false)
}
}
}
private fun showProgress(show: Boolean) {
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
loginForm.visibility = if (show) View.GONE else View.VISIBLE
loginForm
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
binding.loginForm
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() {
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
loginProgress
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
binding.loginProgress
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
}
}
)
@ -270,29 +280,19 @@ class LoginActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.login_menu, menu)
menu.findItem(R.id.login_debug).isChecked = logErrors
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
return when (item.itemId) {
R.id.about -> {
LibsBuilder()
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(this)
return true
true
}
R.id.login_debug -> {
val newState = !item.isChecked
item.isChecked = newState
logErrors = newState
editor.putBoolean("login_debug", newState)
editor.apply()
return true
}
else -> return super.onOptionsItemSelected(item)
else -> super.onOptionsItemSelected(item)
}
}
}

View File

@ -3,25 +3,23 @@ package apps.amine.bou.readerforselfoss
import android.content.Intent
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
import apps.amine.bou.readerforselfoss.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean(
"firstStart",
true
)) {
val i = Intent(this@MainActivity, IntroActivity::class.java)
startActivity(i)
} else {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

View File

@ -1,58 +1,32 @@
package apps.amine.bou.readerforselfoss
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.preference.PreferenceManager
import android.support.multidex.MultiDexApplication
import android.widget.ImageView
import androidx.multidex.MultiDexApplication
import apps.amine.bou.readerforselfoss.utils.Config
import com.anupcowkur.reservoir.Reservoir
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.github.stkent.amplify.feedback.DefaultEmailFeedbackCollector
import com.github.stkent.amplify.feedback.GooglePlayStoreFeedbackCollector
import com.github.stkent.amplify.tracking.Amplify
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import org.acra.ACRA
import org.acra.ReportField
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraDialog
import org.acra.annotation.AcraHttpSender
import org.acra.sender.HttpSender
import java.io.IOException
import java.util.UUID.randomUUID
@AcraHttpSender(uri = "http://amine-bou.fr:5984/acra-selfoss/_design/acra-storage/_update/report",
basicAuthLogin = "selfoss",
basicAuthPassword = "selfoss",
httpMethod = HttpSender.Method.PUT)
@AcraDialog(resText = R.string.crash_dialog_text,
resCommentPrompt = R.string.crash_dialog_comment,
resTheme = android.R.style.Theme_DeviceDefault_Dialog)
@AcraCore(reportContent = [ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
buildConfigClass = BuildConfig::class)
class MyApp : MultiDexApplication() {
private lateinit var config: Config
override fun onCreate() {
super.onCreate()
initAmplify()
initCache()
config = Config(baseContext)
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "").isEmpty()) {
if (prefs.getString("unique_id", "")!!.isEmpty()) {
val editor = prefs.edit()
editor.putString("unique_id", randomUUID().toString())
editor.apply()
@ -63,50 +37,41 @@ class MyApp : MultiDexApplication() {
initTheme()
tryToHandleBug()
handleNotificationChannels()
}
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 handleNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
private fun initAmplify() {
Amplify.initSharedInstance(this)
.setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
.setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
.applyAllDefaultRules()
}
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
private fun initCache() {
try {
Reservoir.init(this, 8192) //in bytes
} catch (e: IOException) {
//failure
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
}
}
private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(
imageView: ImageView?,
uri: Uri?,
placeholder: Drawable?,
tag: String?
) {
Glide.with(imageView?.context)
.load(uri)
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(imageView.context)
.loadMaybeBasicAuth(config, uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView)
}
override fun cancel(imageView: ImageView?) {
Glide.with(imageView?.context).clear(imageView)
override fun cancel(imageView: 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)
}
})

View File

@ -1,43 +1,43 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.res.Resources
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewPager
import android.support.v7.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.core.content.ContextCompat
import androidx.viewpager.widget.ViewPager
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.Toast
import androidx.room.Room
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
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_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_reader.*
import me.relex.circleindicator.CircleIndicator
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ReaderActivity : AppCompatActivity() {
private var markOnScroll: Boolean = false
private var debugReadingItems: Boolean = false
private var currentItem: Int = 0
private lateinit var userIdentifier: String
@ -45,9 +45,20 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1
val JUSTIFY = 1
val ALIGN_LEFT = 2
private fun showMenuItem(willAddToFavorite: Boolean) {
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite
if (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() {
@ -58,120 +69,109 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false)
}
private lateinit var editor: SharedPreferences.Editor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_reader)
setContentView(view)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(toolBar)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
debugReadingItems = sharedPref.getBoolean("read_debug", false)
userIdentifier = sharedPref.getString("unique_id", "")
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit()
if (allItems.isEmpty()) {
finish()
}
userIdentifier = prefs.getString("unique_id", "")!!
markOnScroll = prefs.getBoolean("mark_on_scroll", false)
activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi(
this,
this@ReaderActivity,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getBoolean("should_log_everything", false)
prefs.getString("api_timeout", "-1")!!.toLong()
)
if (allItems.isEmpty()) {
finish()
}
currentItem = intent.getIntExtra("currentItem", 0)
pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem
readItem(allItems[currentItem])
binding.pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
binding.pager.currentItem = currentItem
}
override fun onResume() {
super.onResume()
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
notifyAdapter()
pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager)
binding.pager.setPageTransformer(true, DepthPageTransformer())
(binding.indicator as CircleIndicator).setViewPager(binding.pager)
pager.addOnPageChangeListener(
binding.pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
var isLastItem = false
override fun onPageSelected(position: Int) {
isLastItem = (position === (allItems.size - 1))
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
}
override fun onPageScrollStateChanged(state: Int) {
if (markOnScroll && (state === ViewPager.SCROLL_STATE_DRAGGING || (state === ViewPager.SCROLL_STATE_IDLE && isLastItem))) {
api.markItem(allItems[pager.currentItem].id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), this@ReaderActivity)
}
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@ReaderActivity)
}
}
}
)
}
readItem(allItems[position])
}
}
)
}
private fun readItem(item: Item) {
if (markOnScroll) {
SharedItems.readItem(applicationContext, api, db, item)
}
}
private fun notifyAdapter() {
(binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
if (markOnScroll) {
pager.clearOnPageChangeListeners()
binding.pager.clearOnPageChangeListeners()
}
}
override fun onSaveInstanceState(oldInstanceState: Bundle?) {
override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState)
oldInstanceState!!.clear()
oldInstanceState.clear()
}
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
FragmentStatePagerAdapter(fm) {
override fun getCount(): Int {
return allItems.size
}
@ -183,10 +183,20 @@ class ReaderActivity : AppCompatActivity() {
override fun startUpdate(container: ViewGroup) {
super.startUpdate(container)
container.background = ColorDrawable(ContextCompat.getColor(this@ReaderActivity, appColors.colorBackground))
container.background = ColorDrawable(
ContextCompat.getColor(
this@ReaderActivity,
appColors.colorBackground
)
)
}
}
fun alignmentMenu(showJustify: Boolean) {
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.reader_menu, menu)
@ -197,66 +207,76 @@ class ReaderActivity : AppCompatActivity() {
} else {
canFavorite()
}
if (activeAlignment == JUSTIFY) {
alignmentMenu(false)
} else {
alignmentMenu(true)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
notifyAdapter()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
notifyAdapter()
canFavorite()
}
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.save -> {
api.starrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
canRemoveFromFavorite()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
SharedItems.unstarItem(
this@ReaderActivity,
api,
db,
allItems[binding.pager.currentItem]
)
afterUnsave()
} else {
SharedItems.starItem(
this@ReaderActivity,
api,
db,
allItems[binding.pager.currentItem]
)
afterSave()
}
}
R.id.unsave -> {
api.unstarrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
canFavorite()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
R.id.align_left -> {
editor.putInt("text_align", ALIGN_LEFT)
editor.apply()
alignmentMenu(true)
refreshFragment()
}
R.id.align_justify -> {
editor.putInt("text_align", JUSTIFY)
editor.apply()
alignmentMenu(false)
refreshFragment()
}
}
return super.onOptionsItemSelected(item)
}
private fun refreshFragment() {
finish()
overridePendingTransition(0, 0)
startActivity(intent)
overridePendingTransition(0, 0)
}
companion object {
var allItems: ArrayList<Item> = ArrayList()
}

View File

@ -1,20 +1,24 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -22,80 +26,87 @@ import retrofit2.Response
class SourcesActivity : AppCompatActivity() {
private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity)
super.onCreate(savedInstanceState)
binding = ActivitySourcesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_sources)
setContentView(view)
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)
}
setSupportActionBar(toolbar)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
fab.rippleColor = appColors.colorAccentDark
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
binding.fab.rippleColor = appColors.colorAccentDark
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
}
override fun onStop() {
super.onStop()
recyclerView.clearOnScrollListeners()
binding.recyclerView.clearOnScrollListeners()
}
override fun onResume() {
super.onResume()
val mLayoutManager = LinearLayoutManager(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val api = SelfossApi(
this,
this@SourcesActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getBoolean("should_log_everything", false)
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
var items: ArrayList<Sources> = ArrayList()
var items: ArrayList<Source> = ArrayList()
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
api.sources.enqueue(object : Callback<List<Sources>> {
override fun onResponse(
call: Call<List<Sources>>,
response: Response<List<Sources>>
) {
if (response.body() != null && response.body()!!.isNotEmpty()) {
items = response.body() as ArrayList<Sources>
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
api.sources.enqueue(object : Callback<List<Source>> {
override fun onResponse(
call: Call<List<Source>>,
response: Response<List<Source>>
) {
if (response.body() != null && response.body()!!.isNotEmpty()) {
items = response.body() as ArrayList<Source>
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
override fun onFailure(call: Call<List<Source>>, t: Throwable) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
}
}
})
}
override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
}
})
fab.setOnClickListener {
binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
}
}

View File

@ -2,23 +2,26 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.content.Context
import android.support.v7.widget.CardView
import android.support.v7.widget.RecyclerView
import android.text.Html
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.api.selfoss.Item
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.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
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.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
@ -27,24 +30,19 @@ import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.card_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ItemCardAdapter(
override val app: Activity,
override var items: ArrayList<Item>,
override val api: SelfossApi,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
private val fullHeightCards: Boolean,
override val appColors: AppColors,
override val debugReadingItems: Boolean,
override val userIdentifier: String,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
@ -53,130 +51,108 @@ class ItemCardAdapter(
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
return ViewHolder(v)
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
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
holder.mView.title.text = Html.fromHtml(itm.title)
binding.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) {
holder.mView.itemImage.maxHeight = imageMaxHeight
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
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 {
return items.size
}
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
mView.setCardBackgroundColor(appColors.cardBackgroundColor)
binding.root.setCardBackgroundColor(appColors.cardBackgroundColor)
handleClickListeners()
handleCustomTabActions()
}
private fun handleClickListeners() {
mView.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
if (isNetworkAvailable(c)) {
if (item.starred) {
SharedItems.unstarItem(c, api, db, item)
item.starred = false
binding.favButton.isSelected = false
} else {
SharedItems.starItem(c, api, db, item)
item.starred = true
binding.favButton.isSelected = true
}
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
}
})
mView.shareBtn.setOnClickListener {
c.shareLink(items[adapterPosition].getLinkDecoded())
}
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
mView.setOnClickListener {
binding.root.setOnClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,

View File

@ -2,238 +2,115 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.content.Context
import android.support.constraint.ConstraintLayout
import android.support.v7.widget.RecyclerView
import android.text.Html
import android.util.TypedValue
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import androidx.core.content.ContextCompat
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.databinding.ListItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
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.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.list_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.collections.ArrayList
class ItemListAdapter(
override val app: Activity,
override var items: ArrayList<Item>,
override val api: SelfossApi,
override val db: AppDatabase,
private val helper: CustomTabActivityHelper,
private val clickBehavior: Boolean,
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
override val debugReadingItems: Boolean,
override val userIdentifier: String,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext
private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(
R.layout.list_item,
parent,
false
) as ConstraintLayout
return ViewHolder(v)
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
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.setLinkTextColor(appColors.colorAccent)
binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
binding.title.setOnTouchListener(LinkOnTouchListener())
if (itm.getThumbnail(c).isEmpty()) {
val sizeInInt = 46
val sizeInDp = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
.displayMetrics
).toInt()
binding.title.setLinkTextColor(appColors.colorAccent)
val marginInInt = 16
val marginInDp = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
.displayMetrics
).toInt()
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
val params = holder.mView.itemImage.layoutParams as ViewGroup.MarginLayoutParams
params.height = sizeInDp
params.width = sizeInDp
params.setMargins(marginInDp, 0, 0, 0)
holder.mView.itemImage.layoutParams = params
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.sourcetitle)
if (itm.getThumbnail(c).isEmpty()) {
val drawable =
TextDrawable
.builder()
.round()
.build(itm.sourcetitle.toTextDrawableString(c), color)
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
holder.mView.itemImage.setImageDrawable(drawable)
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
}
} 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)
}
// TODO: maybe handle this differently. It crashes when changing tab
try {
if (bars[position]) {
holder.mView.actionBar.visibility = View.VISIBLE
} else {
holder.mView.actionBar.visibility = View.GONE
}
} catch (e: IndexOutOfBoundsException) {
holder.mView.actionBar.visibility = View.GONE
}
holder.mView.favButton.isLiked = itm.starred
}
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 {
handleClickListeners()
handleCustomTabActions()
}
private fun handleClickListeners() {
mView.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
}
})
mView.shareBtn.setOnClickListener {
c.shareLink(items[adapterPosition].getLinkDecoded())
}
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
}
}
private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
if (!clickBehavior) {
mView.setOnClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
}
mView.setOnLongClickListener {
actionBarShowHide()
true
}
} else {
mView.setOnClickListener { actionBarShowHide() }
mView.setOnLongClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
true
}
}
}
private fun actionBarShowHide() {
bars[adapterPosition] = true
if (mView.actionBar.visibility == View.GONE) {
mView.actionBar.visibility = View.VISIBLE
} else {
mView.actionBar.visibility = View.GONE
binding.root.setOnClickListener {
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
}
}
}

View File

@ -2,38 +2,34 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.graphics.Color
import android.support.design.widget.Snackbar
import android.support.v7.widget.RecyclerView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
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.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.succeeded
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import com.google.android.material.snackbar.Snackbar
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item>
abstract val api: SelfossApi
abstract val debugReadingItems: Boolean
abstract val db: AppDatabase
abstract val userIdentifier: String
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) {
items = newItems
fun updateAllItems() {
items = SharedItems.focusedItems
notifyDataSetChanged()
updateItems(items)
}
private fun doUnmark(i: Item, position: Int) {
private fun unmarkSnackbar(i: Item, position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
@ -41,77 +37,69 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG
)
.setAction(R.string.undo_string) {
items.add(position, i)
notifyItemInserted(position)
updateItems(items)
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
}
})
SharedItems.unreadItem(app, api, db, i)
if (SharedItems.displayedItems == "unread") {
addItemAtIndex(i, position)
} else {
notifyItemChanged(position)
}
}
val view = s.view
val tv: TextView = view.findViewById(android.support.design.R.id.snackbar_text)
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
}
fun removeItemAtIndex(position: Int) {
private fun markSnackbar(position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
R.string.marked_as_unread,
Snackbar.LENGTH_LONG
)
.setAction(R.string.undo_string) {
SharedItems.readItem(app, api, db, items[position])
items = SharedItems.focusedItems
if (SharedItems.displayedItems == "unread") {
notifyItemRemoved(position)
updateItems(items)
} else {
notifyItemChanged(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)
}
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
}
doUnmark(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
}
})
private fun unreadItemAtIndex(position: Int) {
SharedItems.unreadItem(app, api, db, items[position])
notifyItemChanged(position)
markSnackbar(position)
}
fun addItemAtIndex(item: Item, position: Int) {

View File

@ -2,59 +2,60 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.content.Context
import android.support.constraint.ConstraintLayout
import android.support.v7.widget.RecyclerView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
import apps.amine.bou.readerforselfoss.api.selfoss.Source
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.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlinx.android.synthetic.main.source_list_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SourcesListAdapter(
private val app: Activity,
private val items: ArrayList<Sources>,
private val items: ArrayList<Source>,
private val api: SelfossApi
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
private lateinit var binding: SourceListItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(
R.layout.source_list_item,
parent,
false
) as ConstraintLayout
return ViewHolder(v)
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position]
config = Config(c)
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.title)
val color = generator.getColor(itm.getTitleDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.title.toTextDrawableString(c), color)
holder.mView.itemImage.setImageDrawable(drawable)
.build(itm.getTitleDecoded().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable)
} 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
@ -70,33 +71,35 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
val (id) = items[adapterPosition]
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
if (c.isNetworkAccessible(null)) {
val (id) = items[adapterPosition]
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
})
})
}
}
}
}

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss
import android.app.Activity
import android.content.Context
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
@ -12,18 +13,22 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator
import com.burgstaller.okhttp.digest.Credentials
import com.burgstaller.okhttp.digest.DigestAuthenticator
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 retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
class SelfossApi(
c: Context,
callingActivity: Activity,
callingActivity: Activity?,
isWithSelfSignedCert: Boolean,
shouldLog: Boolean
timeout: Long
) {
private lateinit var service: SelfossService
@ -38,19 +43,39 @@ class SelfossApi(
this
}
fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
if (timeout != -1L) {
this.readTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
} else {
this
}
fun Credentials.createAuthenticator(): DispatchingAuthenticator =
DispatchingAuthenticator.Builder()
.with("digest", DigestAuthenticator(this))
.with("basic", BasicAuthenticator(this))
.build()
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean): OkHttpClient.Builder {
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
return OkHttpClient
.Builder()
.maybeWithSettingsTimeout(timeout)
.maybeWithSelfSigned(isWithSelfSignedCert)
.authenticator(CachingAuthenticatorDecorator(this, authCache))
.addInterceptor(AuthenticationCacheInterceptor(authCache))
.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 {
@ -66,20 +91,40 @@ class SelfossApi(
val gson =
GsonBuilder()
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
.setLenient()
.create()
val logging = HttpLoggingInterceptor()
logging.level = if (shouldLog) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert)
logging.level = HttpLoggingInterceptor.Level.NONE
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 {
val retrofit =
@ -91,49 +136,59 @@ class SelfossApi(
.build()
service = retrofit.create(SelfossService::class.java)
} catch (e: IllegalArgumentException) {
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
if (callingActivity != null) {
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
}
}
}
fun login(): Call<SuccessResponse> =
service.loginToSelfoss(config.userLogin, config.userPassword)
fun readItems(
tag: String?,
sourceId: Long?,
search: String?,
suspend fun readItems(
itemsNumber: Int,
offset: Int
): Call<List<Item>> =
getItems("read", tag, sourceId, search, itemsNumber, offset)
): retrofit2.Response<List<Item>> =
getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun newItems(
tag: String?,
sourceId: Long?,
search: String?,
suspend fun newItems(
itemsNumber: Int,
offset: Int
): Call<List<Item>> =
getItems("unread", tag, sourceId, search, itemsNumber, offset)
): retrofit2.Response<List<Item>> =
getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun starredItems(
tag: String?,
sourceId: Long?,
search: String?,
suspend fun starredItems(
itemsNumber: Int,
offset: Int
): Call<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset)
): retrofit2.Response<List<Item>> =
getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
private fun getItems(
fun allItems(): Call<List<Item>> =
service.allItems(userName, password)
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,
tag: String?,
sourceId: Long?,
search: String?,
items: Int,
offset: Int
): Call<List<Item>> =
service.getItems(type, tag, sourceId, search, userName, password, items, offset)
): retrofit2.Response<List<Item>> =
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> =
service.markAsRead(itemId, userName, password)
@ -141,7 +196,7 @@ class SelfossApi(
fun unmarkItem(itemId: String): Call<SuccessResponse> =
service.unmarkAsRead(itemId, userName, password)
fun readAll(ids: List<String>): Call<SuccessResponse> =
suspend fun readAll(ids: List<String>): SuccessResponse =
service.markAllAsRead(ids, userName, password)
fun starrItem(itemId: String): Call<SuccessResponse> =
@ -150,8 +205,7 @@ class SelfossApi(
fun unstarrItem(itemId: String): Call<SuccessResponse> =
service.unstarr(itemId, userName, password)
val stats: Call<Stats>
get() = service.stats(userName, password)
suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
val tags: Call<List<Tag>>
get() = service.tags(userName, password)
@ -159,7 +213,10 @@ class SelfossApi(
fun update(): Call<String> =
service.update(userName, password)
val sources: Call<List<Sources>>
val apiVersion: Call<ApiVersion>
get() = service.version()
val sources: Call<List<Source>>
get() = service.sources(userName, password)
fun deleteSource(id: String): Call<SuccessResponse> =
@ -176,4 +233,13 @@ class SelfossApi(
filter: String
): Call<SuccessResponse> =
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,18 +4,24 @@ import android.content.Context
import android.net.Uri
import android.os.Parcel
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.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
private fun constructUrl(config: Config?, path: String, file: String): String {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
private fun constructUrl(config: Config?, path: String, file: String?): String {
return if (file.isEmptyOrNullOrNullString()) {
""
} else {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}
@ -24,7 +30,11 @@ data class Tag(
@SerializedName("tag") val tag: String,
@SerializedName("color") val color: String,
@SerializedName("unread") val unread: Int
)
) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
class SuccessResponse(@SerializedName("success") val success: Boolean) {
val isSuccess: Boolean
@ -42,10 +52,23 @@ data class Spout(
@SerializedName("description") val description: String
)
data class Sources(
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(
@SerializedName("id") val id: String,
@SerializedName("title") val title: String,
@SerializedName("tags") val tags: String,
@SerializedName("tags") val tags: SelfossTagType,
@SerializedName("spout") val spout: String,
@SerializedName("error") val error: String,
@SerializedName("icon") val icon: String
@ -58,6 +81,10 @@ data class Sources(
}
return constructUrl(config, "favicons", icon)
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
}
data class Item(
@ -65,13 +92,13 @@ data class Item(
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") val unread: Boolean,
@SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String,
@SerializedName("icon") val icon: String,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: String
@SerializedName("tags") val tags: SelfossTagType
) : Parcelable {
var config: Config? = null
@ -84,17 +111,17 @@ data class Item(
}
constructor(source: Parcel) : this(
id = source.readString(),
datetime = source.readString(),
title = source.readString(),
content = source.readString(),
id = source.readString().orEmpty(),
datetime = source.readString().orEmpty(),
title = source.readString().orEmpty(),
content = source.readString().orEmpty(),
unread = 0.toByte() != source.readByte(),
starred = 0.toByte() != source.readByte(),
thumbnail = source.readString(),
icon = source.readString(),
link = source.readString(),
sourcetitle = source.readString(),
tags = source.readString()
link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(),
tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("")
)
override fun describeContents() = 0
@ -110,7 +137,7 @@ data class Item(
dest.writeString(icon)
dest.writeString(link)
dest.writeString(sourcetitle)
dest.writeString(tags)
dest.writeParcelable(tags, flags)
}
fun getIcon(app: Context): String {
@ -127,6 +154,51 @@ data class Item(
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
fun getLinkDecoded(): String {
var stringUrl: String
@ -153,4 +225,27 @@ data class Item(
return stringUrl
}
}
data class SelfossTagType(val tags: String) : Parcelable {
companion object {
@JvmField val CREATOR: Parcelable.Creator<SelfossTagType> =
object : Parcelable.Creator<SelfossTagType> {
override fun createFromParcel(source: Parcel): SelfossTagType =
SelfossTagType(source)
override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
tags = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(tags)
}
}

View File

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

@ -0,0 +1,22 @@
package apps.amine.bou.readerforselfoss.api.selfoss
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> {
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): SelfossTagType? =
if (json.isJsonArray) {
SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() })
} else {
SelfossTagType(json.toString())
}
}

View File

@ -0,0 +1,169 @@
package apps.amine.bou.readerforselfoss.background
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.preference.PreferenceManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_LOW
import androidx.room.Room
import androidx.work.Worker
import androidx.work.WorkerParameters
import apps.amine.bou.readerforselfoss.MainActivity
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
lateinit var db: AppDatabase
override fun doWork(): Result {
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()
)
if (isNetworkAvailable(context)) {
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
api.markItem(action.articleId),
action
)
action.unread -> doAndReportOnFail(
api.unmarkItem(action.articleId),
action
)
action.starred -> doAndReportOnFail(
api.starrItem(action.articleId),
action
)
action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId),
action
)
}
}
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)
}
}
}
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
call.enqueue(object : Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>
) {
thread {
db.actionsDao().delete(action)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
}
})
}
}

View File

@ -1,290 +1,405 @@
package apps.amine.bou.readerforselfoss.fragments
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
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.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.customtabs.CustomTabsIntent
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v4.widget.NestedScrollView
import android.support.v7.app.AlertDialog
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import android.view.*
import android.webkit.*
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment
import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.room.Room
import apps.amine.bou.readerforselfoss.ImageActivity
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
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_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.*
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toPx
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.view.*
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException
import java.net.URL
import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList
class ArticleFragment : Fragment() {
private lateinit var pageNumber: Number
private var fontSize: Int = 14
private var fontSize: Int = 16
private lateinit var allItems: ArrayList<Item>
private lateinit var mCustomTabActivityHelper: CustomTabActivityHelper
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
private lateinit var url: String
private lateinit var contentText: String
private lateinit var contentSource: String
private lateinit var contentImage: String
private lateinit var contentTitle: String
private var showMalformedUrl: Boolean = false
private lateinit var allImages : ArrayList<String>
private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private lateinit var prefs: SharedPreferences
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
private var staticBar = false
override fun onStop() {
super.onStop()
mCustomTabActivityHelper.unbindCustomTabsService(activity)
if (mCustomTabActivityHelper != null) {
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(activity!!)
appColors = AppColors(requireActivity())
config = Config(requireActivity())
super.onCreate(savedInstanceState)
pageNumber = arguments!!.getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
pageNumber = requireArguments().getInt(ARG_POSITION)
allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
db = Room.databaseBuilder(
requireContext(),
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
}
private lateinit var rootView: ViewGroup
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rootView = inflater
.inflate(R.layout.fragment_article, container, false) as ViewGroup
): View {
try {
_binding = FragmentArticleBinding.inflate(inflater, container, false)
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].getTitleDecoded()
contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity())
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
allImages = allItems[pageNumber.toInt()].getImages()
fab = rootView.fab
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16")!!.toInt()
staticBar = prefs.getBoolean("reader_static_bar", false)
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
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
}
}
fab.rippleColor = appColors.colorAccentDark
refreshAlignment()
val floatingToolbar: FloatingToolbar = rootView.floatingToolbar
floatingToolbar.attachFab(fab)
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val api = SelfossApi(
requireContext(),
requireActivity(),
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
val customTabsIntent = activity!!.buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper.bindCustomTabsService(activity)
fab = binding.fab
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "14").toInt()
showMalformedUrl = prefs.getBoolean("show_error_malformed_url", true)
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = requireActivity().buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
R.id.share_action -> activity!!.shareLink(url)
R.id.open_action -> activity!!.openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
false,
false,
activity!!
)
else -> Unit
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent)
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
false,
false,
requireActivity()
)
R.id.unread_action -> if (context != null) {
if (allItems[pageNumber.toInt()].unread) {
SharedItems.readItem(
context!!,
api,
db,
allItems[pageNumber.toInt()]
)
allItems[pageNumber.toInt()].unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG
).show()
} else {
SharedItems.unreadItem(
context!!,
api,
db,
allItems[pageNumber.toInt()]
)
allItems[pageNumber.toInt()].unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
}
}
)
override fun onItemLongClick(item: MenuItem?) {
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent)
} else {
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
)
rootView.source.text = contentSource
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide()
} else {
if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
}
)
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
rootView.titleView.text = contentTitle
htmlToWebview(contentText, prefs)
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView.imageView.visibility = View.VISIBLE
Glide
.with(context!!)
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(rootView.imageView)
} else {
rootView.imageView.visibility = View.GONE
}
} catch (e: InflateException) {
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { _, _ ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.apply()
requireActivity().finish()
}
.create()
.show()
}
rootView.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
fab.hide()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
)
return binding.root
}
return rootView
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun refreshAlignment() {
textAlignment = when (prefs.getInt("text_align", 1)) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
}
private fun getContentFromMercury(
customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences
customTabsIntent: CustomTabsIntent
) {
rootView.progressBar.visibility = View.VISIBLE
val parser = MercuryApi(
prefs.getBoolean("should_log_everything", false)
)
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
override fun onResponse(
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
rootView.titleView.text = response.body()!!.title
url = response.body()!!.url
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
htmlToWebview(response.body()!!.content.orEmpty(), prefs)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
rootView.imageView.visibility = View.VISIBLE
try {
Glide
.with(context!!)
.asBitmap()
.load(response.body()!!.lead_image_url)
.apply(RequestOptions.fitCenterTransform())
.into(rootView.imageView)
} catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
override fun onResponse(
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
} else {
rootView.imageView.visibility = View.GONE
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView.nestedScrollView.scrollTo(0, 0)
try {
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
}
rootView.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} catch (e: Exception) {
}
} else {
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) { }
try {
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} catch (e: Exception) { }
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) { }
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} catch (e: Exception) { }
}
}
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
}
)
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing(customTabsIntent)
}
)
}
}
private fun htmlToWebview(c: String, prefs: SharedPreferences) {
private fun htmlToWebview() {
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) {
if (context != null) {
rootView.webcontent.setBackgroundColor(
binding.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
requireContext(),
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 {
Pair(null, null)
}
} else {
if (context != null) {
rootView.webcontent.setBackgroundColor(
binding.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
requireContext(),
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 {
Pair(null, null)
}
@ -302,15 +417,61 @@ class ArticleFragment : Fragment() {
"#FFFFFF"
}
rootView.webcontent.settings.useWideViewPort = true
rootView.webcontent.settings.loadWithOverviewMode = true
rootView.webcontent.settings.javaScriptEnabled = false
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
binding.webcontent.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
return true
}
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.ROOT).contains(".jpg") || url.lowercase(Locale.ROOT).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.lowercase(Locale.ROOT).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.lowercase(Locale.ROOT).contains(".webp")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP
}
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, compressFormat))
}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)}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rootView.webcontent.settings.layoutAlgorithm =
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} else {
rootView.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
binding.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
}
var baseUrl: String? = null
@ -319,91 +480,88 @@ class ArticleFragment : Fragment() {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
if (showMalformedUrl && context != null) {
val alertDialog = AlertDialog.Builder(context!!).create()
alertDialog.setTitle("Error")
alertDialog.setMessage("You are encountering a bug that I can't solve. Can you please contact me to solve the issue, please ?")
alertDialog.setButton(
AlertDialog.BUTTON_POSITIVE,
"Send mail",
{ dialog, _ ->
// This won't be translated because it should only be temporary.
val to = Config.feedbackEmail
val subject= "[ReaderForSelfoss MalformedURLException]"
val body= "Please specify the source, item and spout you are using for the url below : \n ${e.message}"
val mailTo = "mailto:" + to + "?&subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body)
val emailIntent = Intent(Intent.ACTION_VIEW)
emailIntent.data = Uri.parse(mailTo)
startActivity(emailIntent)
dialog.dismiss()
}
)
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"Not now",
{ dialog, _ -> dialog.dismiss() }
)
alertDialog.setButton(
AlertDialog.BUTTON_NEGATIVE,
"Don't show anymore.",
{ dialog, _ ->
editor.putBoolean("show_error_malformed_url", false)
editor.apply()
dialog.dismiss()
}
)
alertDialog.show()
}
}
rootView.webcontent.loadDataWithBaseURL(
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,
"""<style>
|img {
| display: inline-block;
| height: auto;
| width: 100%;
| max-width: 100%;
|}
|a {
| color: $stringColor !important;
|}
|*:not(a) {
| color: $stringTextColor;
|}
|* {
| font-size: ${fontSize.toPx}px;
| text-align: justify;
| word-break: break-word;
| overflow:hidden;
|}
|a, pre, code {
| text-align: left;
|}
|pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: $stringBackgroundColor;
|}</style>$c""".trimMargin(),
"text/html; charset=utf-8",
"""<html>
|<head>
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <style>
| img {
| display: inline-block;
| height: auto;
| width: 100%;
| max-width: 100%;
| }
| a {
| color: $stringColor !important;
| }
| *:not(a) {
| color: $stringTextColor;
| }
| * {
| font-size: ${fontSize}px;
| text-align: $textAlignment;
| word-break: break-word;
| 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 {
| text-align: $textAlignment;
| }
| pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: $stringBackgroundColor;
| }
| </style>
| $fontLinkAndStyle
|</head>
|<body>
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8",
null
)
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
rootView.progressBar.visibility = View.GONE
activity!!.openItemUrl(
binding.progressBar.visibility = View.GONE
requireActivity().openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
true,
false,
activity!!
requireActivity()
)
}
@ -424,5 +582,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

@ -0,0 +1,23 @@
package apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
@Dao
interface ActionsDao {
@Query("SELECT * FROM actions order by id asc")
suspend fun actions(): List<ActionEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllActions(vararg actions: ActionEntity)
@Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
fun deleteReadActionForArticle(article_id: String)
@Delete
fun delete(action: ActionEntity)
}

View File

@ -0,0 +1,36 @@
package apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Delete
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
@Dao
interface DrawerDataDao {
@Query("SELECT * FROM tags")
fun tags(): List<TagEntity>
@Query("SELECT * FROM sources")
fun sources(): List<SourceEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllTags(vararg tags: TagEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllSources(vararg sources: SourceEntity)
@Query("DELETE FROM tags")
fun deleteAllTags()
@Query("DELETE FROM sources")
fun deleteAllSources()
@Delete
fun deleteTag(tag: TagEntity)
@Delete
fun deleteSource(source: SourceEntity)
}

View File

@ -0,0 +1,29 @@
package apps.amine.bou.readerforselfoss.persistence.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import androidx.room.Update
@Dao
interface ItemsDao {
@Query("SELECT * FROM items order by id desc")
suspend fun items(): List<ItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllItems(vararg items: ItemEntity)
@Query("DELETE FROM items")
suspend fun deleteAllItems()
@Delete
suspend fun delete(item: ItemEntity)
@Update
suspend fun updateItem(item: ItemEntity)
}

View File

@ -0,0 +1,20 @@
package apps.amine.bou.readerforselfoss.persistence.database
import androidx.room.RoomDatabase
import androidx.room.Database
import apps.amine.bou.readerforselfoss.persistence.dao.ActionsDao
import apps.amine.bou.readerforselfoss.persistence.dao.DrawerDataDao
import apps.amine.bou.readerforselfoss.persistence.dao.ItemsDao
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
@Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun drawerDataDao(): DrawerDataDao
abstract fun itemsDao(): ItemsDao
abstract fun actionsDao(): ActionsDao
}

View File

@ -0,0 +1,22 @@
package apps.amine.bou.readerforselfoss.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "actions")
data class ActionEntity(
@ColumnInfo(name = "articleid")
val articleId: String,
@ColumnInfo(name = "read")
val read: Boolean,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "unstarred")
var unstarred: Boolean
) {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
}

View File

@ -0,0 +1,33 @@
package apps.amine.bou.readerforselfoss.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey
@ColumnInfo(name = "tag")
val tag: String,
@ColumnInfo(name = "color")
val color: String,
@ColumnInfo(name = "unread")
val unread: Int
)
@Entity(tableName = "sources")
data class SourceEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "tags")
val tags: String,
@ColumnInfo(name = "spout")
val spout: String,
@ColumnInfo(name = "error")
val error: String,
@ColumnInfo(name = "icon")
val icon: String
)

View File

@ -0,0 +1,32 @@
package apps.amine.bou.readerforselfoss.persistence.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String,
@ColumnInfo(name = "datetime")
val datetime: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String,
@ColumnInfo(name = "unread")
val unread: Boolean,
@ColumnInfo(name = "starred")
var starred: Boolean,
@ColumnInfo(name = "thumbnail")
val thumbnail: String?,
@ColumnInfo(name = "icon")
val icon: String?,
@ColumnInfo(name = "link")
val link: String,
@ColumnInfo(name = "sourcetitle")
val sourcetitle: String,
@ColumnInfo(name = "tags")
val tags: String
)

View File

@ -0,0 +1,34 @@
package apps.amine.bou.readerforselfoss.persistence.migrations
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.room.migration.Migration
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
}
}
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

@ -4,13 +4,13 @@ import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.appbar.AppBarLayout;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;

View File

@ -2,8 +2,6 @@ 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;
@ -13,17 +11,14 @@ 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 android.support.v7.app.ActionBar;
import androidx.appcompat.app.ActionBar;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -31,7 +26,6 @@ import android.widget.Toast;
import java.util.List;
import apps.amine.bou.readerforselfoss.BuildConfig;
import apps.amine.bou.readerforselfoss.R;
import apps.amine.bou.readerforselfoss.themes.AppColors;
import apps.amine.bou.readerforselfoss.utils.Config;
@ -53,13 +47,10 @@ 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;
}
private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
preference.setSummary(stringValue);
return true;
};
/**
@ -125,6 +116,27 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
AppColors appColors = new AppColors(this);
if (appColors.isDarkTheme()) {
for (Header header : target) {
tryLoadIconDark(header);
}
}
}
private void tryLoadIconDark(Header header){
try{
if (header.fragmentArguments != null) {
String iconDark = header.fragmentArguments.getString("iconDark");
int iconDarkId = getResources().getIdentifier(iconDark, "drawable", getPackageName());
if (iconDarkId != 0) {
header.iconRes = iconDarkId;
}
}
} catch (Exception e) {
Log.e("SettingsActivity", "Can not load dark icon", e);
}
}
/**
@ -136,7 +148,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|| DebugPreferenceFragment.class.getName().equals(fragmentName)
|| OfflinePreferenceFragment.class.getName().equals(fragmentName)
|| ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
|| LinksPreferenceFragment.class.getName().equals(fragmentName)
|| ThemePreferenceFragment.class.getName().equals(fragmentName);
}
@ -153,46 +166,21 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active");
final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
tabOnTap.setEnabled(!cardViewActive.isChecked());
cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean isEnabled = (Boolean) newValue;
tabOnTap.setEnabled(!isEnabled);
return true;
}
});
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
itemsNumber.getEditText().setFilters(new InputFilter[]{
new InputFilter() {
@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 "";
(source, start, end, dest, dstart, 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)
@ -212,79 +200,21 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
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) {}
public void afterTextChanged(Editable editable) throws NumberFormatException {
fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
}
});
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 "";
}
(source, start, end, dest, dstart, dend) -> {
try {
int input = Integer.parseInt(dest.toString() + source.toString());
if (input > 0)
return null;
} catch (NumberFormatException ignored) {}
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);
}
}
/**
@ -304,39 +234,20 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
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();
findPreference("trackerLink").setOnPreferenceClickListener(preference -> {
openUrl(Uri.parse(Config.trackerUrl));
return true;
}
return super.onOptionsItemSelected(item);
});
findPreference("sourceLink").setOnPreferenceClickListener(preference -> {
openUrl(Uri.parse(Config.sourceUrl));
return false;
});
findPreference("translation").setOnPreferenceClickListener(preference -> {
openUrl(Uri.parse(Config.translationUrl));
return false;
});
}
}
@ -352,10 +263,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@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) {
if (id == R.id.clear) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences.Editor editor = pref.edit();
editor.remove("color_primary");
@ -364,7 +272,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
editor.remove("color_accent_dark");
editor.remove("dark_theme");
editor.apply();
getActivity().finish();
getActivity().recreate();
}
return super.onOptionsItemSelected(item);
}
@ -375,11 +283,32 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
}
@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);
}
}
@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) {
finish();
super.onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -2,13 +2,11 @@ package apps.amine.bou.readerforselfoss.themes
import android.app.Activity
import android.content.Context
import android.os.Build
import android.preference.PreferenceManager
import android.support.annotation.ColorInt
import android.support.v7.view.ContextThemeWrapper
import androidx.annotation.ColorInt
import android.util.TypedValue
import apps.amine.bou.readerforselfoss.R
import android.view.LayoutInflater
import android.view.ViewGroup
class AppColors(a: Activity) {
@ -18,31 +16,55 @@ class AppColors(a: Activity) {
@ColorInt val colorAccentDark: Int
@ColorInt val cardBackgroundColor: Int
@ColorInt val colorBackground: Int
@ColorInt val textColor: Int
val isDarkTheme: Boolean
init {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(a)
colorPrimary =
sharedPref.getInt(
"color_primary",
a.resources.getColor(R.color.colorPrimary)
)
colorPrimaryDark =
sharedPref.getInt(
"color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark)
)
colorAccent =
sharedPref.getInt(
"color_accent",
a.resources.getColor(R.color.colorAccent)
)
colorAccentDark =
sharedPref.getInt(
"color_accent_dark",
a.resources.getColor(R.color.colorAccentDark)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
colorPrimary =
sharedPref.getInt(
"color_primary",
a.resources.getColor(R.color.colorPrimary, a.theme)
)
colorPrimaryDark =
sharedPref.getInt(
"color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark, a.theme)
)
colorAccent =
sharedPref.getInt(
"color_accent",
a.resources.getColor(R.color.colorAccent, a.theme)
)
colorAccentDark =
sharedPref.getInt(
"color_accent_dark",
a.resources.getColor(R.color.colorAccentDark, a.theme)
)
} else {
colorPrimary =
sharedPref.getInt(
"color_primary",
a.resources.getColor(R.color.colorPrimary)
)
colorPrimaryDark =
sharedPref.getInt(
"color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark)
)
colorAccent =
sharedPref.getInt(
"color_accent",
a.resources.getColor(R.color.colorAccent)
)
colorAccentDark =
sharedPref.getInt(
"color_accent_dark",
a.resources.getColor(R.color.colorAccentDark)
)
}
isDarkTheme =
sharedPref.getBoolean(
"dark_theme",
@ -57,8 +79,14 @@ class AppColors(a: Activity) {
android.R.color.background_light
}
textColor = if (isDarkTheme) {
R.color.white
} else {
R.color.grey_900
}
val wrapper = Context::class.java
val method = wrapper!!.getMethod("getThemeResId")
val method = wrapper.getMethod("getThemeResId")
method.isAccessible = true
val typedCardBackground = TypedValue()

View File

@ -1,6 +1,6 @@
package apps.amine.bou.readerforselfoss.transformers
import android.support.v4.view.ViewPager
import androidx.viewpager.widget.ViewPager
import android.view.View
class DepthPageTransformer : ViewPager.PageTransformer {

View File

@ -1,12 +0,0 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.preference.PreferenceManager
import org.acra.ErrorReporter
fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx)
if (sharedPref.getBoolean("acra_should_log", false)) {
this.handleSilentException(throwable)
}
}

View File

@ -13,7 +13,7 @@ fun String.longHash(): Long {
val chars = this.toCharArray()
for (i in 0 until l) {
h = 31 * h + chars[i].toLong()
h = 31 * h + chars[i].code.toLong()
}
return h
}
@ -25,11 +25,12 @@ fun String.toStringUriWithHttp(): String =
this
}
fun Context.shareLink(itemUrl: String) {
fun Context.shareLink(itemUrl: String, itemTitle: String) {
val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.type = "text/plain"
startActivity(
Intent.createChooser(

View File

@ -11,19 +11,19 @@ class Config(c: Context) {
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
val baseUrl: String
get() = settings.getString("url", "")
get() = settings.getString("url", "")!!
val userLogin: String
get() = settings.getString("login", "")
get() = settings.getString("login", "")!!
val userPassword: String
get() = settings.getString("password", "")
get() = settings.getString("password", "")!!
val httpUserLogin: String
get() = settings.getString("httpUserName", "")
get() = settings.getString("httpUserName", "")!!
val httpUserPassword: String
get() = settings.getString("httpPassword", "")
get() = settings.getString("httpPassword", "")!!
companion object {
const val settingsName = "paramsselfoss"
@ -36,6 +36,12 @@ class Config(c: Context) {
const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
const val syncChannelId = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
var dateTimeFormatter = "yyyy-MM-dd HH:mm:ss"
fun logoutAndRedirect(
c: Context,
callingActivity: Activity,

View File

@ -3,7 +3,7 @@ package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.text.format.DateUtils
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import org.acra.ACRA
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
@ -14,7 +14,6 @@ fun String.toTextDrawableString(c: Context): String {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, c)
}
}
return textDrawable.toString()
@ -23,7 +22,7 @@ fun String.toTextDrawableString(c: Context): String {
fun Item.sourceAndDateText(): String {
val formattedDate: String = try {
" " + DateUtils.getRelativeTimeSpanString(
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
SimpleDateFormat(Config.dateTimeFormatter).parse(this.datetime).time,
Date().time,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
@ -33,7 +32,7 @@ fun Item.sourceAndDateText(): String {
""
}
return this.sourcetitle + formattedDate
return this.getSourceTitle() + formattedDate
}
fun Item.toggleStar(): Item {
@ -44,8 +43,8 @@ fun Item.toggleStar(): Item {
fun List<Item>.flattenTags(): List<Item> =
this.flatMap {
val item = it
val tags: List<String> = it.tags.split(",")
tags.map {
item.copy(tags = it.trim())
val tags: List<String> = it.tags.tags.split(",")
tags.map { t ->
item.copy(tags = SelfossTagType(t.trim()))
}
}

View File

@ -2,29 +2,42 @@ package apps.amine.bou.readerforselfoss.utils
import android.app.Activity
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.support.customtabs.CustomTabsIntent
import android.os.Build
import android.text.Spannable
import android.text.style.ClickableSpan
import androidx.browser.customtabs.CustomTabsIntent
import android.util.Patterns
import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.ReaderActivity
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
val actionIntent = Intent(Intent.ACTION_SEND)
actionIntent.type = "text/plain"
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
this,
0,
actionIntent,
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
)
}
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
this,
0,
actionIntent,
pflags
)
val intentBuilder = CustomTabsIntent.Builder()
@ -68,6 +81,7 @@ fun Context.openItemUrlInternally(
) {
if (articleViewer) {
ReaderActivity.allItems = allItems
SharedItems.position = currentItem
val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem)
app.startActivity(intent)
@ -123,17 +137,21 @@ fun Context.openItemUrl(
private fun openInBrowser(linkDecoded: String, app: Activity) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(linkDecoded)
app.startActivity(intent)
try {
app.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
}
}
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(): Boolean {
val baseUrl = HttpUrl.parse(this)
fun String.isBaseUrlValid(ctx: Context): Boolean {
val baseUrl = this.toHttpUrlOrNull()
var existsAndEndsWithSlash = false
if (baseUrl != null) {
val pathSegments = baseUrl.pathSegments()
val pathSegments = baseUrl.pathSegments
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
}
@ -146,3 +164,40 @@ fun Context.openInBrowserAsNewTask(i: Item) {
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
startActivity(intent)
}
class LinkOnTouchListener: View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
var ret = false
val widget: TextView = v as TextView
val text: CharSequence = widget.text
val stext = Spannable.Factory.getInstance().newSpannable(text)
val action = event!!.action
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
var x: Float = event.x
var y: Float = event.y
x -= widget.totalPaddingLeft
y -= widget.totalPaddingTop
x += widget.scrollX
y += widget.scrollY
val layout = widget.layout
val line = layout.getLineForVertical(y.toInt())
val off = layout.getOffsetForHorizontal(line, x)
val link = stext.getSpans(off, off, ClickableSpan::class.java)
if (link.isNotEmpty()) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget)
}
ret = true
}
}
return ret
}
}

View File

@ -1,54 +0,0 @@
package apps.amine.bou.readerforselfoss.utils
import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.util.AttributeSet
import android.view.View
class ScrollAwareFABBehavior(
context: Context,
attrs: AttributeSet
) : CoordinatorLayout.Behavior<FloatingActionButton>() {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
nestedScrollAxes: Int
): Boolean {
return true
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed
)
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
fab!!.visibility = View.INVISIBLE
}
})
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
child.show()
}
}
}

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 { SimpleDateFormat(Config.dateTimeFormatter).parse((it.datetime)) })
items = tmpItems
}
}

View File

@ -4,10 +4,10 @@ package apps.amine.bou.readerforselfoss.utils.customtabs;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsClient;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsServiceConnection;
import android.support.customtabs.CustomTabsSession;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import java.util.List;

View File

@ -7,7 +7,7 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsService;
import android.text.TextUtils;
import android.util.Log;

View File

@ -2,8 +2,8 @@ package apps.amine.bou.readerforselfoss.utils.customtabs;
import android.content.ComponentName;
import android.support.customtabs.CustomTabsClient;
import android.support.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import java.lang.ref.WeakReference;
@ -13,7 +13,7 @@ import java.lang.ref.WeakReference;
*/
public class ServiceConnection extends CustomTabsServiceConnection {
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
private final WeakReference<ServiceConnectionCallback> mConnectionCallback;
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
mConnectionCallback = new WeakReference<>(connectionCallback);

View File

@ -1,7 +1,7 @@
package apps.amine.bou.readerforselfoss.utils.customtabs;
import android.support.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsClient;
public interface ServiceConnectionCallback {

View File

@ -1,7 +1,7 @@
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
package apps.amine.bou.readerforselfoss.utils.drawer
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView

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

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

View File

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

@ -0,0 +1,64 @@
package apps.amine.bou.readerforselfoss.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.view.View
import android.widget.TextView
import apps.amine.bou.readerforselfoss.R
import com.google.android.material.snackbar.Snackbar
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
val networkIsAccessible = isNetworkAvailable(this)
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v
s = Snackbar
.make(
v,
R.string.no_network_connectivity,
Snackbar.LENGTH_INDEFINITE
)
s.setAction(android.R.string.ok) {
snackBarShown = false
s.dismiss()
}
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
snackBarShown = true
}
if (snackBarShown && networkIsAccessible && !overrideOffline) {
s.dismiss()
}
return if(overrideOffline) overrideOffline else networkIsAccessible
}
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

@ -0,0 +1,73 @@
package apps.amine.bou.readerforselfoss.utils.persistence
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.api.selfoss.Tag
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
fun TagEntity.toView(): Tag =
Tag(
this.tag,
this.color,
this.unread
)
fun SourceEntity.toView(): Source =
Source(
this.id,
this.title,
SelfossTagType(this.tags),
this.spout,
this.error,
this.icon
)
fun Source.toEntity(): SourceEntity =
SourceEntity(
this.id,
this.getTitleDecoded(),
this.tags.tags,
this.spout,
this.error,
this.icon.orEmpty()
)
fun Tag.toEntity(): TagEntity =
TagEntity(
this.tag,
this.color,
this.unread
)
fun ItemEntity.toView(): Item =
Item(
this.id,
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
SelfossTagType(this.tags)
)
fun Item.toEntity(): ItemEntity =
ItemEntity(
this.id,
this.datetime,
this.getTitleDecoded(),
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.getSourceTitle(),
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

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
</vector>

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: 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: 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: 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

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