Compare commits

...

86 Commits

Author SHA1 Message Date
1c57435f54 Store api version and select correct date formatter when offline. 2021-03-21 14:16:21 +01:00
4bb20a75d7 Move api check to MyApp. 2021-03-21 13:57:08 +01:00
1f20e19a97 Ensure api version has been fetched before checking. 2021-03-21 11:45:25 +01:00
9b372a45ce Global date formatter. 2021-03-20 07:58:08 +01:00
f4f8503037 Check new date format in items. 2021-03-19 17:24:25 +01:00
5b70ae138e Set date format according to api version. 2021-03-19 17:23:44 +01:00
626c9e2797 Migrate to View Binding. (#338) 2021-03-17 17:50:44 +01:00
05cd96afc0 New Crowdin updates (#333)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

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

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

* Use getItems

* Cache images of read articles

* Remove unused function

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

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

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

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

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

* Scale image dimension to fit the display

* Hide toolbar from Image view

* Add back button to the Image view

* Open one image on tap

* Allow zooming on images

* Revert to using Toolbar for navigation.

* Remove vibration when opening the Image view

* Do not open links associated with images

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

* Change image on swipe

* Store article images in cache in background.

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

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

* Remove redundant overrides

* Do not close the settings after resetting the themes.

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

* Remove unused imports.

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

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

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

* Formatted with ktlint

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

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Galician)

* New translations strings.xml (Spanish)

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

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

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Dutch)

* New translations strings.xml (French)

* New translations strings.xml (Galician)

* New translations strings.xml (German)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Spanish)

* New translations strings.xml (Turkish)

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

* New translations strings.xml (Japanese)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Swedish)

* New translations strings.xml (Spanish)

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

* New translations strings.xml (Russian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Polish)

* New translations strings.xml (Norwegian)

* New translations strings.xml (Korean)

* New translations strings.xml (Italian)

* New translations strings.xml (Afrikaans)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Hebrew)

* New translations strings.xml (Greek)

* New translations strings.xml (German)

* New translations strings.xml (French)

* New translations strings.xml (Finnish)

* New translations strings.xml (Dutch)

* New translations strings.xml (Danish)

* New translations strings.xml (Czech)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Arabic)

* New translations strings.xml (Galician)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

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

View File

@ -1,5 +1,13 @@
**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.
@ -18,6 +26,22 @@
- Closing #38. Only doing api calls on network available.
- Closing #298 and #287. Issues with Listview rendering
- Closing #290. Fixing back button issue in Settings
- Closing #300. Fixing issues when displaying some special characters.
- Closing #310. Some feeds don't have icons nor thumbnails.
- Closing #178. Expending images on tap.
- Closing #323. Old issue with textview not having the right color.
- Closing #324. Svg images loading crashes the app.
- Closing #322. App crashed because of svg images.
**1.6.x**
- Handling hidden tags.
@ -38,6 +62,8 @@
- Fixes #215, #208.
- Fixes #328.
**1.5.7.x**
- Added confirmation to the mark as read and update menues.

View File

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

View File

@ -2,7 +2,15 @@ buildscript {
}
def gitVersion() {
def process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
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()
}
@ -16,25 +24,26 @@ def versionNameFromGit() {
return gitVersion()
}
apply plugin: 'kotlin-kapt'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.3'
buildFeatures {
viewBinding true
}
defaultConfig {
applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16
targetSdkVersion 28
targetSdkVersion 30
versionCode versionCodeFromGit()
versionName versionNameFromGit()
@ -59,14 +68,11 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
debug {
buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
}
}
flavorDimensions "build"
@ -75,36 +81,32 @@ android {
versionNameSuffix '-github'
dimension "build"
}
storeConfig {
// As jenkins publishes to alpha first, this is the default suffix now.
versionNameSuffix '-store'
dimension "build"
}
}
}
dependencies {
// Testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta02'
androidTestImplementation 'androidx.test:runner:1.1.0-beta02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
androidTestImplementation 'androidx.test:runner:1.3.1-alpha02'
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-beta02'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0-alpha02'
// Espresso-intents for validation and stubbing of Intents
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-beta02'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0-alpha02'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support
implementation "androidx.appcompat:appcompat:$android_version"
implementation "com.google.android.material:material:$android_version"
implementation "androidx.recyclerview:recyclerview:$android_version"
implementation "androidx.appcompat:appcompat:1.3.0-alpha02"
implementation 'com.google.android.material:material:1.3.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation "androidx.vectordrawable:vectordrawable:$android_version"
implementation "androidx.browser:browser:$android_version"
implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:$android_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha2'
implementation 'org.jsoup:jsoup:1.13.1'
//multidex
implementation 'androidx.multidex:multidex:2.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
// About
implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
@ -118,7 +120,7 @@ dependencies {
implementation 'com.burgstaller:okhttp-digest:1.12'
// Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5'
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.1.0'
implementation 'com.github.jd-alexander:LikeButton:0.2.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
@ -126,11 +128,8 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.1.1'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
// Asking politely users to rate the app
implementation 'com.github.stkent:amplify:2.2.0'
// Drawer
implementation 'co.zsmb:materialdrawer-kt:2.0.1'
implementation 'co.zsmb:materialdrawer-kt:2.0.2'
// Themes
implementation 'com.52inc:scoops:1.0.0'
@ -140,34 +139,16 @@ dependencies {
// Pager
implementation 'me.relex:circleindicator:2.0.0@aar'
implementation 'androidx.core:core-ktx:1.0.0'
//PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
// Crash
implementation 'ch.acra:acra-http:5.2.1'
implementation 'ch.acra:acra-dialog:5.2.1'
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:2.3.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-runtime:2.3.0-alpha04"
kapt "androidx.room:room-compiler:2.3.0-alpha04"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}
afterEvaluate {
initAppLoginPropertiesIfNeeded()
}
def initAppLoginPropertiesIfNeeded() {
def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties')
if (!propertiesFile.exists()) {
def commentMessage = "This is autogenerated local property from system environment to prevent key to be committed to source control."
ant.propertyfile(file: System.getProperty("user.home") + "/.gradle/gradle.properties", comment: commentMessage) {
entry(key: "appLoginUrl", value: System.getProperty("appLoginUrl"))
entry(key: "appLoginUsername", value: System.getProperty("appLoginUsername"))
entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
}
}
}

View File

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

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
@ -19,11 +18,11 @@
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"
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
@ -38,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"
@ -56,15 +55,16 @@
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".ReaderActivity">
</activity>
<activity
android:name=".ImageActivity">
</activity>
<meta-data
android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
@ -77,6 +77,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
@ -22,11 +23,11 @@ import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_add_source.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import android.graphics.PorterDuff
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
@ -36,72 +37,76 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var api: SelfossApi
private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity)
super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_add_source)
setContentView(view)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
val drawable = nameInput.background
val drawable = binding.nameInput.background
drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
// TODO: clean
if(Build.VERSION.SDK_INT > 16) {
nameInput.background = drawable
binding.nameInput.background = drawable
} else{
nameInput.setBackgroundDrawable(drawable)
binding.nameInput.setBackgroundDrawable(drawable)
}
val drawable1 = sourceUri.background
val drawable1 = binding.sourceUri.background
drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) {
sourceUri.background = drawable1
binding.sourceUri.background = drawable1
} else{
sourceUri.setBackgroundDrawable(drawable1)
binding.sourceUri.setBackgroundDrawable(drawable1)
}
val drawable2 = tags.background
val drawable2 = binding.tags.background
drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) {
tags.background = drawable2
binding.tags.background = drawable2
} else{
tags.setBackgroundDrawable(drawable2)
binding.tags.setBackgroundDrawable(drawable2)
}
setSupportActionBar(toolbar)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
try {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
api = SelfossApi(
this,
this@AddSourceActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
} catch (e: IllegalArgumentException) {
mustLoginToAddSource()
}
maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput)
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
saveBtn.setTextColor(appColors.colorAccent)
binding.saveBtn.setTextColor(appColors.colorAccent)
saveBtn.setOnClickListener {
handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!)
binding.saveBtn.setOnClickListener {
handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api)
}
}
@ -109,10 +114,10 @@ class AddSourceActivity : AppCompatActivity() {
super.onResume()
val config = Config(this)
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false, this@AddSourceActivity)) {
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)
handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer)
}
}

View File

@ -4,47 +4,37 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import androidx.core.view.MenuItemCompat
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.recyclerview.widget.*
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import apps.amine.bou.readerforselfoss.adapters.ItemCardAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemsAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.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.api.selfoss.*
import apps.amine.bou.readerforselfoss.background.LoadingWorker
import apps.amine.bou.readerforselfoss.databinding.ActivityHomeBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.settings.SettingsActivity
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
@ -52,10 +42,8 @@ import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.bottombar.maybeShow
import apps.amine.bou.readerforselfoss.utils.bottombar.removeBadge
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
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
@ -67,9 +55,8 @@ import co.zsmb.materialdrawerkt.draweritems.profile.profile
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.bumptech.glide.Glide
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
@ -78,12 +65,10 @@ import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import 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.text.SimpleDateFormat
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
@ -101,8 +86,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var items: ArrayList<Item> = ArrayList()
private var allItems: ArrayList<Item> = ArrayList()
private var debugReadingItems = false
private var shouldLogEverything = false
private var internalBrowser = false
private var articleViewer = false
private var shouldBeCardView = false
@ -119,6 +102,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false
private var itemsCaching: Boolean = false
private var updateSources: Boolean = true
private var hiddenTags: List<String> = emptyList()
private var periodicRefresh = false
@ -138,6 +122,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var firstVisible: Int = 0
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var settings: SharedPreferences
private lateinit var binding: ActivityHomeBinding
private var recyclerAdapter: RecyclerView.Adapter<*>? = null
@ -152,6 +137,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var db: AppDatabase
private lateinit var config: Config
data class DrawerData(val tags: List<Tag>?, val sources: List<Source>?)
override fun onStart() {
@ -161,8 +148,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@HomeActivity)
config = Config(this@HomeActivity)
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false)
@ -171,19 +161,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
elementsShown = intent.getIntExtra("shortcutTab", UNREAD_SHOWN)
}
setContentView(R.layout.activity_home)
setContentView(view)
handleThemeBinding()
setSupportActionBar(toolBar)
if (savedInstanceState == null) {
Amplify.getSharedInstance().promptIfReady(promptView)
}
setSupportActionBar(binding.toolBar)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
customTabActivityHelper = CustomTabActivityHelper()
@ -195,8 +182,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
this@HomeActivity,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
shouldLogEverything
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
items = ArrayList()
allItems = ArrayList()
@ -205,15 +191,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleDrawer()
handleSwipeRefreshLayout()
handleSharedPrefs()
getElementsAccordingToTab()
}
private fun handleSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(
binding.swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1,
R.color.refresh_progress_2,
R.color.refresh_progress_3
)
swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
allItems = ArrayList()
lastFetchDone = false
@ -230,7 +220,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(
@ -250,14 +240,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val i = items.elementAtOrNull(position)
if (i != null) {
val adapter = recyclerView.adapter
val adapter = binding.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.tags.split(",").map { it.longHash() }
@ -284,7 +278,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(recyclerView)
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun handleBottomBar() {
@ -304,35 +298,34 @@ 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)
bottomBar
binding.bottomBar
.addItem(tabNew)
.addItem(tabArchive)
.addItem(tabStarred)
.setFirstSelectedPosition(0)
.initialise()
bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
binding.bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
if (fromTabShortcut) {
bottomBar.selectTab(elementsShown - 1)
binding.bottomBar.selectTab(elementsShown - 1)
}
}
@ -346,8 +339,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
editor = settings.edit()
handleSharedPrefs()
handleDrawerItems()
handleThemeUpdate()
@ -355,24 +346,20 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
reloadLayoutManager()
if (!infiniteScroll) {
recyclerView.setHasFixedSize(true)
binding.recyclerView.setHasFixedSize(true)
} else {
handleInfiniteScroll()
}
handleBottomBarActions()
getElementsAccordingToTab()
handleGDPRDialog(sharedPref.getBoolean("GDPR_shown", false))
handleRecurringTask()
handleOfflineActions()
}
private fun getAndStoreAllItems() {
api.allItems().enqueue(object : Callback<List<Item>> {
api.allNewItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
@ -380,18 +367,48 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
}
}
enqueueArticles(response, true)
}
})
api.allReadItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
enqueueArticles(response, false)
}
})
api.allStarredItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
enqueueArticles(response, false)
}
})
}
private fun enqueueArticles(response: Response<List<Item>>, clearDatabase: Boolean) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
if (clearDatabase) {
db.itemsDao().deleteAllItems()
}
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
}
}
}
override fun onStop() {
@ -400,27 +417,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
private fun handleSharedPrefs() {
debugReadingItems = sharedPref.getBoolean("read_debug", false)
shouldLogEverything = sharedPref.getBoolean("should_log_everything", false)
internalBrowser = sharedPref.getBoolean("prefer_internal_browser", true)
articleViewer = sharedPref.getBoolean("prefer_article_viewer", true)
shouldBeCardView = sharedPref.getBoolean("card_view_active", false)
displayUnreadCount = sharedPref.getBoolean("display_unread_count", true)
displayAllCount = sharedPref.getBoolean("display_other_count", false)
fullHeightCards = sharedPref.getBoolean("full_height_cards", false)
itemsNumber = sharedPref.getString("prefer_api_items_number", "200").toInt()
userIdentifier = sharedPref.getString("unique_id", "")
itemsNumber = sharedPref.getString("prefer_api_items_number", "200")!!.toInt()
userIdentifier = sharedPref.getString("unique_id", "")!!
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
itemsCaching = sharedPref.getBoolean("items_caching", false)
hiddenTags = if (sharedPref.getString("hidden_tags", "").isNotEmpty()) {
sharedPref.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",")
updateSources = sharedPref.getBoolean("update_sources", true)
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
sharedPref.getString("hidden_tags", "")!!.replace("\\s".toRegex(), "").split(",")
} else {
emptyList()
}
periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
refreshWhenChargingOnly = sharedPref.getBoolean("refresh_when_charging", false)
refreshMinutes = sharedPref.getString("periodic_refresh_minutes", "360").toLong()
refreshMinutes = sharedPref.getString("periodic_refresh_minutes", "360")!!.toLong()
if (refreshMinutes <= 15) {
refreshMinutes = 15
@ -429,7 +445,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun handleThemeBinding() {
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
@ -452,24 +468,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
drawer = drawer {
rootViewRes = R.id.drawer_layout
toolbar = toolBar
toolbar = binding.toolBar
actionBarDrawerToggleEnabled = true
actionBarDrawerToggleAnimated = true
showOnFirstLaunch = true
onSlide { _, p1 ->
bottomBar.alpha = (1 - p1)
binding.bottomBar.alpha = (1 - p1)
}
onClosed {
bottomBar.show()
binding.bottomBar.show()
}
onOpened {
bottomBar.hide()
binding.bottomBar.hide()
}
if (displayAccountHeader) {
accountHeader {
background = R.drawable.bg
profile(settings.getString("url", "")) {
profile(settings.getString("url", "")!!) {
iconDrawable = resources.getDrawable(R.mipmap.ic_launcher)
}
selectionListEnabledForSingleProfile = false
@ -478,7 +494,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))
@ -488,7 +504,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(
@ -518,7 +534,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
}
} else {
val filteredTags = maybeTags.filterNot { hiddenTags.contains(it.tag) }
val filteredTags = maybeTags
.filterNot { hiddenTags.contains(it.tag) }
.sortedBy { it.unread == 0 }
tagsBadge = filteredTags.map {
val gd = GradientDrawable()
val color = try {
@ -531,12 +549,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)
@ -547,6 +564,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)
@ -578,12 +600,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)
@ -594,6 +615,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)
@ -612,18 +638,27 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
} else {
for (tag in maybeSources) {
drawer.addItem(
CustomUrlPrimaryDrawerItem()
.withName(tag.title)
val item = PrimaryDrawerItem()
.withName(tag.getTitleDecoded())
.withIdentifier(tag.id.toLong())
.withIcon(tag.getIcon(this@HomeActivity))
.withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList()
maybeSourceFilter = tag
getElementsAccordingToTab()
false
}
)
if (tag.getIcon(this@HomeActivity).isNotBlank()) {
thread {
try {
item.withIcon(BitmapDrawable(resources, Glide.with(this@HomeActivity).asBitmap().load(tag.getIcon(this@HomeActivity)).submit(100, 100).get()))
} catch (e: Exception) {
}
}
} else {
item.withIcon(R.mipmap.ic_launcher)
}
drawer.addItem(item)
}
}
}
@ -680,17 +715,10 @@ 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()
.withActivityStyle(
if (appColors.isDarkTheme) {
Libs.ActivityStyle.DARK
} else {
Libs.ActivityStyle.LIGHT_DARK_TOOLBAR
}
)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(this@HomeActivity)
@ -703,6 +731,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (maybeDrawerData.tags != null) {
thread {
val tagEntities = maybeDrawerData.tags.map { it.toEntity() }
db.drawerDataDao().deleteAllTags()
db.drawerDataDao().insertAllTags(*tagEntities.toTypedArray())
}
}
@ -710,6 +739,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
thread {
val sourceEntities =
maybeDrawerData.sources.map { it.toEntity() }
db.drawerDataDao().deleteAllSources()
db.drawerDataDao().insertAllSources(*sourceEntities.toTypedArray())
}
}
@ -737,7 +767,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
var sources: List<Source>?
fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
api.sources.enqueue(object : Callback<List<Source>> {
override fun onResponse(
call: Call<List<Source>>?,
@ -760,7 +790,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
api.tags.enqueue(object : Callback<List<Tag>> {
override fun onResponse(
call: Call<List<Tag>>,
@ -794,7 +824,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
private fun reloadLayoutManager() {
val currentManager = recyclerView.layoutManager
val currentManager = binding.recyclerView.layoutManager
val layoutManager: RecyclerView.LayoutManager
// This will only update the layout manager if settings changed
@ -805,7 +835,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
calculateNoOfColumns()
)
recyclerView.layoutManager = layoutManager
binding.recyclerView.layoutManager = layoutManager
}
is GridLayoutManager ->
if (shouldBeCardView) {
@ -815,7 +845,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
recyclerView.layoutManager = layoutManager
binding.recyclerView.layoutManager = layoutManager
}
else ->
if (currentManager == null) {
@ -824,7 +854,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
calculateNoOfColumns()
)
recyclerView.layoutManager = layoutManager
binding.recyclerView.layoutManager = layoutManager
} else {
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
@ -832,7 +862,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
recyclerView.layoutManager = layoutManager
binding.recyclerView.layoutManager = layoutManager
}
} else {
}
@ -840,11 +870,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
private fun handleBottomBarActions() {
bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) {
val layoutManager = recyclerView.adapter
val layoutManager = binding.recyclerView.adapter
when (layoutManager) {
is StaggeredGridLayoutManager ->
@ -869,12 +899,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (itemsCaching) {
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
if (!binding.swipeRefreshLayout.isRefreshing) {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
val dbItems = db.itemsDao().items().map { it.toView() }.sortedByDescending {
SimpleDateFormat(dateTimeFormatter).parse(it.datetime)
}
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (position) {
@ -920,7 +952,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
val manager = recyclerView.layoutManager
val manager = binding.recyclerView.layoutManager
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
@ -936,17 +968,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
recyclerView.clearOnScrollListeners()
recyclerView.addOnScrollListener(recyclerViewScrollListener)
binding.recyclerView.clearOnScrollListeners()
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun mayBeEmpty() =
if (items.isEmpty()) {
emptyText.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
binding.emptyText.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
} else {
emptyText.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
binding.emptyText.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
}
private fun getElementsAccordingToTab(
@ -971,12 +1003,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (itemsCaching) {
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
if (!binding.swipeRefreshLayout.isRefreshing) {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
val dbItems = db.itemsDao().items().map { it.toView() }.sortedByDescending {
SimpleDateFormat(dateTimeFormatter).parse(it.datetime)
}
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (elementsShown) {
@ -1040,11 +1074,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleListResult(appendResults)
if (!appendResults) mayBeEmpty()
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
}
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
if (!binding.swipeRefreshLayout.isRefreshing) {
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = true }
}
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
@ -1058,7 +1092,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
Toast.makeText(
this@HomeActivity,
toastMessage,
@ -1067,7 +1101,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
})
} else {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = false }
binding.swipeRefreshLayout.post { binding.swipeRefreshLayout.isRefreshing = false }
}
}
@ -1112,7 +1146,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) {
val oldManager = recyclerView.layoutManager
val oldManager = binding.recyclerView.layoutManager
firstVisible = when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
@ -1135,8 +1169,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
articleViewer,
fullHeightCards,
appColors,
debugReadingItems,
userIdentifier
userIdentifier,
config
) {
updateItems(it)
}
@ -1150,21 +1184,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
customTabActivityHelper,
internalBrowser,
articleViewer,
debugReadingItems,
userIdentifier,
appColors
appColors,
config
) {
updateItems(it)
}
recyclerView.addItemDecoration(
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this@HomeActivity,
DividerItemDecoration.VERTICAL
)
)
}
recyclerView.adapter = recyclerAdapter
binding.recyclerView.adapter = recyclerAdapter
} else {
if (!appendResults) {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
@ -1315,7 +1349,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
val ids = allItems.map { it.id }
val itemsByTag: Map<Long, Int> =
allItems.flattenTags()
@ -1323,10 +1357,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.map { it.key to it.value.size }
.toMap()
fun readAllDebug(e: Throwable) {
ACRA.getErrorReporter().maybeHandleSilentException(e, this@HomeActivity)
}
if (ids.isNotEmpty() && this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.readAll(ids).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
@ -1341,12 +1371,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 {
@ -1356,17 +1381,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
Toast.LENGTH_SHORT
).show()
if (debugReadingItems) {
readAllDebug(
Throwable(
"Got response, but : response.body() (${response.body()}) != null && response.body()!!.isSuccess (${response.body()?.isSuccess})." +
"Request url was (${call.request().url()}), ids were $ids"
)
)
}
}
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
@ -1375,11 +1393,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
swipeRefreshLayout.isRefreshing = false
if (debugReadingItems) {
readAllDebug(t)
}
binding.swipeRefreshLayout.isRefreshing = false
}
})
items = ArrayList()
@ -1416,24 +1430,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
items = adapterItems
}
private fun handleGDPRDialog(GDPRShown: Boolean) {
val sharedEditor = sharedPref.edit()
if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ ->
sharedEditor.putBoolean("GDPR_shown", true)
sharedEditor.commit()
dialog.dismiss()
}
alertDialog.show()
}
}
private fun handleRecurringTask() {
if (periodicRefresh) {
val myConstraints = Constraints.Builder()
@ -1466,7 +1462,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
override fun onFailure(call: Call<T>, t: Throwable) {
ACRA.getErrorReporter().maybeHandleSilentException(t, this@HomeActivity)
}
})
}

View File

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

View File

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

View File

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

View File

@ -3,53 +3,43 @@ package apps.amine.bou.readerforselfoss
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.preference.PreferenceManager
import androidx.multidex.MultiDexApplication
import android.widget.ImageView
import androidx.multidex.MultiDexApplication
import apps.amine.bou.readerforselfoss.api.selfoss.ApiVersion
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
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
import com.github.stkent.amplify.feedback.DefaultEmailFeedbackCollector
import com.github.stkent.amplify.feedback.GooglePlayStoreFeedbackCollector
import com.github.stkent.amplify.tracking.Amplify
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import org.acra.ACRA
import org.acra.ReportField
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraDialog
import org.acra.annotation.AcraHttpSender
import org.acra.sender.HttpSender
import java.io.IOException
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.UUID.randomUUID
var dateTimeFormatter = "yyyy-MM-dd HH:mm:ss"
@AcraHttpSender(uri = "http://37.187.110.167/amine/acra/simplest-acra.php",
httpMethod = HttpSender.Method.POST)
@AcraDialog(resText = R.string.crash_dialog_text,
resCommentPrompt = R.string.crash_dialog_comment,
resTheme = android.R.style.Theme_DeviceDefault_Dialog)
@AcraCore(reportContent = [ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
buildConfigClass = BuildConfig::class)
class MyApp : MultiDexApplication() {
private lateinit var config: Config
private lateinit var api: SelfossApi
private lateinit var settings: SharedPreferences
private lateinit var sharedPref: SharedPreferences
private var apiVersionMajor: Int = 0
override fun onCreate() {
super.onCreate()
initAmplify()
config = Config(baseContext)
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "").isEmpty()) {
if (prefs.getString("unique_id", "")!!.isEmpty()) {
val editor = prefs.edit()
editor.putString("unique_id", randomUUID().toString())
editor.apply()
@ -62,6 +52,19 @@ class MyApp : MultiDexApplication() {
tryToHandleBug()
handleNotificationChannels()
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
apiVersionMajor = sharedPref.getInt("apiVersionMajor", 0)
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
api = SelfossApi(
this,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
getApiMajorVersion()
}
private fun handleNotificationChannels() {
@ -81,21 +84,6 @@ class MyApp : MultiDexApplication() {
}
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
ACRA.init(this)
ACRA.getErrorReporter().putCustomData("unique_id", prefs.getString("unique_id", ""))
}
private fun initAmplify() {
Amplify.initSharedInstance(this)
.setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
.setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
.applyAllDefaultRules()
}
private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(
@ -105,7 +93,7 @@ class MyApp : MultiDexApplication() {
tag: String?
) {
Glide.with(imageView?.context)
.load(uri)
.loadMaybeBasicAuth(config, uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView)
}
@ -141,4 +129,24 @@ class MyApp : MultiDexApplication() {
}
}
}
private fun getApiMajorVersion() {
api.apiVersion.enqueue(object : Callback<ApiVersion> {
override fun onFailure(call: Call<ApiVersion>, t: Throwable) {
if (apiVersionMajor >= 4) {
dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX"
}
}
override fun onResponse(call: Call<ApiVersion>, response: Response<ApiVersion>) {
val version = response.body() as ApiVersion
apiVersionMajor = version.getApiMajorVersion()
sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).commit()
if (apiVersionMajor >= 4) {
dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX"
}
}
})
}
}

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,27 +15,29 @@ 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.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_reader.*
import me.relex.circleindicator.CircleIndicator
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -42,7 +46,6 @@ import kotlin.concurrent.thread
class ReaderActivity : AppCompatActivity() {
private var markOnScroll: Boolean = false
private var debugReadingItems: Boolean = false
private var currentItem: Int = 0
private lateinit var userIdentifier: String
@ -51,6 +54,12 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1
val JUSTIFY = 1
val ALIGN_LEFT = 2
private fun showMenuItem(willAddToFavorite: Boolean) {
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
@ -65,38 +74,45 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false)
}
private lateinit var editor: SharedPreferences.Editor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_reader)
setContentView(view)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(toolBar)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
debugReadingItems = prefs.getBoolean("read_debug", false)
userIdentifier = prefs.getString("unique_id", "")
prefs = PreferenceManager.getDefaultSharedPreferences(this)
editor = prefs.edit()
userIdentifier = prefs.getString("unique_id", "")!!
markOnScroll = prefs.getBoolean("mark_on_scroll", false)
activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi(
this,
this@ReaderActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
if (allItems.isEmpty()) {
@ -107,9 +123,9 @@ class ReaderActivity : AppCompatActivity() {
readItem(allItems[currentItem])
pager.adapter =
binding.pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
pager.currentItem = currentItem
binding.pager.currentItem = currentItem
}
override fun onResume() {
@ -117,10 +133,10 @@ class ReaderActivity : AppCompatActivity() {
notifyAdapter()
pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager)
binding.pager.setPageTransformer(true, DepthPageTransformer())
(binding.indicator as CircleIndicator).setViewPager(binding.pager)
pager.addOnPageChangeListener(
binding.pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
@ -130,7 +146,7 @@ class ReaderActivity : AppCompatActivity() {
} else {
canFavorite()
}
readItem(allItems[pager.currentItem])
readItem(allItems[binding.pager.currentItem])
}
}
)
@ -148,18 +164,6 @@ class ReaderActivity : AppCompatActivity() {
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(
@ -169,10 +173,6 @@ class ReaderActivity : AppCompatActivity() {
thread {
db.itemsDao().insertAllItems(item.toEntity())
}
if (debugReadingItems) {
ACRA.getErrorReporter()
.maybeHandleSilentException(t, this@ReaderActivity)
}
}
}
)
@ -185,19 +185,19 @@ class ReaderActivity : AppCompatActivity() {
}
private fun notifyAdapter() {
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
(binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
if (markOnScroll) {
pager.clearOnPageChangeListeners()
binding.pager.clearOnPageChangeListeners()
}
}
override fun onSaveInstanceState(oldInstanceState: Bundle?) {
override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState)
oldInstanceState!!.clear()
oldInstanceState.clear()
}
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
@ -223,6 +223,11 @@ class ReaderActivity : AppCompatActivity() {
}
}
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)
@ -233,20 +238,25 @@ 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()
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
notifyAdapter()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
notifyAdapter()
canFavorite()
}
@ -258,7 +268,7 @@ class ReaderActivity : AppCompatActivity() {
}
R.id.save -> {
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.starrItem(allItems[pager.currentItem].id)
api.starrItem(allItems[binding.pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
@ -280,14 +290,14 @@ class ReaderActivity : AppCompatActivity() {
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, true, false))
afterSave()
}
}
}
R.id.unsave -> {
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.unstarrItem(allItems[pager.currentItem].id)
api.unstarrItem(allItems[binding.pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
@ -309,15 +319,34 @@ class ReaderActivity : AppCompatActivity() {
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.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
@ -11,11 +12,13 @@ import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -23,50 +26,54 @@ import retrofit2.Response
class SourcesActivity : AppCompatActivity() {
private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity)
super.onCreate(savedInstanceState)
binding = ActivitySourcesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(R.layout.activity_sources)
setContentView(view)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(toolbar)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
fab.rippleColor = appColors.colorAccentDark
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
binding.fab.rippleColor = appColors.colorAccentDark
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
}
override fun onStop() {
super.onStop()
recyclerView.clearOnScrollListeners()
binding.recyclerView.clearOnScrollListeners()
}
override fun onResume() {
super.onResume()
val mLayoutManager = LinearLayoutManager(this)
val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val api = SelfossApi(
this,
this@SourcesActivity,
prefs.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
var items: ArrayList<Source> = ArrayList()
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
api.sources.enqueue(object : Callback<List<Source>> {
@ -78,7 +85,7 @@ class SourcesActivity : AppCompatActivity() {
items = response.body() as ArrayList<Source>
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
recyclerView.adapter = mAdapter
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
@ -99,7 +106,7 @@ class SourcesActivity : AppCompatActivity() {
})
}
fab.setOnClickListener {
binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
}
}

View File

@ -4,19 +4,21 @@ import android.app.Activity
import android.content.Context
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView.ScaleType
import android.widget.Toast
import androidx.core.content.ContextCompat
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.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
@ -33,7 +35,6 @@ import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.card_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -49,8 +50,8 @@ class ItemCardAdapter(
private val articleViewer: Boolean,
private val fullHeightCards: Boolean,
override val appColors: AppColors,
override val debugReadingItems: Boolean,
override val userIdentifier: String,
override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
@ -59,68 +60,79 @@ class ItemCardAdapter(
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
return ViewHolder(v)
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position]
with(holder) {
val itm = items[position]
holder.mView.favButton.isLiked = itm.starred
holder.mView.title.text = Html.fromHtml(itm.title)
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
binding.favButton.isLiked = itm.starred
binding.title.text = itm.getTitleDecoded()
binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
binding.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setLinkTextColor(appColors.colorAccent)
binding.title.setLinkTextColor(appColors.colorAccent)
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
if (!fullHeightCards) {
holder.mView.itemImage.maxHeight = imageMaxHeight
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
if (!fullHeightCards) {
binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP
}
if (itm.getThumbnail(c).isEmpty()) {
binding.itemImage.visibility = View.GONE
Glide.with(c).clear(binding.itemImage)
binding.itemImage.setImageDrawable(null)
} else {
binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
}
binding.favButton.isLiked = itm.starred
}
if (itm.getThumbnail(c).isEmpty()) {
holder.mView.itemImage.visibility = View.GONE
Glide.with(c).clear(holder.mView.itemImage)
holder.mView.itemImage.setImageDrawable(null)
} else {
holder.mView.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.sourcetitle)
val drawable =
TextDrawable
.builder()
.round()
.build(itm.sourcetitle.toTextDrawableString(c), color)
holder.mView.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
}
holder.mView.favButton.isLiked = itm.starred
}
override fun getItemCount(): Int {
return items.size
}
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
mView.setCardBackgroundColor(appColors.cardBackgroundColor)
binding.root.setCardBackgroundColor(appColors.cardBackgroundColor)
handleClickListeners()
handleCustomTabActions()
}
private fun handleClickListeners() {
mView.favButton.setOnLikeListener(object : OnLikeListener {
binding.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
val (id) = items[bindingAdapterPosition]
if (c.isNetworkAccessible(null)) {
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
@ -133,7 +145,7 @@ class ItemCardAdapter(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = false
binding.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
@ -149,7 +161,7 @@ class ItemCardAdapter(
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[adapterPosition]
val (id) = items[bindingAdapterPosition]
if (c.isNetworkAccessible(null)) {
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
@ -162,7 +174,7 @@ class ItemCardAdapter(
call: Call<SuccessResponse>,
t: Throwable
) {
mView.favButton.isLiked = true
binding.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
@ -178,13 +190,13 @@ class ItemCardAdapter(
}
})
mView.shareBtn.setOnClickListener {
val item = items[adapterPosition]
c.shareLink(item.getLinkDecoded(), item.title)
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
}
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
@ -192,11 +204,11 @@ class ItemCardAdapter(
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
mView.setOnClickListener {
binding.root.setOnClickListener {
c.openItemUrl(
items,
adapterPosition,
items[adapterPosition].getLinkDecoded(),
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,

View File

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

View File

@ -13,11 +13,10 @@ import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -27,10 +26,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
abstract var items: ArrayList<Item>
abstract val api: SelfossApi
abstract val db: AppDatabase
abstract val debugReadingItems: Boolean
abstract val userIdentifier: String
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) {
@ -39,7 +38,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),
@ -69,12 +68,11 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
}
notifyItemRemoved(position)
updateItems(items)
doUnmark(i, position)
}
})
} else {
thread {
db.actionsDao().deleteReadActionForArticle(i.id)
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
}
}
@ -85,7 +83,64 @@ 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)
@ -101,33 +156,17 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (!response.succeeded() && debugReadingItems) {
val message =
"message: ${response.message()} " +
"response isSuccess: ${response.isSuccessful} " +
"response code: ${response.code()} " +
"response message: ${response.message()} " +
"response errorBody: ${response.errorBody()?.string()} " +
"body success: ${response.body()?.success} " +
"body isSuccess: ${response.body()?.isSuccess}"
ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
}
doUnmark(i, position)
unmarkSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (debugReadingItems) {
ACRA.getErrorReporter().maybeHandleSilentException(t, app)
Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
}
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(i)
items.add(position, i)
notifyItemInserted(position)
updateItems(items)
@ -139,7 +178,48 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
doUnmark(i, position)
}
}
}
private fun unreadItemAtIndex(position: Int) {
val i = items[position]
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
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>
) {
markSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
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))
}
}
}

View File

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

View File

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

View File

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

View File

@ -12,11 +12,12 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator
import com.burgstaller.okhttp.digest.Credentials
import com.burgstaller.okhttp.digest.DigestAuthenticator
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
@ -24,8 +25,7 @@ class SelfossApi(
c: Context,
callingActivity: Activity?,
isWithSelfSignedCert: Boolean,
timeout: Long,
shouldLog: Boolean
timeout: Long
) {
private lateinit var service: SelfossService
@ -62,6 +62,17 @@ class SelfossApi(
.maybeWithSelfSigned(isWithSelfSignedCert)
.authenticator(CachingAuthenticatorDecorator(this, authCache))
.addInterceptor(AuthenticationCacheInterceptor(authCache))
.addInterceptor(object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response: Response = chain.proceed(request)
if (response.code() == 408) {
return response
}
return response
}
})
}
init {
@ -83,15 +94,34 @@ class SelfossApi(
val logging = HttpLoggingInterceptor()
logging.level = if (shouldLog) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
logging.level = HttpLoggingInterceptor.Level.NONE
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
httpClient.addInterceptor(logging)
val timeoutCode = 504
httpClient
.addInterceptor { chain ->
val res = chain.proceed(chain.request())
if (res.code() == timeoutCode) {
throw SocketTimeoutException("timeout")
}
res
}
.addInterceptor(logging)
.addInterceptor { chain ->
val request = chain.request()
try {
chain.proceed(request)
} catch (e: SocketTimeoutException) {
Response.Builder()
.code(timeoutCode)
.protocol(Protocol.HTTP_2)
.body(ResponseBody.create(MediaType.parse("text/plain"), ""))
.message("")
.request(request)
.build()
}
}
try {
val retrofit =
@ -142,6 +172,15 @@ class SelfossApi(
fun allItems(): Call<List<Item>> =
service.allItems(userName, password)
fun allNewItems(): Call<List<Item>> =
getItems("unread", null, null, null, 200, 0)
fun allReadItems(): Call<List<Item>> =
getItems("read", null, null, null, 200, 0)
fun allStarredItems(): Call<List<Item>> =
getItems("read", null, null, null, 200, 0)
private fun getItems(
type: String,
tag: String?,
@ -176,6 +215,9 @@ class SelfossApi(
fun update(): Call<String> =
service.update(userName, password)
val apiVersion: Call<ApiVersion>
get() = service.version()
val sources: Call<List<Source>>
get() = service.sources(userName, password)

View File

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

View File

@ -103,6 +103,9 @@ internal interface SelfossService {
@Query("password") password: String
): Call<List<Source>>
@GET("api/about")
fun version(): Call<ApiVersion>
@DELETE("source/{id}")
fun deleteSource(
@Path("id") id: String,

View File

@ -19,11 +19,10 @@ import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.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
@ -46,7 +45,7 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_cloud_download)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
@ -58,17 +57,16 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1").toLong(),
sharedPref.getBoolean("should_log_everything", false)
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
api.allItems().enqueue(object : Callback<List<Item>> {
api.allNewItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
@ -79,41 +77,38 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize))
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
storeItems(response, true, notifyNewItems, notificationManager)
}
})
api.allReadItems().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>>
) {
storeItems(response, false, notifyNewItems, notificationManager)
}
})
api.allStarredItems().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>>
) {
storeItems(response, false, notifyNewItems, notificationManager)
}
})
thread {
val actions = db.actionsDao().actions()
@ -130,7 +125,47 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
}
}
}
return Result.SUCCESS
return Result.success()
}
private fun storeItems(response: Response<List<Item>>, newItems: Boolean, notifyNewItems: Boolean, notificationManager: NotificationManager) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
if (newItems) {
db.itemsDao().deleteAllItems()
}
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (newItems && 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())
}
}
apiItems.map { it.preloadImages(context) }
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
@ -145,7 +180,6 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
}
override fun onFailure(call: Call<T>, t: Throwable) {
ACRA.getErrorReporter().maybeHandleSilentException(t, context)
}
})
}

View File

@ -1,56 +1,63 @@
package apps.amine.bou.readerforselfoss.fragments
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.InflateException
import android.view.*
import android.webkit.*
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment
import androidx.core.content.ContextCompat
import androidx.core.widget.NestedScrollView
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.room.Room
import apps.amine.bou.readerforselfoss.ImageActivity
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.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.glide.getBitmapInputStream
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 com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.view.*
import org.acra.ACRA
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException
import java.net.URL
import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList
import kotlin.concurrent.thread
class ArticleFragment : Fragment() {
@ -63,10 +70,21 @@ class ArticleFragment : Fragment() {
private lateinit var contentSource: String
private lateinit var contentImage: String
private lateinit var contentTitle: String
private lateinit var allImages : ArrayList<String>
private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private lateinit var prefs: SharedPreferences
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
override fun onStop() {
super.onStop()
@ -76,64 +94,74 @@ class ArticleFragment : Fragment() {
}
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(activity!!)
appColors = AppColors(requireActivity())
config = Config(requireActivity())
super.onCreate(savedInstanceState)
pageNumber = arguments!!.getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
pageNumber = requireArguments().getInt(ARG_POSITION)
allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
db = Room.databaseBuilder(
context!!,
requireContext(),
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
}
private var rootView: ViewGroup? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
try {
rootView = inflater
.inflate(R.layout.fragment_article, container, false) as ViewGroup
_binding = FragmentArticleBinding.inflate(inflater, container, false)
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].title
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentTitle = allItems[pageNumber.toInt()].getTitleDecoded()
contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity())
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
allImages = allItems[pageNumber.toInt()].getImages()
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16").toInt()
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 = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
} catch (e: java.lang.Exception) {
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
// Just to be sure
null
}
}
refreshAlignment()
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val api = SelfossApi(
context!!,
activity!!,
requireContext(),
requireActivity(),
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1").toLong(),
prefs.getBoolean("should_log_everything", false)
prefs.getString("api_timeout", "-1")!!.toLong()
)
fab = rootView!!.fab
fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark
val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = activity!!.buildCustomTabsIntent()
val customTabsIntent = requireActivity().buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
@ -143,43 +171,29 @@ class ArticleFragment : Fragment() {
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(
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
false,
false,
activity!!
requireActivity()
)
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
R.id.unread_action -> if ((context != null && requireContext().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!!)
}
}
}
)
@ -197,29 +211,35 @@ class ArticleFragment : Fragment() {
}
)
rootView!!.source.text = contentSource
binding.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
rootView!!.titleView.text = contentTitle
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview(contentText, prefs)
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
rootView!!.imageView.visibility = View.VISIBLE
binding.imageView.visibility = View.VISIBLE
Glide
.with(context!!)
.with(requireContext())
.asBitmap()
.load(contentImage)
.loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
.into(binding.imageView)
} else {
rootView!!.imageView.visibility = View.GONE
binding.imageView.visibility = View.GONE
}
}
rootView!!.nestedScrollView.setOnScrollChangeListener(
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
fab.hide()
@ -230,33 +250,44 @@ class ArticleFragment : Fragment() {
)
} catch (e: InflateException) {
AlertDialog.Builder(context!!)
.setMessage(context!!.getString(R.string.webview_dialog_issue_message))
.setTitle(context!!.getString(R.string.webview_dialog_issue_title))
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { dialog, which ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.commit()
activity!!.finish()
requireActivity().finish()
}
.create()
.show()
}
return rootView
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun refreshAlignment() {
textAlignment = when (prefs.getInt("text_align", 1)) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
}
private fun getContentFromMercury(
customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences
) {
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
rootView!!.progressBar.visibility = View.VISIBLE
val parser = MercuryApi(
prefs.getBoolean("should_log_everything", false)
)
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> {
@ -268,57 +299,52 @@ class ArticleFragment : Fragment() {
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
rootView!!.titleView.text = response.body()!!.title
binding.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url)
url = response.body()!!.url
} catch (e: MalformedURLException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
// Mercury returned a relative url. We do nothing.
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
htmlToWebview(response.body()!!.content.orEmpty(), prefs)
contentText = response.body()!!.content.orEmpty()
htmlToWebview()
} 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
binding.imageView.visibility = View.VISIBLE
try {
Glide
.with(context!!)
.with(requireContext())
.asBitmap()
.load(response.body()!!.lead_image_url)
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.apply(RequestOptions.fitCenterTransform())
.into(rootView!!.imageView)
.into(binding.imageView)
} catch (e: Exception) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
} else {
rootView!!.imageView.visibility = View.GONE
binding.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
try {
rootView!!.nestedScrollView.scrollTo(0, 0)
binding.nestedScrollView.scrollTo(0, 0)
rootView!!.progressBar.visibility = View.GONE
binding.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
} else {
@ -326,13 +352,11 @@ class ArticleFragment : Fragment() {
openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
}
} catch (e: Exception) {
if (context != null) {
ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
}
}
}
@ -346,31 +370,36 @@ class ArticleFragment : Fragment() {
}
}
private fun htmlToWebview(c: String, prefs: SharedPreferences) {
private fun htmlToWebview() {
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
rootView!!.webcontent.visibility = View.VISIBLE
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
if (context != null) {
rootView!!.webcontent.setBackgroundColor(
binding.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
requireContext(),
R.color.dark_webview
)
)
Pair(ContextCompat.getColor(context!!, R.color.dark_webview_text), ContextCompat.getColor(context!!, R.color.light_webview_text))
Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview))
} else {
Pair(null, null)
}
} else {
if (context != null) {
rootView!!.webcontent.setBackgroundColor(
binding.webcontent.setBackgroundColor(
ContextCompat.getColor(
context!!,
requireContext(),
R.color.light_webview
)
)
Pair(ContextCompat.getColor(context!!, R.color.light_webview_text), ContextCompat.getColor(context!!, R.color.dark_webview_text))
Pair(ContextCompat.getColor(requireContext(), R.color.light_webview_text), ContextCompat.getColor(requireContext(), R.color.light_webview))
} else {
Pair(null, null)
}
@ -388,15 +417,56 @@ class ArticleFragment : Fragment() {
"#FFFFFF"
}
rootView!!.webcontent.settings.useWideViewPort = true
rootView!!.webcontent.settings.loadWithOverviewMode = true
rootView!!.webcontent.settings.javaScriptEnabled = false
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
binding.webcontent.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
return true
}
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.toLowerCase().contains(".jpg") || url.toLowerCase().contains(".jpeg")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
}catch ( e : ExecutionException) {}
}
else if (url.toLowerCase().contains(".png")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
}catch ( e : ExecutionException) {}
}
else if (url.toLowerCase().contains(".webp")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
}catch ( e : ExecutionException) {}
}
return super.shouldInterceptRequest(view, url)
}
}
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return performClick()
}
})
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
rootView!!.webcontent.settings.layoutAlgorithm =
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} else {
rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
binding.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
}
var baseUrl: String? = null
@ -405,10 +475,27 @@ class ArticleFragment : Fragment() {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
}
rootView!!.webcontent.loadDataWithBaseURL(
val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
binding.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
|<head>
@ -428,12 +515,20 @@ class ArticleFragment : Fragment() {
| }
| * {
| font-size: ${fontSize}px;
| text-align: justify;
| text-align: $textAlignment;
| word-break: break-word;
| overflow:hidden;
| line-height: 1.5em;
| background-color: $stringBackgroundColor;
| }
| body, html {
| background-color: $stringBackgroundColor !important;
| border-color: $stringBackgroundColor !important;
| padding: 0 !important;
| margin: 0 !important;
| }
| a, pre, code {
| text-align: left;
| text-align: $textAlignment;
| }
| pre, code {
| white-space: pre-wrap;
@ -441,9 +536,10 @@ class ArticleFragment : Fragment() {
| background-color: $stringBackgroundColor;
| }
| </style>
| $fontLinkAndStyle
|</head>
|<body>
| $c
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8",
@ -452,15 +548,15 @@ class ArticleFragment : Fragment() {
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
rootView!!.progressBar.visibility = View.GONE
activity!!.openItemUrl(
binding.progressBar.visibility = View.GONE
requireActivity().openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
true,
false,
activity!!
requireActivity()
)
}
@ -481,5 +577,20 @@ class ArticleFragment : Fragment() {
}
}
fun performClick(): Boolean {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages)
intent.putExtra("position", position)
startActivity(intent)
return false
}
return false
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}
/**
@ -137,7 +160,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|| 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);
}
@ -173,16 +195,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@ -223,58 +235,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class DebugPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_debug);
setHasOptionsMenu(true);
SharedPreferences pref = getActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE);
final String id = pref.getString("unique_id", "...");
final Preference identifier = findPreference("debug_identifier");
final ClipboardManager clipboard = (ClipboardManager)
getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
identifier.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (clipboard != null) {
ClipData clip = ClipData.newPlainText("Selfoss unique id", id);
clipboard.setPrimaryClip(clip);
Toast.makeText(getActivity(), R.string.unique_id_to_clipboard, Toast.LENGTH_LONG).show();
return true;
}
return false;
}
});
identifier.setTitle(id);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
/**
@ -318,16 +278,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@ -342,10 +292,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
} else if (id == R.id.clear) {
if (id == R.id.clear) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences.Editor editor = pref.edit();
editor.remove("color_primary");
@ -354,7 +301,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
editor.remove("color_accent_dark");
editor.remove("dark_theme");
editor.apply();
getActivity().finish();
getActivity().recreate();
}
return super.onOptionsItemSelected(item);
}
@ -373,16 +320,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
addPreferencesFromResource(R.xml.pref_offline);
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@ -393,16 +330,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
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);
}
}
@ -410,7 +337,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
super.onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -18,6 +18,7 @@ class AppColors(a: Activity) {
@ColorInt val colorAccentDark: Int
@ColorInt val cardBackgroundColor: Int
@ColorInt val colorBackground: Int
@ColorInt val textColor: Int
val isDarkTheme: Boolean
init {
@ -57,6 +58,12 @@ class AppColors(a: Activity) {
android.R.color.background_light
}
textColor = if (isDarkTheme) {
R.color.md_white_1000
} else {
R.color.md_grey_900
}
val wrapper = Context::class.java
val method = wrapper!!.getMethod("getThemeResId")
method.isAccessible = true

View File

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

View File

@ -11,19 +11,19 @@ class Config(c: Context) {
val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
val baseUrl: String
get() = settings.getString("url", "")
get() = settings.getString("url", "")!!
val userLogin: String
get() = settings.getString("login", "")
get() = settings.getString("login", "")!!
val userPassword: String
get() = settings.getString("password", "")
get() = settings.getString("password", "")!!
val httpUserLogin: String
get() = settings.getString("httpUserName", "")
get() = settings.getString("httpUserName", "")!!
val httpUserPassword: String
get() = settings.getString("httpPassword", "")
get() = settings.getString("httpPassword", "")!!
companion object {
const val settingsName = "paramsselfoss"

View File

@ -4,7 +4,7 @@ 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 apps.amine.bou.readerforselfoss.dateTimeFormatter
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
@ -15,7 +15,6 @@ fun String.toTextDrawableString(c: Context): String {
try {
textDrawable.append(s[0])
} catch (e: StringIndexOutOfBoundsException) {
ACRA.getErrorReporter().maybeHandleSilentException(e, c)
}
}
return textDrawable.toString()
@ -24,7 +23,7 @@ fun String.toTextDrawableString(c: Context): String {
fun Item.sourceAndDateText(): String {
val formattedDate: String = try {
" " + DateUtils.getRelativeTimeSpanString(
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
SimpleDateFormat(dateTimeFormatter).parse(this.datetime).time,
Date().time,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
@ -34,7 +33,7 @@ fun Item.sourceAndDateText(): String {
""
}
return this.sourcetitle + formattedDate
return this.getSourceTitle() + formattedDate
}
fun Item.toggleStar(): Item {

View File

@ -20,7 +20,6 @@ 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 {
@ -140,7 +139,7 @@ private fun openInBrowser(linkDecoded: String, app: Activity) {
fun String.isUrlValid(): Boolean =
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean {
fun String.isBaseUrlValid(ctx: Context): Boolean {
val baseUrl = HttpUrl.parse(this)
var existsAndEndsWithSlash = false
if (baseUrl != null) {
@ -148,11 +147,7 @@ fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean {
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
}
val isValid = Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
if (!isValid && logErrors) {
ACRA.getErrorReporter().doHandleSilentException(java.lang.Exception("Patterns.WEB_URL.matcher(this).matches() == ${Patterns.WEB_URL.matcher(this).matches()} && existsAndEndsWithSlash == $existsAndEndsWithSlash && baseUrl.pathSegments() == ${baseUrl?.pathSegments()}"), ctx)
}
return isValid
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
}
fun Context.openInBrowserAsNewTask(i: Item) {

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 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

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