Compare commits

...

105 Commits

Author SHA1 Message Date
ddc754ec25 Closes #270. 2019-01-15 19:54:46 +01:00
134a0766d6 Updates. 2019-01-14 20:56:12 +01:00
69da932ab5 Just to be sure. 2019-01-14 19:39:54 +01:00
592fb6328a Workaround for #275. 2019-01-14 19:37:43 +01:00
a0aead6491 Closes #271. 2019-01-13 15:59:38 +01:00
722b6cc06d Fixed issue with read/unread cont when swiping. 2019-01-13 15:59:38 +01:00
6d7c4b40f6 Wip fixing the badges reloading when reading all the items. 2019-01-13 15:59:38 +01:00
f538ed39fc New translations strings.xml (Chinese Traditional) (#274) 2019-01-11 14:20:35 +01:00
65821492ad New Crowdin translations (#273)
* New translations strings.xml (Catalan)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)

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

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

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

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

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-12-10 19:26:48 +01:00
37e7b987ee Changed color of alignment icons. 2018-12-10 19:25:18 +01:00
9eac51e729 Closes #257. 2018-12-09 16:38:35 +01:00
fa9cce6783 Removed some logs. 2018-12-09 14:26:20 +01:00
f0d4b63a97 Merge pull request #256 from aminecmi/fdroid/build
Fixes #254 and #255.
2018-12-02 13:22:37 +01:00
83eeb11388 Fixes #254 and #255. 2018-12-02 13:13:19 +01:00
01f746f33d Merge pull request #255 from aminecmi/fdroid/build
A fix for #254.
2018-12-02 03:54:36 +01:00
200851894b See #254. 2018-12-01 19:32:28 +01:00
862e5cf4ab New Crowdin translations (#253)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)
2018-11-29 06:06:32 +01:00
0b07f2a407 Closes #174. Closes #248. 2018-11-28 21:03:00 +01:00
9ba6feef0b Removed density calculation to solve #248. 2018-11-27 21:44:34 +01:00
63a0638522 Removed throw... 2018-11-27 21:36:12 +01:00
f9a4e6e363 New Crowdin translations (#252)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)
2018-11-27 21:32:12 +01:00
6b40fd4bdc Typos. 2018-11-27 21:25:18 +01:00
04c7776466 Trying to fix an issue with webview not available with the article viewer. Fixed an issue when the browser isn't available. 2018-11-27 21:14:03 +01:00
92c335b4e1 Closes #251. 2018-11-27 20:02:53 +01:00
17251e576b New Crowdin translations (#250)
* New translations strings.xml (Catalan)

* New translations strings.xml (Catalan)

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

* New translations strings.xml (German)

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

* New translations strings.xml (Galician)
2018-11-13 15:51:16 +01:00
7da7d49277 New Crowdin translations (#242)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)
2018-11-11 17:17:37 +01:00
9b45365441 Changelog. Previous commit should close #238. 2018-11-11 12:25:45 +01:00
91a7464bce Added experimental settings with a custom timeout setting. 2018-11-11 12:23:59 +01:00
51add226eb New Crowdin translations (#241)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-11-08 08:59:37 +01:00
332e9f5108 New Crowdin translations (#240)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)
2018-11-07 21:25:14 +01:00
0b91087c07 CHANGELOG. 2018-11-07 21:22:19 +01:00
ebbb1ba0f8 Closes #220. 2018-11-07 21:22:06 +01:00
e9143ae852 Initial changes for #238. 2018-11-07 21:07:29 +01:00
42e8ecee78 Offline shortcut. 2018-11-07 21:05:23 +01:00
4efd76fcbc Tab selection from app shortcut. 2018-11-07 20:45:51 +01:00
fb1614070e Inital app shortcuts. 2018-11-07 20:25:48 +01:00
c473dd7227 Fixes #239. 2018-11-07 19:32:10 +01:00
76bddb195d New Crowdin translations (#237)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)
2018-11-06 20:35:30 +01:00
1e02ad2041 Closes #201. 2018-11-06 20:29:00 +01:00
f6ab909f8b Fixed issue. 2018-11-06 20:11:07 +01:00
7e520e9bed Still fixing selfoss version issues. 2018-11-05 21:11:25 +01:00
32e2d05014 CHANGELOG. 2018-11-05 20:30:58 +01:00
40d9c97f73 Fixes #216. 2018-11-05 20:30:37 +01:00
1aa68d3449 New Crowdin translations (#234)
* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)
2018-11-05 13:44:22 +01:00
aeeac8cccd Little fix. 2018-11-04 14:56:48 +01:00
7292edf997 #Closes #179. 2018-11-04 14:37:22 +01:00
f49256c72f Manual sync for read/unread/star/unstar. 2018-11-04 14:33:50 +01:00
d02b28b81f Initial changes for #179. 2018-11-04 14:25:05 +01:00
08117043dd Do not replace the background task. 2018-11-03 21:07:27 +01:00
63496c993e New Crowdin translations (#233)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)

* New translations strings.xml (Chinese Simplified)
2018-11-03 19:01:40 +01:00
00ef542e49 Closes #33. 2018-11-03 18:48:50 +01:00
a78c6e6b33 Sync with settings. 2018-11-03 18:47:43 +01:00
363eaf9bf9 Preferences for the background tasks. 2018-11-03 18:14:22 +01:00
fec6683701 Merge branch 'master' of github.com:aminecmi/ReaderforSelfoss 2018-11-03 11:30:13 +01:00
1549edb647 ... 2018-11-03 11:29:53 +01:00
3de48ba162 Some more background tasks. 2018-11-03 11:29:03 +01:00
a2a3d6f1a7 New Crowdin translations (#232)
* New translations strings.xml (Spanish)

* New translations strings.xml (Galician)

* New translations strings.xml (French)
2018-11-02 10:34:04 +01:00
ccab2c7648 Initial work on background task. 2018-11-01 21:51:31 +01:00
880dd1db5c New Crowdin translations (#231)
* New translations strings.xml (Catalan)

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)
2018-11-01 21:27:59 +01:00
ed18fea356 Closes #38. 2018-11-01 21:11:54 +01:00
9816b20bf6 Only do api calls on network available. 2018-11-01 21:10:00 +01:00
0bb2195bff Network status on articles loading. 2018-11-01 20:42:49 +01:00
ab2d0c4036 Closes #230. 2018-10-31 20:14:20 +01:00
99fc417109 Fixed #230. 2018-10-29 19:53:41 +01:00
dc304ef8c1 Updated gradle. 2018-10-20 09:29:09 +02:00
c5511880bc Just trying to fix fragment issues. 2018-10-19 05:27:16 +02:00
247 changed files with 4867 additions and 5433 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.

View File

@ -1,9 +1,31 @@
**1.7.x**
- Hiding tags with 0 articles
- Fixed issue with basic auth and images loading
- Added the ability to justify or left align the reader text
- Fixed #251
- Added experimental issue to set a default timeout. Should work for #238.
- Closing #220.
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
- Closes #216. Issue with selfoss version 2.19.
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
- Closes #33. Background sync with settings.
- Closing #1. Initial article caching.
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
- Closing #38. Only doing api calls on network available.
**1.6.x**
- Handling hidden tags.

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() {
@ -67,7 +67,7 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
@ -75,7 +75,6 @@ android {
buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
applicationIdSuffix ".dev"
}
}
flavorDimensions "build"
@ -113,7 +112,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
//multidex
implementation 'androidx.multidex:multidex:2.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
// About
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
@ -152,14 +151,16 @@ dependencies {
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"
}
@ -177,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,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

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="apps.amine.bou.readerforselfoss"
xmlns:tools="http://schemas.android.com/tools">
package="apps.amine.bou.readerforselfoss">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
@ -11,15 +11,19 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar">
<activity
android:name=".MainActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".LoginActivity"
@ -33,7 +37,7 @@
android:parentActivityName=".HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
android:value=".HomeActivity" />
</activity>
<activity
android:name=".SourcesActivity"
@ -51,9 +55,7 @@
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
@ -72,6 +74,9 @@
android:value="true" />
<meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>

View File

@ -1,5 +1,6 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -85,10 +86,13 @@ class AddSourceActivity : AppCompatActivity() {
try {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
api = SelfossApi(
this,
this@AddSourceActivity,
prefs.getBoolean("isSelfSignedCert", false),
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
} catch (e: IllegalArgumentException) {
@ -108,7 +112,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

@ -18,13 +18,15 @@ 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.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
@ -34,8 +36,11 @@ 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
@ -47,6 +52,7 @@ 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
@ -59,7 +65,6 @@ import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.ftinc.scoop.Scoop
import com.github.stkent.amplify.tracking.Amplify
import com.google.gson.reflect.TypeToken
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.materialdrawer.Drawer
@ -69,11 +74,11 @@ 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 {
@ -110,6 +115,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
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
private lateinit var tabStarredBadge: TextBadgeItem
@ -130,10 +139,15 @@ 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>
private lateinit var db: AppDatabase
private lateinit var config: Config
data class DrawerData(val tags: List<Tag>?, val sources: List<Source>?)
override fun onStart() {
@ -143,9 +157,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@HomeActivity)
config = Config(this@HomeActivity)
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()
@ -158,7 +180,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).build()
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
customTabActivityHelper = CustomTabActivityHelper()
@ -170,6 +192,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
this@HomeActivity,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
shouldLogEverything
)
items = ArrayList()
@ -188,6 +211,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.color.refresh_progress_3
)
swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
allItems = ArrayList()
lastFetchDone = false
handleDrawerItems()
@ -203,7 +227,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int =
if (elementsShown != UNREAD_SHOWN) {
if (elementsShown != UNREAD_SHOWN && elementsShown != READ_SHOWN) {
0
} else {
super.getSwipeDirs(
@ -223,17 +247,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val i = items.elementAtOrNull(position)
if (i != null) {
val adapter = recyclerView.adapter
val adapter = recyclerView.adapter as ItemsAdapter<*>
when (adapter) {
is ItemCardAdapter -> adapter.removeItemAtIndex(position)
is ItemListAdapter -> adapter.removeItemAtIndex(position)
val wasItemUnread = adapter.unreadItemStatusAtIndex(position)
adapter.handleItemAtIndex(position)
if (wasItemUnread) {
badgeNew--
} else {
badgeNew++
}
badgeNew--
reloadBadgeContent()
val tagHashes = i.tags.split(",").map { it.longHash() }
val tagHashes = i.tags.tags.split(",").map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
(it.key to (it.value - 1))
@ -277,19 +305,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val tabNew =
BottomNavigationItem(
R.drawable.ic_fiber_new_black_24dp,
R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new)
).setActiveColor(appColors.colorAccent)
.setBadgeItem(tabNewBadge)
val tabArchive =
BottomNavigationItem(
R.drawable.ic_archive_black_24dp,
R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read)
).setActiveColor(appColors.colorAccentDark)
.setBadgeItem(tabArchiveBadge)
val tabStarred =
BottomNavigationItem(
R.drawable.ic_favorite_black_24dp,
R.drawable.ic_tab_favorite_black_24dp,
getString(R.string.tab_favs)
).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge)
@ -303,6 +331,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() {
@ -333,8 +365,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
getElementsAccordingToTab()
handleGDPRDialog(sharedPref.getBoolean("GDPR_shown", false))
handleRecurringTask()
handleOfflineActions()
}
private fun getAndStoreAllItems() {
@ -349,7 +384,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags)
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
db.itemsDao().deleteAllItems()
db.itemsDao()
@ -384,6 +419,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} 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() {
@ -437,7 +479,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
footer {
primaryItem(R.string.drawer_report_bug) {
icon = R.drawable.ic_bug_report
icon = R.drawable.ic_bug_report_black_24dp
iconTintingEnabled = true
onClick { _ ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Config.trackerUrl))
@ -447,7 +489,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
primaryItem(R.string.title_activity_settings) {
icon = R.drawable.ic_settings
icon = R.drawable.ic_settings_black_24dp
iconTintingEnabled = true
onClick { _ ->
startActivityForResult(
@ -490,12 +532,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
drawer.addItem(
var drawerItem =
PrimaryDrawerItem()
.withName(it.tag)
.withIdentifier(it.tag.longHash())
.withIcon(gd)
.withBadge("${it.unread}")
.withBadgeStyle(
BadgeStyle().withTextColor(Color.WHITE)
.withColor(appColors.colorAccent)
@ -506,6 +547,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
getElementsAccordingToTab()
false
}
if (it.unread > 0) {
drawerItem = drawerItem.withBadge("${it.unread}")
}
drawer.addItem(
drawerItem
)
(it.tag.longHash() to it.unread)
@ -537,12 +583,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
drawer.addItem(
var drawerItem =
PrimaryDrawerItem()
.withName(it.tag)
.withIdentifier(it.tag.longHash())
.withIcon(gd)
.withBadge("${it.unread}")
.withBadgeStyle(
BadgeStyle().withTextColor(Color.WHITE)
.withColor(appColors.colorAccent)
@ -553,6 +598,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
getElementsAccordingToTab()
false
}
if (it.unread > 0) {
drawerItem = drawerItem.withBadge("${it.unread}")
}
drawer.addItem(
drawerItem
)
(it.tag.longHash() to it.unread)
@ -603,14 +653,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(
@ -622,6 +664,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))
@ -639,7 +689,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
PrimaryDrawerItem()
.withName(R.string.action_about)
.withSelectable(false)
.withIcon(R.drawable.ic_info_outline)
.withIcon(R.drawable.ic_info_outline_white_24dp)
.withIconTintingEnabled(true)
.withOnDrawerItemClickListener { _, _, _ ->
LibsBuilder()
@ -696,36 +746,44 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
var sources: List<Source>?
fun sourcesApiCall() {
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)
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<Source>>?, 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(
@ -817,11 +875,51 @@ 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
}
}
}
})
@ -899,8 +997,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleListResult()
doGetAccordingToTab()
} else {
doGetAccordingToTab()
getAndStoreAllItems()
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
doGetAccordingToTab()
getAndStoreAllItems()
}
}
}
}
@ -928,7 +1028,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
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()) {
@ -956,24 +1056,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) {
@ -1041,7 +1145,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
fullHeightCards,
appColors,
debugReadingItems,
userIdentifier
userIdentifier,
config
) {
updateItems(it)
}
@ -1057,7 +1162,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
articleViewer,
debugReadingItems,
userIdentifier,
appColors
appColors,
config
) {
updateItems(it)
}
@ -1082,7 +1188,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) {
@ -1188,30 +1294,34 @@ 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()
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
}
return true
}
R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) {
@ -1220,7 +1330,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
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()
@ -1228,7 +1338,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
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>,
@ -1242,12 +1352,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
).show()
tabNewBadge.removeBadge()
tagsBadge = itemsByTag.map {
(it.key to ((tagsBadge[it.key] ?: it.value) - it.value))
}.toMap()
reloadTagsBadges()
handleDrawerItems()
getElementsAccordingToTab()
} else {
@ -1334,5 +1439,58 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
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

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

@ -1,12 +1,16 @@
package apps.amine.bou.readerforselfoss
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.preference.PreferenceManager
import androidx.multidex.MultiDexApplication
import android.widget.ImageView
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
@ -25,10 +29,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)
@ -41,10 +43,11 @@ import java.util.UUID.randomUUID
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
buildConfigClass = BuildConfig::class)
class MyApp : MultiDexApplication() {
private lateinit var config: Config
override fun onCreate() {
super.onCreate()
config = Config(baseContext)
initAmplify()
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
@ -59,6 +62,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?) {
@ -85,7 +107,7 @@ class MyApp : MultiDexApplication() {
tag: String?
) {
Glide.with(imageView?.context)
.load(uri)
.loadMaybeBasicAuth(config, uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView)
}

View File

@ -1,5 +1,7 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
@ -13,17 +15,22 @@ 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
@ -48,6 +55,11 @@ 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
@ -62,6 +74,8 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false)
}
private lateinit var editor: SharedPreferences.Editor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -70,11 +84,11 @@ class ReaderActivity : AppCompatActivity() {
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).build()
).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)
}
@ -82,16 +96,22 @@ class ReaderActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit()
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),
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
@ -103,7 +123,8 @@ class ReaderActivity : AppCompatActivity() {
readItem(allItems[currentItem])
pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem
}
@ -136,39 +157,46 @@ class ReaderActivity : AppCompatActivity() {
thread {
db.itemsDao().delete(item.toEntity())
}
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)
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)
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))
}
)
}
}
}
@ -191,7 +219,6 @@ class ReaderActivity : AppCompatActivity() {
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
FragmentStatePagerAdapter(fm) {
override fun getCount(): Int {
return allItems.size
}
@ -203,10 +230,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)
@ -217,68 +254,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()
notifyAdapter()
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()
notifyAdapter()
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

@ -1,5 +1,6 @@
package apps.amine.bou.readerforselfoss
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
@ -13,6 +14,8 @@ import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
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.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call
@ -53,12 +56,15 @@ class SourcesActivity : AppCompatActivity() {
super.onResume()
val mLayoutManager = LinearLayoutManager(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val api = SelfossApi(
this,
this@SourcesActivity,
prefs.getBoolean("isSelfSignedCert", false),
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
var items: ArrayList<Source> = ArrayList()
@ -66,34 +72,36 @@ class SourcesActivity : AppCompatActivity() {
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager
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>
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<Source>>, 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

@ -15,11 +15,15 @@ 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.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
@ -34,6 +38,7 @@ 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,
@ -47,6 +52,7 @@ class ItemCardAdapter(
override val appColors: AppColors,
override val debugReadingItems: Boolean,
override val userIdentifier: String,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
@ -65,6 +71,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)
@ -81,7 +88,7 @@ class ItemCardAdapter(
holder.mView.itemImage.setImageDrawable(null)
} else {
holder.mView.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
@ -94,7 +101,7 @@ class ItemCardAdapter(
.build(itm.sourcetitle.toTextDrawableString(c), color)
holder.mView.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.sourceImage)
}
holder.mView.favButton.isLiked = itm.starred
@ -116,53 +123,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

@ -5,10 +5,14 @@ import android.content.Context
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
@ -16,6 +20,8 @@ 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.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
@ -47,6 +53,7 @@ class ItemListAdapter(
override val debugReadingItems: Boolean,
override val userIdentifier: String,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
@ -67,6 +74,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()
@ -101,10 +110,10 @@ class ItemListAdapter(
holder.mView.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.itemImage)
}
} else {
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage)
}
}

View File

@ -11,8 +11,11 @@ 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.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 org.acra.ACRA
@ -29,6 +32,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
abstract val userIdentifier: String
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) {
@ -37,7 +41,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
updateItems(items)
}
private fun doUnmark(i: Item, position: Int) {
private fun unmarkSnackbar(i: Item, position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
@ -52,23 +56,28 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
notifyItemInserted(position)
updateItems(items)
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
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)
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
})
}
}
val view = s.view
@ -77,61 +86,177 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}
fun removeItemAtIndex(position: Int) {
private fun markSnackbar(i: Item, position: Int) {
val s = Snackbar
.make(
app.findViewById(R.id.coordLayout),
R.string.marked_as_unread,
Snackbar.LENGTH_LONG
)
.setAction(R.string.undo_string) {
items.add(position, i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemInserted(position)
updateItems(items)
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
}
}
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
}
fun handleItemAtIndex(position: Int) {
if (unreadItemStatusAtIndex(position)) {
readItemAtIndex(position)
} else {
unreadItemAtIndex(position)
}
}
fun unreadItemStatusAtIndex(position: Int): Boolean {
return items[position].unread
}
private fun readItemAtIndex(position: Int) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
// TODO: Handle network status.
// IF offline, delete from cached articles, and add to some table that will replay the calls on network activation.
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 =
"MARK 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()
}
unmarkSnackbar(i, position)
}
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(position, i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
private fun unreadItemAtIndex(position: Int) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"UNMARK 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()
}
markSnackbar(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_unread),
Toast.LENGTH_SHORT
).show()
items.add(i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().delete(i.toEntity())
}
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
})
}
}
fun addItemAtIndex(item: Item, position: Int) {

View File

@ -12,7 +12,9 @@ import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
@ -28,6 +30,7 @@ class SourcesListAdapter(
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(
@ -40,6 +43,7 @@ class SourcesListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position]
config = Config(c)
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.title)
@ -51,7 +55,7 @@ class SourcesListAdapter(
.build(itm.title.toTextDrawableString(c), color)
holder.mView.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.itemImage)
}
holder.mView.sourceTitle.text = itm.title
@ -70,33 +74,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)
}
}
}

View File

@ -45,7 +45,7 @@ data class Spout(
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

@ -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_stat_cloud_download_black_24dp)
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_tab_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

@ -3,10 +3,13 @@ package apps.amine.bou.readerforselfoss.fragments
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.InflateException
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment
@ -17,26 +20,35 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
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.glide.loadMaybeBasicAuth
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.succeeded
import apps.amine.bou.readerforselfoss.utils.toPx
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.*
import kotlinx.android.synthetic.main.fragment_article.view.*
import org.acra.ACRA
import retrofit2.Call
@ -44,12 +56,13 @@ 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
@ -58,259 +71,338 @@ class ArticleFragment : Fragment() {
private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var rootView: ViewGroup? = null
private lateinit var prefs: SharedPreferences
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
override fun onStop() {
super.onStop()
mCustomTabActivityHelper.unbindCustomTabsService(activity)
if (mCustomTabActivityHelper != null) {
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(activity!!)
config = Config(activity!!)
super.onCreate(savedInstanceState)
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
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()
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "14").toInt()
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16").toInt()
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val debugReadingItems = prefs.getBoolean("read_debug", false)
font = prefs.getString("reader_font", "")
if (font.isNotEmpty()) {
resId = context!!.resources.getIdentifier(font, "font", context!!.packageName)
typeface = try {
ResourcesCompat.getFont(context!!, resId)!!
} catch (e: java.lang.Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), context!!)
// Just to be sure
null
}
}
val api = SelfossApi(
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getBoolean("should_log_everything", false)
)
refreshAlignment()
fab = rootView.fab
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val debugReadingItems = prefs.getBoolean("read_debug", false)
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
val api = SelfossApi(
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
)
fab.rippleColor = appColors.colorAccentDark
fab = rootView!!.fab
val floatingToolbar: FloatingToolbar = rootView.floatingToolbar
floatingToolbar.attachFab(fab)
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark
val customTabsIntent = activity!!.buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper.bindCustomTabsService(activity)
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!!
)
R.id.unread_action -> 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!!)
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
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
}
}
)
override fun onItemLongClick(item: MenuItem?) {
rootView!!.source.text = contentSource
if (typeface != null) {
rootView!!.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
rootView!!.titleView.text = contentTitle
if (typeface != null) {
rootView!!.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE
Glide
.with(context!!)
.asBitmap()
.loadMaybeBasicAuth(config, 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
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 {
// 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) {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
}
} 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
rootView!!.titleView.text = response.body()!!.title
if (typeface != null) {
rootView!!.titleView.typeface = typeface
}
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()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.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
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = context!!.obtainStyledAttributes(resId, attrs)
rootView!!.webcontent.settings.standardFontFamily = a.getString(0)
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
@ -322,7 +414,7 @@ class ArticleFragment : Fragment() {
}
} else {
if (context != null) {
rootView.webcontent.setBackgroundColor(
rootView!!.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
R.color.light_webview
@ -346,15 +438,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
@ -366,10 +458,29 @@ class ArticleFragment : Fragment() {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
}
rootView.webcontent.loadDataWithBaseURL(
val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
rootView!!.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
|<head>
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <style>
| img {
| display: inline-block;
@ -384,13 +495,14 @@ class ArticleFragment : Fragment() {
| color: $stringTextColor;
| }
| * {
| font-size: ${fontSize.toPx}px;
| text-align: justify;
| font-size: ${fontSize}px;
| text-align: $textAlignment;
| word-break: break-word;
| overflow:hidden;
| line-height: 1.5em;
| }
| a, pre, code {
| text-align: left;
| text-align: $textAlignment;
| }
| pre, code {
| white-space: pre-wrap;
@ -398,9 +510,10 @@ class ArticleFragment : Fragment() {
| background-color: $stringBackgroundColor;
| }
| </style>
| $fontLinkAndStyle
|</head>
|<body>
| $c
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8",
@ -409,7 +522,7 @@ class ArticleFragment : Fragment() {
}
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

@ -2,15 +2,19 @@ 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], version = 2)
@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

@ -8,3 +8,9 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) {
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

@ -8,6 +8,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -24,6 +25,7 @@ import android.text.Editable;
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -124,6 +126,27 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
AppColors appColors = new AppColors(this);
if (appColors != null && appColors.isDarkTheme()) {
for (Header header : target) {
tryLoadIconDark(header);
}
}
}
private void tryLoadIconDark(Header header){
try{
if (header.fragmentArguments != null) {
String iconDark = header.fragmentArguments.getString("iconDark");
int iconDarkId = getResources().getIdentifier(iconDark, "drawable", getPackageName());
if (iconDarkId != 0) {
header.iconRes = iconDarkId;
}
}
} catch (Exception e) {
Log.e("SettingsActivity", "Can not load dark icon", e);
}
}
/**
@ -136,6 +159,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|| 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);
@ -384,6 +408,26 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
}
@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) {

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

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

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

@ -1,6 +1,7 @@
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
@ -18,7 +19,7 @@ fun SourceEntity.toView(): Source =
Source(
this.id,
this.title,
this.tags,
SelfossTagType(this.tags),
this.spout,
this.error,
this.icon
@ -28,7 +29,7 @@ fun Source.toEntity(): SourceEntity =
SourceEntity(
this.id,
this.title,
this.tags,
this.tags.tags,
this.spout,
this.error,
this.icon.orEmpty()
@ -53,7 +54,7 @@ fun ItemEntity.toView(): Item =
this.icon,
this.link,
this.sourcetitle,
this.tags
SelfossTagType(this.tags)
)
fun Item.toEntity(): ItemEntity =
@ -68,5 +69,5 @@ fun Item.toEntity(): ItemEntity =
this.icon,
this.link,
this.sourcetitle,
this.tags
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="#FFFFFFFF"
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
</vector>

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

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