Compare commits

..

184 Commits

Author SHA1 Message Date
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
145 changed files with 8136 additions and 6661 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,23 @@
**1.7.x**
- 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.
**1.6.x**
- Handling hidden tags.
@ -16,6 +36,8 @@
- Versions updates.
- Fixes #215, #208.
**1.5.7.x**
- Added confirmation to the mark as read and update menues.

View File

@ -18,7 +18,11 @@ Also, the last APK built from source is available [here](https://jenkins.amine-b
## 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

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() {
@ -24,6 +24,8 @@ def versionNameFromGit() {
return gitVersion()
}
apply plugin: 'kotlin-kapt'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
@ -36,11 +38,11 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 28
buildToolsVersion '28.0.1'
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16
targetSdkVersion 27
targetSdkVersion 28
versionCode versionCodeFromGit()
versionName versionNameFromGit()
@ -53,7 +55,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 {
@ -84,32 +93,29 @@ android {
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.1.0-beta02'
androidTestImplementation 'androidx.test:runner:1.1.0-beta02'
// 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.1.0-beta02'
// 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.1.0-beta02'
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:$android_version"
implementation "com.google.android.material:material:$android_version"
implementation "androidx.recyclerview:recyclerview:$android_version"
implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation "androidx.vectordrawable:vectordrawable:$android_version"
implementation "androidx.browser:browser:$android_version"
implementation "androidx.cardview:cardview:$android_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
//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.0'
// About
implementation('com.mikepenz:aboutlibraries:6.0.0@aar') {
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
transitive = true
}
@ -120,8 +126,8 @@ dependencies {
implementation 'com.burgstaller:okhttp-digest:1.12'
// 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.0.5'
implementation 'com.github.jd-alexander:LikeButton:0.2.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
// glide
@ -129,25 +135,32 @@ dependencies {
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'
implementation 'com.github.stkent:amplify:2.2.0'
// Drawer
implementation 'co.zsmb:materialdrawer-kt:1.3.5'
implementation 'com.anupcowkur:reservoir:3.1.0'
implementation 'co.zsmb:materialdrawer-kt:2.0.1'
// 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'
implementation 'androidx.core:core-ktx:1.0.0'
// Crash
implementation 'ch.acra:acra-http:5.1.3'
implementation 'ch.acra:acra-dialog:5.1.3'
implementation 'ch.acra:acra-http:5.2.1'
implementation 'ch.acra:acra-dialog:5.2.1'
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}
@ -165,4 +178,4 @@ def initAppLoginPropertiesIfNeeded() {
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

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

@ -3,6 +3,7 @@
package="apps.amine.bou.readerforselfoss"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -11,6 +12,7 @@
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"
@ -20,10 +22,9 @@
<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"

View File

@ -4,8 +4,8 @@ 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
@ -89,6 +89,7 @@ class AddSourceActivity : AppCompatActivity() {
this,
this@AddSourceActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
} catch (e: IllegalArgumentException) {
@ -108,7 +109,7 @@ class AddSourceActivity : AppCompatActivity() {
super.onResume()
val config = Config(this)
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid()) {
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false, this@AddSourceActivity)) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)

View File

@ -9,29 +9,42 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v4.view.MenuItemCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SearchView
import android.support.v7.widget.StaggeredGridLayoutManager
import android.support.v7.widget.helper.ItemTouchHelper
import androidx.core.view.MenuItemCompat
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import apps.amine.bou.readerforselfoss.adapters.ItemCardAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemsAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.Item
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.Stats
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.api.selfoss.Tag
import apps.amine.bou.readerforselfoss.background.LoadingWorker
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.settings.SettingsActivity
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
@ -43,14 +56,14 @@ import apps.amine.bou.readerforselfoss.utils.drawer.CustomUrlPrimaryDrawerItem
import apps.amine.bou.readerforselfoss.utils.flattenTags
import apps.amine.bou.readerforselfoss.utils.longHash
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.persistence.toView
import co.zsmb.materialdrawerkt.builders.accountHeader
import co.zsmb.materialdrawerkt.builders.drawer
import co.zsmb.materialdrawerkt.builders.footer
import co.zsmb.materialdrawerkt.draweritems.badgeable.primaryItem
import co.zsmb.materialdrawerkt.draweritems.profile.profile
import com.anupcowkur.reservoir.Reservoir
import com.anupcowkur.reservoir.ReservoirGetCallback
import com.anupcowkur.reservoir.ReservoirPutCallback
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -66,10 +79,14 @@ import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.fragment_article.*
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private val MENU_PREFERENCES = 12302
@ -84,7 +101,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var items: ArrayList<Item> = ArrayList()
private var allItems: ArrayList<Item> = ArrayList()
private var clickBehavior = false
private var debugReadingItems = false
private var shouldLogEverything = false
private var internalBrowser = false
@ -96,14 +112,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var itemsNumber: Int = 200
private var elementsShown: Int = 0
private var maybeTagFilter: Tag? = null
private var maybeSourceFilter: Sources? = null
private var maybeSourceFilter: Source? = null
private var maybeSearchFilter: String? = null
private var userIdentifier: String = ""
private var displayAccountHeader: Boolean = false
private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false
private var itemsCaching: Boolean = false
private var hiddenTags: List<String> = emptyList()
private var periodicRefresh = false
private var refreshMinutes: Long = 360L
private var refreshWhenChargingOnly = false
private lateinit var tabNewBadge: TextBadgeItem
private lateinit var tabArchiveBadge: TextBadgeItem
@ -116,7 +136,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var appColors: AppColors
private var offset: Int = 0
private var firstVisible: Int = 0
private var recyclerViewScrollListener: RecyclerView.OnScrollListener? = null
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var settings: SharedPreferences
private var recyclerAdapter: RecyclerView.Adapter<*>? = null
@ -125,10 +145,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var badgeAll: Int = -1
private var badgeFavs: Int = -1
private var fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false
private lateinit var tagsBadge: Map<Long, Int>
data class DrawerData(val tags: List<Tag>?, val sources: List<Sources>?)
private lateinit var db: AppDatabase
data class DrawerData(val tags: List<Tag>?, val sources: List<Source>?)
override fun onStart() {
super.onStart()
@ -140,6 +164,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
super.onCreate(savedInstanceState)
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) {
elementsShown = intent.getIntExtra("shortcutTab", UNREAD_SHOWN)
}
setContentView(R.layout.activity_home)
handleThemeBinding()
@ -149,6 +180,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
Amplify.getSharedInstance().promptIfReady(promptView)
}
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
customTabActivityHelper = CustomTabActivityHelper()
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
@ -158,6 +195,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
this@HomeActivity,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
shouldLogEverything
)
items = ArrayList()
@ -169,30 +207,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleSwipeRefreshLayout()
}
private fun handleGDPRDialog(GDPRShown: Boolean) {
val sharedEditor = sharedPref.edit()
if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK",
{ dialog, _ ->
sharedEditor.putBoolean("GDPR_shown", true)
sharedEditor.commit()
dialog.dismiss()
}
)
alertDialog.show()
}
if (sharedPref.getString("acra.user.email", "").isNotEmpty()) {
sharedEditor.remove("acra.user.email")
sharedEditor.commit()
}
}
private fun handleSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1,
@ -200,6 +214,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.color.refresh_progress_3
)
swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
allItems = ArrayList()
lastFetchDone = false
handleDrawerItems()
@ -212,8 +227,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
) {
override fun getSwipeDirs(
recyclerView: RecyclerView?,
viewHolder: RecyclerView.ViewHolder?
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int =
if (elementsShown != UNREAD_SHOWN) {
0
@ -242,44 +257,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
is ItemListAdapter -> adapter.removeItemAtIndex(position)
}
if (items.size > 0) {
badgeNew--
reloadBadgeContent()
badgeNew--
reloadBadgeContent()
val tagHashes = i.tags.split(",").map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
(it.key to (it.value - 1))
} else {
(it.key to it.value)
}
}.toMap()
reloadTagsBadges()
} else {
tabNewBadge.hide()
}
val manager = recyclerView.layoutManager
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
Log.e("TAGTAG", "$lastVisibleItem === ${items.size} && ${items.size} <= ${maxItemNumber()} && ${!lastFetchDone}x")
if (lastVisibleItem === items.size &&
items.size <= maxItemNumber() &&
!lastFetchDone
) {
if (maxItemNumber() <= itemsNumber) {
lastFetchDone = true
val tagHashes = i.tags.tags.split(",").map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
(it.key to (it.value - 1))
} else {
(it.key to it.value)
}
getElementsAccordingToTab(
appendResults = true,
offsetOverride = lastVisibleItem
)
}.toMap()
reloadTagsBadges()
// Just load everythin
if (items.size <= 0) {
getElementsAccordingToTab()
}
} else {
Toast.makeText(
@ -337,6 +330,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
if (fromTabShortcut) {
bottomBar.selectTab(elementsShown - 1)
}
}
override fun onResume() {
@ -368,6 +365,33 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
getElementsAccordingToTab()
handleGDPRDialog(sharedPref.getBoolean("GDPR_shown", false))
handleRecurringTask()
handleOfflineActions()
}
private fun getAndStoreAllItems() {
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
}
}
}
})
}
override fun onStop() {
@ -378,7 +402,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun handleSharedPrefs() {
debugReadingItems = sharedPref.getBoolean("read_debug", false)
shouldLogEverything = sharedPref.getBoolean("should_log_everything", false)
clickBehavior = sharedPref.getBoolean("tab_on_tap", false)
internalBrowser = sharedPref.getBoolean("prefer_internal_browser", true)
articleViewer = sharedPref.getBoolean("prefer_article_viewer", true)
shouldBeCardView = sharedPref.getBoolean("card_view_active", false)
@ -389,17 +412,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
userIdentifier = sharedPref.getString("unique_id", "")
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
itemsCaching = sharedPref.getBoolean("items_caching", false)
hiddenTags = if (sharedPref.getString("hidden_tags", "").isNotEmpty()) {
sharedPref.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",")
} else {
emptyList()
}
periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
refreshWhenChargingOnly = sharedPref.getBoolean("refresh_when_charging", false)
refreshMinutes = sharedPref.getString("periodic_refresh_minutes", "360").toLong()
if (refreshMinutes <= 15) {
refreshMinutes = 15
}
}
private fun handleThemeBinding() {
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
}
@ -409,7 +440,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val scoop = Scoop.getInstance()
scoop.update(Toppings.PRIMARY.value, appColors.colorPrimary)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.update(Toppings.PRIMARY_DARK.value, appColors.colorPrimaryDark)
}
}
@ -533,7 +564,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
}
} else {
val filteredHiddenTags: List<Tag> = maybeTags.filter { hiddenTags.contains(it.tag) }
val filteredHiddenTags: List<Tag> =
maybeTags.filter { hiddenTags.contains(it.tag) }
tagsBadge = filteredHiddenTags.map {
val gd = GradientDrawable()
val color = try {
@ -569,7 +601,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
fun handleSources(maybeSources: List<Sources>?) {
fun handleSources(maybeSources: List<Source>?) {
if (maybeSources == null) {
if (loadedFromCache) {
drawer.addItem(
@ -612,14 +644,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
false
}
)
drawer.addItem(DividerDrawerItem())
drawer.addItem(
SecondaryDrawerItem()
.withName(getString(R.string.drawer_item_tags))
.withIdentifier(DRAWER_ID_TAGS)
.withSelectable(false)
)
handleTags(maybeDrawerData.tags)
if (hiddenTags.isNotEmpty()) {
drawer.addItem(DividerDrawerItem())
drawer.addItem(
@ -631,6 +655,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleHiddenTags(maybeDrawerData.tags)
}
drawer.addItem(DividerDrawerItem())
drawer.addItem(
SecondaryDrawerItem()
.withName(getString(R.string.drawer_item_tags))
.withIdentifier(DRAWER_ID_TAGS)
.withSelectable(false)
)
handleTags(maybeDrawerData.tags)
drawer.addItem(DividerDrawerItem())
drawer.addItem(
SecondaryDrawerItem()
.withName(getString(R.string.drawer_item_sources))
@ -668,14 +700,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (!loadedFromCache) {
Reservoir.putAsync(
"drawerData", maybeDrawerData, object : ReservoirPutCallback {
override fun onSuccess() {
}
override fun onFailure(p0: Exception?) {
}
})
if (maybeDrawerData.tags != null) {
thread {
val tagEntities = maybeDrawerData.tags.map { it.toEntity() }
db.drawerDataDao().insertAllTags(*tagEntities.toTypedArray())
}
}
if (maybeDrawerData.sources != null) {
thread {
val sourceEntities =
maybeDrawerData.sources.map { it.toEntity() }
db.drawerDataDao().insertAllSources(*sourceEntities.toTypedArray())
}
}
}
} else {
if (!loadedFromCache) {
@ -697,39 +734,47 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
fun drawerApiCalls(maybeDrawerData: DrawerData?) {
var tags: List<Tag>? = null
var sources: List<Sources>?
var sources: List<Source>?
fun sourcesApiCall() {
api.sources.enqueue(object : Callback<List<Sources>> {
override fun onResponse(
call: Call<List<Sources>>?,
response: Response<List<Sources>>
) {
sources = response.body()
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.sources.enqueue(object : Callback<List<Source>> {
override fun onResponse(
call: Call<List<Source>>?,
response: Response<List<Source>>
) {
sources = response.body()
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
}
override fun onFailure(call: Call<List<Source>>?, t: Throwable?) {
val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
}
})
}
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.tags.enqueue(object : Callback<List<Tag>> {
override fun onResponse(
call: Call<List<Tag>>,
response: Response<List<Tag>>
) {
tags = response.body()
sourcesApiCall()
}
override fun onFailure(call: Call<List<Sources>>?, t: Throwable?) {
override fun onFailure(call: Call<List<Tag>>?, t: Throwable?) {
sourcesApiCall()
}
})
}
api.tags.enqueue(object : Callback<List<Tag>> {
override fun onResponse(
call: Call<List<Tag>>,
response: Response<List<Tag>>
) {
tags = response.body()
sourcesApiCall()
}
override fun onFailure(call: Call<List<Tag>>?, t: Throwable?) {
sourcesApiCall()
}
})
}
drawer.addItem(
@ -738,18 +783,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
)
val resultType = object : TypeToken<DrawerData>() {}.type
Reservoir.getAsync(
"drawerData", resultType, object : ReservoirGetCallback<DrawerData> {
override fun onSuccess(maybeDrawerData: DrawerData?) {
handleDrawerData(maybeDrawerData, loadedFromCache = true)
drawerApiCalls(maybeDrawerData)
}
override fun onFailure(p0: Exception?) {
drawerApiCalls(null)
}
})
thread {
var drawerData = DrawerData(db.drawerDataDao().tags().map { it.toView() },
db.drawerDataDao().sources().map { it.toView() })
runOnUiThread {
handleDrawerData(drawerData, loadedFromCache = true)
drawerApiCalls(drawerData)
}
}
}
private fun reloadLayoutManager() {
@ -760,7 +801,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
when (currentManager) {
is StaggeredGridLayoutManager ->
if (!shouldBeCardView) {
layoutManager = GridLayoutManager(this, calculateNoOfColumns())
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
recyclerView.layoutManager = layoutManager
}
is GridLayoutManager ->
@ -776,7 +820,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
else ->
if (currentManager == null) {
if (!shouldBeCardView) {
layoutManager = GridLayoutManager(this, calculateNoOfColumns())
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
recyclerView.layoutManager = layoutManager
} else {
layoutManager = StaggeredGridLayoutManager(
@ -819,33 +866,71 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onTabSelected(position: Int) {
offset = 0
lastFetchDone = false
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
if (itemsCaching) {
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (position) {
0 -> ArrayList(dbItems.filter { it.unread })
1 -> ArrayList(dbItems.filter { !it.unread })
2 -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
getAndStoreAllItems()
}
}
}
}
} else {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
}
}
})
}
private fun handleInfiniteScroll() {
if (recyclerViewScrollListener == null) {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView?, dx: Int, dy: Int) {
if (localRecycler != null && dy > 0) {
val manager = recyclerView.layoutManager
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
val manager = recyclerView.layoutManager
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
}
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
}
}
}
@ -868,6 +953,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
appendResults: Boolean = false,
offsetOverride: Int? = null
) {
fun doGetAccordingToTab() {
when (elementsShown) {
UNREAD_SHOWN -> getUnRead(appendResults)
READ_SHOWN -> getRead(appendResults)
FAV_SHOWN -> getStarred(appendResults)
else -> getUnRead(appendResults)
}
}
offset = if (appendResults && offsetOverride === null) {
(offset + itemsNumber)
} else {
@ -875,17 +969,42 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
firstVisible = if (appendResults) firstVisible else 0
when (elementsShown) {
UNREAD_SHOWN -> getUnRead(appendResults)
READ_SHOWN -> getRead(appendResults)
FAV_SHOWN -> getStarred(appendResults)
else -> getUnRead(appendResults)
if (itemsCaching) {
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (elementsShown) {
UNREAD_SHOWN -> ArrayList(dbItems.filter { it.unread })
READ_SHOWN -> ArrayList(dbItems.filter { !it.unread })
FAV_SHOWN -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
doGetAccordingToTab()
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
doGetAccordingToTab()
getAndStoreAllItems()
}
}
}
}
} else {
doGetAccordingToTab()
}
}
private fun filter(tags: String): Boolean {
val tagsList = tags.replace("\\s".toRegex(), "").split(",")
return tagsList.intersect(hiddenTags).isEmpty()
val tagsList = tags.replace("\\s".toRegex(), "").split(",")
return tagsList.intersect(hiddenTags).isEmpty()
}
private fun doCallTo(
@ -894,12 +1013,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
call: (String?, Long?, String?) -> Call<List<Item>>
) {
fun handleItemsResponse(response: Response<List<Item>>) {
val shouldUpdate = (response.body() != items)
val shouldUpdate = (response.body()?.toSet() != items.toSet())
if (response.body() != null) {
if (shouldUpdate) {
getAndStoreAllItems()
items = response.body() as ArrayList<Item>
items = items.filter {
maybeTagFilter != null || filter(it.tags)
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
if (allItems.isEmpty()) {
@ -916,9 +1036,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
allItems = ArrayList()
}
}
if (shouldUpdate) {
handleListResult(appendResults)
}
handleListResult(appendResults)
if (!appendResults) mayBeEmpty()
swipeRefreshLayout.isRefreshing = false
@ -928,24 +1047,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
call(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter)
.enqueue(object : Callback<List<Item>> {
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
handleItemsResponse(response)
}
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
call(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter)
.enqueue(object : Callback<List<Item>> {
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
handleItemsResponse(response)
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
swipeRefreshLayout.isRefreshing = false
Toast.makeText(
this@HomeActivity,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
})
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
swipeRefreshLayout.isRefreshing = false
Toast.makeText(
this@HomeActivity,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
})
} else {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = false }
}
}
private fun getUnRead(appendResults: Boolean = false) {
@ -1006,34 +1129,33 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
items,
api,
db,
customTabActivityHelper,
internalBrowser,
articleViewer,
fullHeightCards,
appColors,
debugReadingItems,
userIdentifier,
{
updateItems(it)
}
)
userIdentifier
) {
updateItems(it)
}
} else {
recyclerAdapter =
ItemListAdapter(
this,
items,
api,
db,
customTabActivityHelper,
clickBehavior,
internalBrowser,
articleViewer,
debugReadingItems,
userIdentifier,
appColors,
{
updateItems(it)
}
)
appColors
) {
updateItems(it)
}
recyclerView.addItemDecoration(
DividerItemDecoration(
@ -1055,7 +1177,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && (displayUnreadCount || displayAllCount)) {
api.stats.enqueue(object : Callback<Stats> {
override fun onResponse(call: Call<Stats>, response: Response<Stats>) {
if (response.body() != null) {
@ -1161,47 +1283,51 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message, {
api.update().enqueue(object : Callback<String> {
override fun onResponse(
call: Call<String>,
response: Response<String>
) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
api.update().enqueue(object : Callback<String> {
override fun onResponse(
call: Call<String>,
response: Response<String>
) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
}
override fun onFailure(call: Call<String>, t: Throwable) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
})
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
})
return true
override fun onFailure(call: Call<String>, t: Throwable) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
})
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
}
return true
} else {
return false
}
}
R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message, {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
swipeRefreshLayout.isRefreshing = false
val ids = allItems.map { it.id }
val itemsByTag: Map<Long, Int> =
allItems.flattenTags()
.groupBy { it.tags.longHash() }
.groupBy { it.tags.tags.longHash() }
.map { it.key to it.value.size }
.toMap()
fun readAllDebug(e: Throwable) {
// TODO: debug
ACRA.getErrorReporter().maybeHandleSilentException(e, this@HomeActivity)
}
if (ids.isNotEmpty()) {
if (ids.isNotEmpty() && this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.readAll(ids).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
@ -1238,7 +1364,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
)
}
}
swipeRefreshLayout.isRefreshing = false
@ -1268,7 +1393,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
).show()
}
handleListResult()
})
}
}
return true
}
@ -1287,8 +1412,79 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
else -> badgeNew // if !elementsShown then unread are fetched.
}
fun updateItems(adapterItems: ArrayList<Item>) {
private fun updateItems(adapterItems: ArrayList<Item>) {
items = adapterItems
}
private fun handleGDPRDialog(GDPRShown: Boolean) {
val sharedEditor = sharedPref.edit()
if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ ->
sharedEditor.putBoolean("GDPR_shown", true)
sharedEditor.commit()
dialog.dismiss()
}
alertDialog.show()
}
}
private fun handleRecurringTask() {
if (periodicRefresh) {
val myConstraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(refreshWhenChargingOnly)
.setRequiresStorageNotLow(true)
.build()
val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(refreshMinutes, TimeUnit.MINUTES)
.setConstraints(myConstraints)
.addTag("selfoss-loading")
.build()
WorkManager.getInstance().enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
}
}
private fun handleOfflineActions() {
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) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@HomeActivity)
}
})
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
thread {
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)
}
}
}
}
}
}

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
@ -21,6 +21,7 @@ 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.*
@ -53,7 +54,6 @@ class LoginActivity : AppCompatActivity() {
handleBaseUrlFail()
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
userIdentifier = settings.getString("unique_id", "")
logErrors = settings.getBoolean("login_debug", false)
@ -144,7 +144,7 @@ class LoginActivity : AppCompatActivity() {
var cancel = false
var focusView: View? = null
if (!url.isBaseUrlValid()) {
if (!url.isBaseUrlValid(logErrors, this@LoginActivity)) {
urlView.error = getString(R.string.login_url_problem)
focusView = urlView
cancel = true
@ -163,7 +163,7 @@ class LoginActivity : AppCompatActivity() {
}
}
if (isWithLogin || isWithHTTPLogin) {
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
passwordView.error = getString(R.string.error_invalid_password)
focusView = passwordView
@ -177,6 +177,20 @@ class LoginActivity : AppCompatActivity() {
}
}
if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) {
httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = httpPasswordView
cancel = true
}
if (TextUtils.isEmpty(httpLogin)) {
httpLoginView.error = getString(R.string.error_field_required)
focusView = httpLoginView
cancel = true
}
}
if (cancel) {
focusView?.requestFocus()
} else {
@ -194,47 +208,53 @@ class LoginActivity : AppCompatActivity() {
this,
this@LoginActivity,
isWithSelfSignedCert,
-1L,
isWithSelfSignedCert
)
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()
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 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)
}
}
}

View File

@ -3,7 +3,7 @@ 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
class MainActivity : AppCompatActivity() {
@ -11,17 +11,9 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
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,14 +1,15 @@
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 androidx.multidex.MultiDexApplication
import android.widget.ImageView
import apps.amine.bou.readerforselfoss.utils.Config
import com.anupcowkur.reservoir.Reservoir
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
@ -27,10 +28,8 @@ 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)
@AcraHttpSender(uri = "http://37.187.110.167/amine/acra/simplest-acra.php",
httpMethod = HttpSender.Method.POST)
@AcraDialog(resText = R.string.crash_dialog_text,
resCommentPrompt = R.string.crash_dialog_comment,
resTheme = android.R.style.Theme_DeviceDefault_Dialog)
@ -49,8 +48,6 @@ class MyApp : MultiDexApplication() {
initAmplify()
initCache()
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "").isEmpty()) {
val editor = prefs.edit()
@ -63,6 +60,25 @@ class MyApp : MultiDexApplication() {
initTheme()
tryToHandleBug()
handleNotificationChannels()
}
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)
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
}
}
override fun attachBaseContext(base: Context?) {
@ -80,14 +96,6 @@ class MyApp : MultiDexApplication() {
.applyAllDefaultRules()
}
private fun initCache() {
try {
Reservoir.init(this, 8192) //in bytes
} catch (e: IOException) {
//failure
}
}
private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(

View File

@ -1,29 +1,35 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.res.Resources
import android.content.SharedPreferences
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.fragment.app.Fragment
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.fragments.ArticleFragment
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.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.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop
@ -33,6 +39,7 @@ import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
class ReaderActivity : AppCompatActivity() {
@ -45,6 +52,13 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
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
@ -58,14 +72,21 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false)
}
private lateinit var editor: SharedPreferences.Editor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
@ -73,89 +94,111 @@ class ReaderActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit()
debugReadingItems = sharedPref.getBoolean("read_debug", false)
userIdentifier = sharedPref.getString("unique_id", "")
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
debugReadingItems = prefs.getBoolean("read_debug", false)
userIdentifier = prefs.getString("unique_id", "")
markOnScroll = prefs.getBoolean("mark_on_scroll", false)
activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi(
this,
this@ReaderActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
if (allItems.isEmpty()) {
finish()
}
api = SelfossApi(
this,
this@ReaderActivity,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getBoolean("should_log_everything", false)
)
currentItem = intent.getIntExtra("currentItem", 0)
pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
readItem(allItems[currentItem])
pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem
}
override fun onResume() {
super.onResume()
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
notifyAdapter()
pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager)
pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
var isLastItem = false
override fun onPageSelected(position: Int) {
isLastItem = (position === (allItems.size - 1))
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
}
override fun onPageScrollStateChanged(state: Int) {
if (markOnScroll && (state === ViewPager.SCROLL_STATE_DRAGGING || (state === ViewPager.SCROLL_STATE_IDLE && isLastItem))) {
api.markItem(allItems[pager.currentItem].id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), this@ReaderActivity)
}
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@ReaderActivity)
}
}
}
)
}
readItem(allItems[pager.currentItem])
}
}
)
}
fun readItem(item: Item) {
if (markOnScroll) {
thread {
db.itemsDao().delete(item.toEntity())
}
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
api.markItem(item.id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter()
.maybeHandleSilentException(Exception(message), this@ReaderActivity)
}
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
thread {
db.itemsDao().insertAllItems(item.toEntity())
}
if (debugReadingItems) {
ACRA.getErrorReporter()
.maybeHandleSilentException(t, this@ReaderActivity)
}
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
}
}
}
}
private fun notifyAdapter() {
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
if (markOnScroll) {
@ -171,7 +214,6 @@ class ReaderActivity : AppCompatActivity() {
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
FragmentStatePagerAdapter(fm) {
override fun getCount(): Int {
return allItems.size
}
@ -183,10 +225,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 +249,115 @@ class ReaderActivity : AppCompatActivity() {
} else {
canFavorite()
}
if (activeAlignment == JUSTIFY) {
alignmentMenu(false)
} else {
alignmentMenu(true)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[pager.currentItem] =
allItems[pager.currentItem].toggleStar()
notifyAdapter()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[pager.currentItem] = allItems[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()
}
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.starrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
afterSave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
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()
}
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.unstarrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
afterUnsave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
afterUnsave()
}
}
}
R.id.align_left -> {
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

@ -5,14 +5,15 @@ 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.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call
@ -59,41 +60,44 @@ class SourcesActivity : AppCompatActivity() {
this,
this@SourcesActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
var items: ArrayList<Sources> = ArrayList()
var items: ArrayList<Source> = ArrayList()
recyclerView.setHasFixedSize(true)
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)
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 {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))

View File

@ -2,8 +2,8 @@ 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 androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import android.text.Html
import android.view.LayoutInflater
import android.view.View
@ -14,11 +14,15 @@ 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.themes.AppColors
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.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
@ -33,11 +37,13 @@ import kotlinx.android.synthetic.main.card_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
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,
@ -63,6 +69,7 @@ class ItemCardAdapter(
holder.mView.favButton.isLiked = itm.starred
holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent)
@ -114,53 +121,66 @@ class ItemCardAdapter(
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>
) {
}
if (c.isNetworkAccessible(null)) {
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 onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false))
}
})
}
}
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>
) {
}
if (c.isNetworkAccessible(null)) {
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()
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
}
})
}
}
})
mView.shareBtn.setOnClickListener {
c.shareLink(items[adapterPosition].getLinkDecoded())
val item = items[adapterPosition]
c.shareLink(item.getLinkDecoded(), item.title)
}
mView.browserBtn.setOnClickListener {

View File

@ -2,19 +2,25 @@ 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.text.Html
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors
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
@ -39,8 +45,8 @@ 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,
@ -50,7 +56,6 @@ class ItemListAdapter(
) : 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(
@ -67,6 +72,8 @@ class ItemListAdapter(
holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent)
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
@ -106,19 +113,6 @@ class ItemListAdapter(
} 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
@ -126,114 +120,23 @@ class ItemListAdapter(
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
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
mView.setOnClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,
app
)
}
}
}

View File

@ -2,25 +2,31 @@ 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 com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item>
abstract val api: SelfossApi
abstract val db: AppDatabase
abstract val debugReadingItems: Boolean
abstract val userIdentifier: String
abstract val app: Activity
@ -42,76 +48,100 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
)
.setAction(R.string.undo_string) {
items.add(position, i)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
notifyItemInserted(position)
updateItems(items)
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
}
})
} else {
thread {
db.actionsDao().deleteReadActionForArticle(i.id)
}
})
}
}
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) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
thread {
db.itemsDao().delete(i.toEntity())
}
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()
if (app.isNetworkAccessible(null)) {
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)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
doUnmark(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
}
})
}
}
fun addItemAtIndex(item: Item, position: Int) {

View File

@ -2,17 +2,18 @@ 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.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
@ -23,7 +24,7 @@ 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
@ -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

@ -18,11 +18,13 @@ import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
class SelfossApi(
c: Context,
callingActivity: Activity,
callingActivity: Activity?,
isWithSelfSignedCert: Boolean,
timeout: Long,
shouldLog: Boolean
) {
@ -38,16 +40,25 @@ 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))
@ -66,6 +77,7 @@ class SelfossApi(
val gson =
GsonBuilder()
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
.setLenient()
.create()
@ -77,7 +89,7 @@ class SelfossApi(
HttpLoggingInterceptor.Level.NONE
}
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert)
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
httpClient.addInterceptor(logging)
@ -91,7 +103,9 @@ 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)
}
}
}
@ -125,6 +139,9 @@ class SelfossApi(
): Call<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset)
fun allItems(): Call<List<Item>> =
service.allItems(userName, password)
private fun getItems(
type: String,
tag: String?,
@ -159,7 +176,7 @@ class SelfossApi(
fun update(): Call<String> =
service.update(userName, password)
val sources: Call<List<Sources>>
val sources: Call<List<Source>>
get() = service.sources(userName, password)
fun deleteSource(id: String): Call<SuccessResponse> =

View File

@ -9,13 +9,13 @@ import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
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()
}
}
@ -42,10 +42,10 @@ data class Spout(
@SerializedName("description") val description: String
)
data class Sources(
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
@ -71,7 +71,7 @@ data class Item(
@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
@ -94,7 +94,7 @@ data class Item(
icon = source.readString(),
link = source.readString(),
sourcetitle = source.readString(),
tags = source.readString()
tags = source.readParcelable(ClassLoader.getSystemClassLoader())
)
override fun describeContents() = 0
@ -110,7 +110,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 {
@ -153,4 +153,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()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(tags)
}
}

View File

@ -27,6 +27,12 @@ internal interface SelfossService {
@Query("offset") offset: Int
): Call<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")
@POST("mark/{id}")
fun markAsRead(
@ -95,7 +101,7 @@ internal interface SelfossService {
fun sources(
@Query("username") username: String,
@Query("password") password: String
): Call<List<Sources>>
): Call<List<Source>>
@DELETE("source/{id}")
fun deleteSource(

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,152 @@
package apps.amine.bou.readerforselfoss.background
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
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.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
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.utils.Config
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import org.acra.ACRA
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 {
if (context.isNetworkAccessible(null)) {
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_cloud_download)
notificationManager.notify(1, notification.build())
val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
sharedPref.getBoolean("should_log_everything", false)
)
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize))
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
})
thread {
val actions = db.actionsDao().actions()
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
)
}
}
}
}
return Result.SUCCESS
}
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) {
ACRA.getErrorReporter().maybeHandleSilentException(t, context)
}
})
}
}

View File

@ -1,38 +1,46 @@
package apps.amine.bou.readerforselfoss.fragments
import android.content.Intent
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.customtabs.CustomTabsIntent
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v4.widget.NestedScrollView
import android.support.v7.app.AlertDialog
import android.view.InflateException
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 android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.room.Room
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.api.selfoss.SuccessResponse
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.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toPx
import apps.amine.bou.readerforselfoss.utils.succeeded
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
@ -43,25 +51,29 @@ import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException
import java.net.URL
import kotlin.concurrent.thread
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 editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
override fun onStop() {
super.onStop()
mCustomTabActivityHelper.unbindCustomTabsService(activity)
if (mCustomTabActivityHelper != null) {
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -71,202 +83,288 @@ class ArticleFragment : Fragment() {
pageNumber = arguments!!.getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
db = Room.databaseBuilder(
context!!,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
}
private lateinit var rootView: ViewGroup
private var rootView: ViewGroup? = null
private lateinit var prefs: SharedPreferences
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rootView = inflater
.inflate(R.layout.fragment_article, container, false) as ViewGroup
try {
rootView = inflater
.inflate(R.layout.fragment_article, container, false) as ViewGroup
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
fab = rootView.fab
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16").toInt()
refreshAlignment()
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val debugReadingItems = prefs.getBoolean("read_debug", false)
fab.rippleColor = appColors.colorAccentDark
val api = SelfossApi(
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
val floatingToolbar: FloatingToolbar = rootView.floatingToolbar
floatingToolbar.attachFab(fab)
fab = rootView!!.fab
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
val customTabsIntent = activity!!.buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper.bindCustomTabsService(activity)
fab.rippleColor = appColors.colorAccentDark
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "14").toInt()
showMalformedUrl = prefs.getBoolean("show_error_malformed_url", true)
val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = activity!!.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, prefs)
R.id.share_action -> activity!!.shareLink(url, contentTitle)
R.id.open_action -> activity!!.openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
false,
false,
activity!!
)
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
api.unmarkItem(allItems[pageNumber.toInt()].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), activity!!)
}
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!)
}
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
}
}
)
override fun onItemLongClick(item: MenuItem?) {
rootView!!.source.text = contentSource
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
rootView!!.titleView.text = contentTitle
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE
Glide
.with(context!!)
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
} else {
rootView!!.imageView.visibility = View.GONE
}
}
)
rootView.source.text = contentSource
rootView!!.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
fab.hide()
} 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(context!!)
.setMessage(context!!.getString(R.string.webview_dialog_issue_message))
.setTitle(context!!.getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { dialog, which ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!)
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.commit()
activity!!.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 rootView
}
private fun refreshAlignment() {
textAlignment = when (prefs.getInt("text_align", 1)) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
}
private fun getContentFromMercury(
customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences
) {
rootView.progressBar.visibility = View.VISIBLE
val parser = MercuryApi(
prefs.getBoolean("should_log_everything", false)
)
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
rootView!!.progressBar.visibility = View.VISIBLE
val parser = MercuryApi(
prefs.getBoolean("should_log_everything", false)
)
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
override fun onResponse(
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
rootView.titleView.text = response.body()!!.title
url = response.body()!!.url
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
htmlToWebview(response.body()!!.content.orEmpty(), prefs)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
rootView.imageView.visibility = View.VISIBLE
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
try {
Glide
.with(context!!)
.asBitmap()
.load(response.body()!!.lead_image_url)
.apply(RequestOptions.fitCenterTransform())
.into(rootView.imageView)
} catch (e: Exception) {
// 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) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} else {
rootView.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView.nestedScrollView.scrollTo(0, 0)
try {
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
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) {
rootView!!.imageView.visibility = View.VISIBLE
try {
Glide
.with(context!!)
.asBitmap()
.load(response.body()!!.lead_image_url)
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
} catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} else {
rootView!!.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView!!.nestedScrollView.scrollTo(0, 0)
rootView!!.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
} else {
try {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
}
} 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) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
}
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
rootView!!.webcontent.visibility = View.VISIBLE
val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
if (context != null) {
rootView.webcontent.setBackgroundColor(
rootView!!.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
R.color.dark_webview
@ -278,7 +376,7 @@ class ArticleFragment : Fragment() {
}
} else {
if (context != null) {
rootView.webcontent.setBackgroundColor(
rootView!!.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
R.color.light_webview
@ -302,15 +400,15 @@ class ArticleFragment : Fragment() {
"#FFFFFF"
}
rootView.webcontent.settings.useWideViewPort = true
rootView.webcontent.settings.loadWithOverviewMode = true
rootView.webcontent.settings.javaScriptEnabled = false
rootView!!.webcontent.settings.useWideViewPort = true
rootView!!.webcontent.settings.loadWithOverviewMode = true
rootView!!.webcontent.settings.javaScriptEnabled = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rootView.webcontent.settings.layoutAlgorithm =
rootView!!.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} else {
rootView.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
}
var baseUrl: String? = null
@ -319,83 +417,54 @@ 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()
}
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
}
rootView.webcontent.loadDataWithBaseURL(
rootView!!.webcontent.loadDataWithBaseURL(
baseUrl,
"""<style>
|img {
| display: inline-block;
| height: auto;
| width: 100%;
| max-width: 100%;
|}
|a {
| color: $stringColor !important;
|}
|*:not(a) {
| color: $stringTextColor;
|}
|* {
| font-size: ${fontSize.toPx}px;
| text-align: justify;
| word-break: break-word;
| overflow:hidden;
|}
|a, pre, code {
| text-align: left;
|}
|pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: $stringBackgroundColor;
|}</style>$c""".trimMargin(),
"text/html; charset=utf-8",
"""<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;
| }
| a, pre, code {
| text-align: $textAlignment;
| }
| pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: $stringBackgroundColor;
| }
| </style>
|</head>
|<body>
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8",
null
)
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
rootView.progressBar.visibility = View.GONE
rootView!!.progressBar.visibility = View.GONE
activity!!.openItemUrl(
allItems,
pageNumber.toInt(),

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")
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")
fun items(): List<ItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllItems(vararg items: ItemEntity)
@Query("DELETE FROM items")
fun deleteAllItems()
@Delete
fun delete(item: ItemEntity)
@Update
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 = 3)
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,16 @@
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`))")
}
}

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

@ -19,7 +19,7 @@ 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;
@ -31,7 +31,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;
@ -136,6 +135,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName)
|| ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
|| OfflinePreferenceFragment.class.getName().equals(fragmentName)
|| ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
|| DebugPreferenceFragment.class.getName().equals(fragmentName)
|| LinksPreferenceFragment.class.getName().equals(fragmentName)
|| ThemePreferenceFragment.class.getName().equals(fragmentName);
@ -153,17 +154,6 @@ 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() {
@ -375,6 +365,47 @@ 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);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class ExperimentalPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_experimental);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();

View File

@ -3,8 +3,8 @@ package apps.amine.bou.readerforselfoss.themes
import android.app.Activity
import android.content.Context
import android.preference.PreferenceManager
import android.support.annotation.ColorInt
import android.support.v7.view.ContextThemeWrapper
import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper
import android.util.TypedValue
import apps.amine.bou.readerforselfoss.R
import android.view.LayoutInflater

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

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

View File

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

@ -36,6 +36,10 @@ 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"
fun logoutAndRedirect(
c: Context,
callingActivity: Activity,

View File

@ -3,6 +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 apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
import org.acra.ACRA
import java.text.ParseException
import java.text.SimpleDateFormat
@ -44,8 +45,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,18 +2,25 @@ 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.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 org.acra.ACRA
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
@ -123,13 +130,17 @@ 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()
fun String.isBaseUrlValid(): Boolean {
fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean {
val baseUrl = HttpUrl.parse(this)
var existsAndEndsWithSlash = false
if (baseUrl != null) {
@ -146,3 +157,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

@ -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;

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

@ -2,10 +2,10 @@
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 androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.materialdrawer.holder.ColorHolder
import com.mikepenz.materialdrawer.holder.ImageHolder

View File

@ -1,8 +1,8 @@
/* 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 androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import android.view.View
import android.widget.TextView
import apps.amine.bou.readerforselfoss.R

View File

@ -2,7 +2,7 @@ package apps.amine.bou.readerforselfoss.utils.glide
import android.content.Context
import android.graphics.Bitmap
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions

View File

@ -0,0 +1,45 @@
package apps.amine.bou.readerforselfoss.utils.network
import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.NetworkInfo
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 cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
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
}

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.title,
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.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags.tags
)

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="#FF000000"
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="#FF000000"
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<vector android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>

View File

@ -1,4 +0,0 @@
<vector android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M1,21h4L5,9L1,9v12zM23,10c0,-1.1 -0.9,-2 -2,-2h-6.31l0.95,-4.57 0.03,-0.32c0,-0.41 -0.17,-0.79 -0.44,-1.06L14.17,1 7.59,7.59C7.22,7.95 7,8.45 7,9v10c0,1.1 0.9,2 2,2h9c0.83,0 1.54,-0.5 1.84,-1.22l3.02,-7.05c0.09,-0.23 0.14,-0.47 0.14,-0.73v-1.91l-0.01,-0.01L23,10z"/>
</vector>

View File

@ -10,22 +10,22 @@
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
@ -121,7 +121,7 @@
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="0.0"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/progress"

View File

@ -30,13 +30,13 @@
app:prompt_view_background_color="?attr/colorAccent"
app:prompt_view_thanks_display_time_ms="2000"/>
<android.support.design.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/promptView">
<android.support.design.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/intern_coordLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -46,18 +46,18 @@
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/drawer_layout"
@ -66,7 +66,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -86,30 +86,30 @@
android:text="@string/nothing_here"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:background="@color/transparent"
android:background="@android:color/transparent"
android:visibility="gone" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:background="@android:color/transparent"
android:clipToPadding="false"
android:paddingBottom="60dp"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.ashokvarma.bottomnavigation.BottomNavigationBar
android:layout_gravity="bottom"
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="60dp"/>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>

View File

@ -6,18 +6,18 @@
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context="apps.amine.bou.readerforselfoss.LoginActivity">
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -45,7 +45,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/urlLayout"
@ -60,7 +60,7 @@
android:inputType="textUri"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:text="@string/withLoginSwitch"
@ -69,7 +69,7 @@
android:id="@+id/withLogin"
android:layout_weight="1"/>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -83,9 +83,9 @@
android:inputType="text"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -99,7 +99,7 @@
android:inputType="textPassword"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/withHttpLogin"
@ -108,7 +108,7 @@
android:layout_weight="1"
android:text="@string/withHttpLoginSwitch" />
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/httpLoginInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -120,9 +120,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_http_login" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/httpPasswordInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -134,7 +134,7 @@
android:layout_height="wrap_content"
android:hint="@string/prompt_http_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/withSelfhostedCert"

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="apps.amine.bou.readerforselfoss.MainActivity">
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:id="@+id/reader_activity_view"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -12,16 +13,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="?attr/toolbarPopupTheme"
app:theme="@style/ToolBarStyle" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<android.support.v4.view.ViewPager
<androidx.viewpager.widget.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
@ -41,4 +42,4 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/pager" />
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,33 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="apps.amine.bou.readerforselfoss.SourcesActivity">
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
</androidx.recyclerview.widget.RecyclerView>
<android.support.design.widget.FloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -40,6 +40,5 @@
android:layout_alignParentEnd="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_behavior="apps.amine.bou.readerforselfoss.utils.ScrollAwareFABBehavior" />
</android.support.design.widget.CoordinatorLayout>
android:layout_marginRight="16dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
@ -18,7 +18,7 @@
card_view:cardUseCompatPadding="true"
card_view:layout_constraintBottom_toBottomOf="parent">
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -34,7 +34,7 @@
app:srcCompat="@drawable/background_splash"
card_view:layout_constraintBottom_toTopOf="@+id/constraintLayout" />
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -143,7 +143,7 @@
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</android.support.v7.widget.CardView>
</androidx.cardview.widget.CardView>

View File

@ -1,4 +1,4 @@
<android.support.design.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -6,12 +6,13 @@
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants">
<android.support.v4.widget.NestedScrollView
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:scrollbars="vertical">
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -70,9 +71,9 @@
app:layout_constraintTop_toBottomOf="@+id/source"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:layout_width="match_parent"
@ -89,7 +90,7 @@
android:layout_gravity="bottom"
app:floatingMenu="@menu/reader_toolbar" />
<android.support.design.widget.FloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -123,4 +124,4 @@
android:progressTint="?attr/colorAccent" />
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -11,7 +11,6 @@
android:id="@+id/itemImage"
android:layout_width="88dp"
android:layout_height="88dp"
app:layout_constraintBottom_toBottomOf="@+id/actionBar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -40,79 +39,16 @@
android:id="@+id/sourceTitleAndDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:gravity="start"
android:textAlignment="viewStart"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/actionBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Google Actualité Il y a 5h" />
<RelativeLayout
android:id="@+id/actionBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#BBBBBB"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible">
<com.like.LikeButton
android:id="@+id/favButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:elevation="5dp"
android:padding="4dp"
app:icon_size="22dp"
app:icon_type="heart" />
<ImageButton
android:id="@+id/shareBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/favButton"
android:layout_toStartOf="@+id/favButton"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:backgroundTint="?android:attr/textColorPrimary"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_share_black_24dp" />
<ImageButton
android:id="@+id/browserBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/shareBtn"
android:layout_toStartOf="@+id/shareBtn"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:backgroundTint="?android:attr/textColorPrimary"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_open_in_browser_black_24dp" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -52,4 +52,4 @@
android:layout_width="34dp"
android:layout_height="34dp"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,13 +6,13 @@
android:title="@string/menu_home_search"
android:icon="@drawable/ic_action_search"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="android.support.v7.widget.SearchView" />
app:actionViewClass="androidx.appcompat.widget.SearchView" />
<item android:id="@+id/readAll"
android:icon="@drawable/ic_done_all_white_24dp"
android:title="@string/readAll"
android:orderInCategory="1"
app:showAsAction="ifRoom"/>
app:showAsAction="always"/>
<item
android:id="@+id/refresh"

View File

@ -2,6 +2,18 @@
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/align_left"
android:icon="@drawable/ic_format_align_left"
android:visible="true"
app:showAsAction="ifRoom"
android:title="@string/reader_text_align_left" />
<item android:id="@+id/align_justify"
android:icon="@drawable/ic_format_align_justify"
android:visible="true"
app:showAsAction="ifRoom"
android:title="@string/reader_text_align_justify" />
<item
android:id="@+id/unsave"
android:icon="@drawable/heart_on"

View File

@ -2,6 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/unread_action"
android:icon="@drawable/ic_fiber_new"
android:title="@string/unmark"
app:showAsAction="ifRoom" />
<item
android:id="@+id/more_action"
android:icon="@drawable/ic_chrome_reader_mode"

View File

@ -1,171 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Log in"</string>
<string name="prompt_password">"Password"</string>
<string name="prompt_http_password">"HTTP Password"</string>
<string name="action_sign_in">"Go"</string>
<string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string>
<string name="withLoginSwitch">"Login required ?"</string>
<string name="withHttpLoginSwitch">"HTTP Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"Username"</string>
<string name="prompt_http_login">"HTTP Username"</string>
<string name="label_share">"Share"</string>
<string name="readAll">"Read all"</string>
<string name="action_disconnect">"Disconnect"</string>
<string name="title_activity_settings">"Settings"</string>
<string name="pref_header_general">"General"</string>
<string name="pref_switch_actions_tap_title">"Tap action on the articles"</string>
<string name="add_source_hint_tags">"Tag1, Tag2, Tag3"</string>
<string name="add_source_hint_url">"Link"</string>
<string name="add_source_hint_name">"Name"</string>
<string name="add_source">"Add a source"</string>
<string name="add_source_save">"Save"</string>
<string name="wrong_infos">"Check your details again."</string>
<string name="all_posts_not_read">"All posts weren't read"</string>
<string name="all_posts_read">"All posts were read"</string>
<string name="cant_get_favs">"Can't get favorites"</string>
<string name="cant_get_new_elements">"Can't get new articles"</string>
<string name="cant_get_read">"Can't get read articles"</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"All"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string>
<string name="undo_string">"Undo"</string>
<string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
<string name="issue_tracker_summary">"Report a bug or ask for a new feature"</string>
<string name="warning_wrong_url">"WARNING"</string>
<string name="pref_switch_card_view_title">"Card View"</string>
<string name="cant_mark_favortie">"Can't mark article as favorite"</string>
<string name="cant_unmark_favortie">"Can't remove item from favorite"</string>
<string name="share">"Share"</string>
<string name="rating_prompt_title">"Enjoying the app ?"</string>
<string name="rating_prompt_yes">"Yes !"</string>
<string name="rating_prompt_no">"Not really "</string>
<string name="rating_prompt_feedback_title">"Can you tell us why ?"</string>
<string name="rating_prompt_feedback_yes">"OK !"</string>
<string name="rating_prompt_feedback_no">"Not now."</string>
<string name="rating_prompt_rating_title">"Great ! Can you rate us on the Store ?"</string>
<string name="rating_prompt_rating_yes">"Sure !"</string>
<string name="rating_prompt_rating_no">"Not right now."</string>
<string name="rating_prompt_thanks">"Thanks, your feedback help enhance the app !"</string>
<string name="switch_unread_count">"Display the unread count as a badge for the bottom bar."</string>
<string name="switch_unread_count_title">"Display unread count"</string>
<string name="display_all_counts_title">"Display count for favorite and read"</string>
<string name="invitation_title">"Try this app for your Selfoss RSS feeds !"</string>
<string name="invitation_message">"I use this app for my Selfoss RSS feeds. You may like it too !"</string>
<string name="invitation_cta">"Try the app"</string>
<string name="text_wrong_url">"You seem to be trying to use an invalid URL. Make sure it is correct, and if the problem persists, contact me (via the store contact link). Please note that the app needs you to be using Selfoss. You can't access RSS feeds without it."</string>
<string name="pref_general_internal_browser_title">"Open links inside the app"</string>
<string name="pref_general_internal_browser_on">"Articles will open inside the app"</string>
<string name="pref_general_internal_browser_off">"Articles will open with your default browser"</string>
<string name="prefer_article_viewer_title">"Use the article viewer"</string>
<string name="prefer_article_viewer_on">"Will use the article viewer instead of the internal browser"</string>
<string name="prefer_article_viewer_off">"Will use the internal browser instead of the article viewer"</string>
<string name="pref_general_category_links">"Link handling"</string>
<string name="pref_general_category_displaying">"Displaying"</string>
<string name="pref_general_category_actions">"Actions"</string>
<string name="pref_switch_card_view_on">"The articles will be displayed as cards"</string>
<string name="pref_switch_card_view_off">"The articles will be displayed as a list"</string>
<string name="pref_switch_actions_tap_on">"Displays the action bar under the article"</string>
<string name="pref_switch_actions_tap_off">"When selecting an article it will open in your selected browser"</string>
<string name="menu_home_refresh">"Update remote"</string>
<string name="refresh_success_response">"The remote is updated, you can now reload the articles list"</string>
<string name="refresh_failer_message">"The update didn't work, try again later, or check your selfoss logs."</string>
<string name="refresh_in_progress">"Refresh in progress"</string>
<string name="new_apk_available_title">"A new APK is available."</string>
<string name="new_apk_available_message">"A new APK is available to download on the official repository."</string>
<string name="new_apk_available_get">"Download now"</string>
<string name="new_apk_available_no">"Ignore version"</string>
<string name="intro_hello_title">"Hi there !"</string>
<string name="intro_hello_message">"Thanks for downloading the app !"</string>
<string name="intro_needs_selfoss_title">"Before you start…"</string>
<string name="intro_needs_selfoss_message">"You can't use the app without a Selfoss instance."</string>
<string name="intro_needs_selfoss_link">"What is Selfoss ?"</string>
<string name="intro_all_set_title">"All set !"</string>
<string name="intro_all_set_message">"You are ready to use the app. Don't forget to go to the settings page to configure your app, and where you'll find some useful links."</string>
<string name="card_height_title">Full height cards</string>
<string name="card_height_on">Cards height will adjust to its content</string>
<string name="card_height_off">Card height will be fixed</string>
<string name="source_code">Source code</string>
<string name="cant_mark_read">Can\'t mark article as read</string>
<string name="drawer_error_loading_tags">Error loading tags…</string>
<string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="drawer_item_filters">Filters</string>
<string name="drawer_action_clear">clear</string>
<string name="drawer_item_tags">Tags</string>
<string name="drawer_item_sources">Sources</string>
<string name="drawer_action_edit">edit</string>
<string name="cache_drawer_error" tools:keep="@string/cache_drawer_error">Couldn\'t cache your drawer data</string>
<string name="no_tags_loaded">No tags loaded</string>
<string name="no_sources_loaded">No sources loaded</string>
<string name="drawer_loading">Loading …</string>
<string name="menu_home_search">Search</string>
<string name="can_delete_source">Can\'t delete the source…</string>
<string name="base_url_error">There was an issue when trying to communicate with your Selfoss Instance. If the issue persists, please get in touch with me.</string>
<string name="pref_header_theme">Themes</string>
<string name="default_theme">Default</string>
<string name="default_dark_theme">Default/Dark</string>
<string name="pref_header_debug">Debug</string>
<string name="login_debug_title">Activate to log login errors</string>
<string name="login_debug_on">Any error on the login page will be logged</string>
<string name="login_debug_off">No log on the login page</string>
<string name="login_menu_debug">Debug</string>
<string name="self_hosted_cert_switch">Using a self hosted certificate ?</string>
<string name="self_signed_cert_warning">Due to security reasons, self signed certificates are not supported by default. By activating this, I\'ll not be responsible of any security problem you encounter.</string>
<string name="pref_selfoss_category">Selfoss Api</string>
<string name="pref_api_items_number_title">Loaded items number</string>
<string name="pref_hidden_tags">Hidden Tags</string>
<string name="read_debug_title">Read articles appearing as unread ?</string>
<string name="read_debug_off">No log when marking an item as read</string>
<string name="read_debug_on">Api calls will be logged when marking an article as read</string>
<string name="summary_debug_identifier">Debug identifier</string>
<string name="unique_id_to_clipboard">Identifier copied to your clipboard</string>
<string name="display_header_drawer_summary">Display a header with the selfoss instance url on the lateral drawer.</string>
<string name="display_header_drawer_title">Account header</string>
<string name="login_everything_title">Logging every api calls</string>
<string name="login_everything_on">This will log every api call for debug purpose.</string>
<string name="login_everything_off">No api call will be logged</string>
<string name="pref_general_infinite_loading_title">Load more articles on scroll</string>
<string name="translation">Translation</string>
<string name="cant_open_invalid_url">The item url is invalid. I\'m looking into solving this issue so the app won\'t crash.</string>
<string name="drawer_report_bug">Report a bug</string>
<string name="items_number_should_be_number">The items number should be an integer.</string>
<string name="reader_action_more">Read more</string>
<string name="reader_action_open">Open in browser</string>
<string name="reader_action_share">Share</string>
<string name="pref_switch_actions_pager_scroll_on">Mark articles as read when swiping between articles.</string>
<string name="add_to_favs_reader">Add to favorites</string>
<string name="remove_to_favs_reader">Remove from favorites</string>
<string name="pref_content_reader_font_size">Article reader content font size</string>
<string name="pref_header_viewer">Article viewer</string>
<string name="refresh_dialog_message">This will refresh your Selfoss instance.</string>
<string name="markall_dialog_message">This will mark all the items as read.</string>
<string name="pref_switch_actions_pager_scroll">Mark as read on swipe</string>
<string name="pref_switch_actions_pager_scroll_off">Don\'t mark articles as read when swiping.</string>
<string name="gdpr_dialog_message">The app does not collect any personal data. Every analytics tools were removed. Crash reports sending is now optional, as is the debug logging. Keep in mind that debugging and crash reports are essential for the app development (You can configure everything in Settings &gt; Debug).</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="crash_dialog_text">Something went wrong. Please send the report to the developer.</string>
<string name="crash_dialog_comment">You can add any helpful details in the comment bellow. Don\'t include any personal data in your comment. You could send me and email with your debug id, and I\'ll keep you posted when the issue is resolved.</string>
<string name="pref_acra_user_email">Contact email</string>
<string name="pref_acra_user_email_summary">Add an email so I can contact you about the crash reports you send.</string>
<string name="pref_acra_alwaysaccept">Automatically send crash reports</string>
<string name="pref_acra_alwaysaccept_enabled">Will send crash reports automatically</string>
<string name="pref_acra_alwaysaccept_disabled">Will ask everytime when sending crash reports.</string>
<string name="pref_debug_crash_reports">Crash reports</string>
<string name="pref_debug_debug_logs">Debug logging (these will be sent without a dialog)</string>
<string name="acra_login">Enable logging</string>
<string name="drawer_item_hidden_tags">Hidden Tags</string>
<string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Log in"</string>
<string name="prompt_password">"Password"</string>
<string name="prompt_http_password">"HTTP Password"</string>
<string name="action_sign_in">"Go"</string>
<string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string>
<string name="withLoginSwitch">"Login required ?"</string>
<string name="withHttpLoginSwitch">"HTTP Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"Username"</string>
<string name="prompt_http_login">"HTTP Username"</string>
<string name="label_share">"Share"</string>
<string name="readAll">"Read all"</string>
<string name="action_disconnect">"Disconnect"</string>
<string name="title_activity_settings">"Settings"</string>
<string name="pref_header_general">"General"</string>
<string name="add_source_hint_tags">"Tag1, Tag2, Tag3"</string>
<string name="add_source_hint_url">"Link"</string>
<string name="add_source_hint_name">"Name"</string>
<string name="add_source">"Add a source"</string>
<string name="add_source_save">"Save"</string>
<string name="wrong_infos">"Check your details again."</string>
<string name="all_posts_not_read">"All posts weren't read"</string>
<string name="all_posts_read">"All posts were read"</string>
<string name="cant_get_favs">"Can't get favorites"</string>
<string name="cant_get_new_elements">"Can't get new articles"</string>
<string name="cant_get_read">"Can't get read articles"</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"All"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string>
<string name="undo_string">"Undo"</string>
<string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</string>
<string name="cant_get_spouts">"Can't get spouts list."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
<string name="issue_tracker_summary">"Report a bug or ask for a new feature"</string>
<string name="warning_wrong_url">"WARNING"</string>
<string name="pref_switch_card_view_title">"Card View"</string>
<string name="cant_mark_favortie">"Can't mark article as favorite"</string>
<string name="cant_unmark_favortie">"Can't remove item from favorite"</string>
<string name="share">"Share"</string>
<string name="rating_prompt_title">"Enjoying the app ?"</string>
<string name="rating_prompt_yes">"Yes !"</string>
<string name="rating_prompt_no">"Not really …"</string>
<string name="rating_prompt_feedback_title">"Can you tell us why ?"</string>
<string name="rating_prompt_feedback_yes">"OK !"</string>
<string name="rating_prompt_feedback_no">"Not now."</string>
<string name="rating_prompt_rating_title">"Great ! Can you rate us on the Store ?"</string>
<string name="rating_prompt_rating_yes">"Sure !"</string>
<string name="rating_prompt_rating_no">"Not right now."</string>
<string name="rating_prompt_thanks">"Thanks, your feedback help enhance the app !"</string>
<string name="switch_unread_count">"Display the unread count as a badge for the bottom bar."</string>
<string name="switch_unread_count_title">"Display unread count"</string>
<string name="display_all_counts_title">"Display count for favorite and read"</string>
<string name="text_wrong_url">"You seem to be trying to use an invalid URL. Make sure it is correct, and if the problem persists, contact me (via the store contact link). Please note that the app needs you to be using Selfoss. You can't access RSS feeds without it."</string>
<string name="pref_general_internal_browser_title">"Open links inside the app"</string>
<string name="pref_general_internal_browser_on">"Articles will open inside the app"</string>
<string name="pref_general_internal_browser_off">"Articles will open with your default browser"</string>
<string name="prefer_article_viewer_title">"Use the article viewer"</string>
<string name="prefer_article_viewer_on">"Will use the article viewer instead of the internal browser"</string>
<string name="prefer_article_viewer_off">"Will use the internal browser instead of the article viewer"</string>
<string name="pref_general_category_links">"Link handling"</string>
<string name="pref_general_category_displaying">"Displaying"</string>
<string name="pref_switch_card_view_on">"The articles will be displayed as cards"</string>
<string name="pref_switch_card_view_off">"The articles will be displayed as a list"</string>
<string name="menu_home_refresh">"Update remote"</string>
<string name="refresh_success_response">"The remote is updated, you can now reload the articles list"</string>
<string name="refresh_failer_message">"The update didn't work, try again later, or check your selfoss logs."</string>
<string name="refresh_in_progress">"Refresh in progress"</string>
<string name="card_height_title">Full height cards</string>
<string name="card_height_on">Cards height will adjust to its content</string>
<string name="card_height_off">Card height will be fixed</string>
<string name="source_code">Source code</string>
<string name="cant_mark_read">Can\'t mark article as read</string>
<string name="drawer_error_loading_tags">Error loading tags…</string>
<string name="drawer_error_loading_sources">Error loading sources…</string>
<string name="drawer_item_filters">Filters</string>
<string name="drawer_action_clear">clear</string>
<string name="drawer_item_tags">Tags</string>
<string name="drawer_item_sources">Sources</string>
<string name="drawer_action_edit">edit</string>
<string name="no_tags_loaded">No tags loaded</string>
<string name="no_sources_loaded">No sources loaded</string>
<string name="drawer_loading">Loading …</string>
<string name="menu_home_search">Search</string>
<string name="can_delete_source">Can\'t delete the source…</string>
<string name="base_url_error">There was an issue when trying to communicate with your Selfoss Instance. If the issue persists, please get in touch with me.</string>
<string name="pref_header_theme">Themes</string>
<string name="default_theme">Default</string>
<string name="default_dark_theme">Default/Dark</string>
<string name="pref_header_debug">Debug</string>
<string name="login_debug_title">Activate to log login errors</string>
<string name="login_debug_on">Any error on the login page will be logged</string>
<string name="login_debug_off">No log on the login page</string>
<string name="login_menu_debug">Debug</string>
<string name="self_hosted_cert_switch">Using a self hosted certificate ?</string>
<string name="self_signed_cert_warning">Due to security reasons, self signed certificates are not supported by default. By activating this, I\'ll not be responsible of any security problem you encounter.</string>
<string name="pref_selfoss_category">Selfoss Api</string>
<string name="pref_api_items_number_title">Loaded items number</string>
<string name="pref_hidden_tags">Hidden Tags</string>
<string name="read_debug_title">Read articles appearing as unread ?</string>
<string name="read_debug_off">No log when marking an item as read</string>
<string name="read_debug_on">Api calls will be logged when marking an article as read</string>
<string name="summary_debug_identifier">Debug identifier</string>
<string name="unique_id_to_clipboard">Identifier copied to your clipboard</string>
<string name="display_header_drawer_summary">Display a header with the selfoss instance url on the lateral drawer.</string>
<string name="display_header_drawer_title">Account header</string>
<string name="login_everything_title">Logging every api calls</string>
<string name="login_everything_on">This will log every api call for debug purpose.</string>
<string name="login_everything_off">No api call will be logged</string>
<string name="pref_general_infinite_loading_title">Load more articles on scroll</string>
<string name="translation">Translation</string>
<string name="cant_open_invalid_url">The item url is invalid. I\'m looking into solving this issue so the app won\'t crash.</string>
<string name="drawer_report_bug">Report a bug</string>
<string name="items_number_should_be_number">The items number should be an integer.</string>
<string name="reader_action_more">Read more</string>
<string name="reader_action_open">Open in browser</string>
<string name="reader_action_share">Share</string>
<string name="pref_switch_actions_pager_scroll_on">Mark articles as read when swiping between articles.</string>
<string name="add_to_favs_reader">Add to favorites</string>
<string name="remove_to_favs_reader">Remove from favorites</string>
<string name="pref_content_reader_font_size">Article reader content font size</string>
<string name="pref_header_viewer">Article viewer</string>
<string name="refresh_dialog_message">This will refresh your Selfoss instance.</string>
<string name="markall_dialog_message">This will mark all the items as read.</string>
<string name="pref_switch_actions_pager_scroll">Mark as read on swipe</string>
<string name="pref_switch_actions_pager_scroll_off">Don\'t mark articles as read when swiping.</string>
<string name="gdpr_dialog_message">The app does not collect any personal data. Every analytics tools were removed. Crash reports sending is now optional, as is the debug logging. Keep in mind that debugging and crash reports are essential for the app development (You can configure everything in Settings &gt; Debug).</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="crash_dialog_text">Something went wrong. Please send the report to the developer.</string>
<string name="crash_dialog_comment">You can add any helpful details in the comment bellow. Don\'t include any personal data in your comment. You could send me and email with your debug id, and I\'ll keep you posted when the issue is resolved.</string>
<string name="pref_acra_alwaysaccept">Automatically send crash reports</string>
<string name="pref_acra_alwaysaccept_enabled">Will send crash reports automatically</string>
<string name="pref_acra_alwaysaccept_disabled">Will ask everytime when sending crash reports.</string>
<string name="pref_debug_crash_reports">Crash reports</string>
<string name="pref_debug_debug_logs">Debug logging (these will be sent without a dialog)</string>
<string name="acra_login">Enable logging</string>
<string name="drawer_item_hidden_tags">Hidden Tags</string>
<string name="unmark">Mark item as unread</string>
<string name="pref_header_offline">Offline and cache</string>
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
<string name="pref_switch_refresh_when_charging">Only refresh when phone is charging</string>
<string name="loading_notification_title">Loading ...</string>
<string name="loading_notification_text">Selfoss is syncing your articles</string>
<string name="notification_channel_sync">Sync notification</string>
<string name="new_items_channel_sync">New items notification</string>
<string name="new_items_notification_title">New items !</string>
<string name="new_items_notification_text">%1$d new items loaded.</string>
<string name="pref_switch_notify_new_items">Notify on new items synced.</string>
<string name="shortcut_offline">Offline</string>
<string name="pref_api_timeout">Api Timeout</string>
<string name="pref_header_experimental">Experimental</string>
<string name="webview_dialog_issue_message">Webview not available. Disabling the article viewer to avoid any future crashes. Will load articles inside of your browser from now on.</string>
<string name="webview_dialog_issue_title">Webview issue</string>
</resources>

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